Skip to content

Commit

Permalink
adding vault access code base
Browse files Browse the repository at this point in the history
  • Loading branch information
Vivek Reddy committed Oct 18, 2024
1 parent fe4d40c commit e83b4a2
Show file tree
Hide file tree
Showing 6 changed files with 344 additions and 202 deletions.
319 changes: 319 additions & 0 deletions pkg/splunk/client/vault_setup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,319 @@
// Copyright (c) 2018-2022 Splunk Inc. All rights reserved.

//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific
//
// Package enterprise provides functionality for integrating with Vault and managing secrets for Splunk Enterprise deployments.
//
// This package includes the following key components:
//
// - SecretData: Represents the structure of a secret's data.
// - Data: Wraps SecretData to represent the data field in a Vault response.
// - Metadata: Contains metadata information about a secret, such as creation time, deletion time, and version.
// - VaultResponse: Represents the structure of a response from a Vault request, including request ID, lease details, data, and metadata.
// - VaultError: Represents the structure of an error response from Vault.
//
// Functions:
//
// - InjectVaultSecret: Adds Vault injection annotations to the StatefulSet Pods deployed by the Splunk Operator. It validates the Vault configuration, constructs the necessary annotations, and applies them to the PodTemplateSpec.
// - CheckAndRestartStatefulSet: Checks if the password version in Vault has changed and restarts the StatefulSet if needed. It authenticates with Vault, retrieves secret metadata, compares versions, and updates the StatefulSet annotations to trigger a rolling restart if any secret version has changed.

package client

import (
"context"
//"encoding/json"
"fmt"
"os"
"strconv"

"time"

//vault "github.com/hashicorp/vault/api"
"math/rand"

"github.com/go-resty/resty/v2"

Check failure on line 44 in pkg/splunk/client/vault_setup.go

View workflow job for this annotation

GitHub Actions / check-formating

no required module provides package github.com/go-resty/resty/v2; to add it:
enterpriseApi "github.com/splunk/splunk-operator/api/v4"
splcommon "github.com/splunk/splunk-operator/pkg/splunk/common"
appsv1 "k8s.io/api/apps/v1"
//corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
// Marshal the splunkConfig to JSON
"gopkg.in/yaml.v2"
)

var logger = log.Log.WithName("vault_setup")

type SecretData struct {
Value string `json:"value,omitempty"`
}

type Data struct {
Data SecretData `json:"data,omitempty"`
}

// Metadata contains metadata information about a secret, such as creation time,
// deletion time, and version. It also includes custom metadata and a flag indicating
// whether the secret has been destroyed.
type Metadata struct {
CreatedTime string `json:"created_time,omitempty"`
CustomMetadata interface{} `json:"custom_metadata,omitempty"`
DeletionTime string `json:"deletion_time,omitempty"`
Destroyed bool `json:"destroyed,omitempty"`
Version int `json:"version,omitempty"`
}

// VaultResponse represents the structure of a response from a Vault request.
// It includes details such as the request ID, lease ID, lease duration, and
// whether the lease is renewable. It also contains nested Data and Metadata
// structures that hold additional information returned by the Vault.
type VaultResponse struct {
RequestId string `json:"request_id,omitempty"`
LeaseId string `json:"lease_id,omitempty"`
Renewable bool `json:"renewable,omitempty"`
LeaseDuration int `json:"lease_duration,omitempty"`
Data Data `json:"data,omitempty"`
Metadata Metadata `json:"metadata,omitempty"`
}

type VaultError struct {
Errors []string `json:"errors,omitempty"`
}

func InjectVaultSecret(ctx context.Context, client splcommon.ControllerClient, statefulSet *appsv1.StatefulSet, vaultSpec *enterpriseApi.VaultIntegration) error {
logger.Info("InjectVaultSecret called", "vaultSpec", vaultSpec)

latestStatefulSet := &appsv1.StatefulSet{}
namedNamespace := types.NamespacedName{Name: statefulSet.Name, Namespace: statefulSet.Namespace}
err := client.Get(ctx, namedNamespace, latestStatefulSet)
if errors.IsNotFound(err) {
latestStatefulSet = statefulSet
} else if err != nil {
logger.Error(err, "Failed to get the latest StatefulSet", "statefulSet", statefulSet.Name)
return fmt.Errorf("failed to get the latest StatefulSet: %v", err)
}

podTemplateSpec := statefulSet.Spec.Template
if !vaultSpec.Enable {
logger.Info("Vault integration is disabled")
return nil
}

// Validate if role and secretPath are provided
if vaultSpec.Role == "" {
logger.Error(fmt.Errorf("vault role is required when vault is enabled"), "Missing vault role")
return fmt.Errorf("vault role is required when vault is enabled")
}
if vaultSpec.SecretPath == "" {
logger.Error(fmt.Errorf("vault secretPath is required when vault is enabled"), "Missing vault secretPath")
return fmt.Errorf("vault secretPath is required when vault is enabled")
}

secretPath := vaultSpec.SecretPath
vaultRole := vaultSpec.Role
secretKeyToEnv := []string{
"hec_token",
"idxc_secret",
"pass4SymmKey",
"password",
"shc_secret",
}

// Adding annotations for vault injection
annotations := map[string]string{
"vault.hashicorp.com/agent-inject": "true",
"vault.hashicorp.com/agent-inject-path": "/mnt/splunk-secrets",
"vault.hashicorp.com/role": vaultRole,
}

splunkConfig := map[string]interface{}{
"splunk": map[string]interface{}{
"hec_disabled": 0,
"hec_enableSSL": 0,
"hec_token": `{{- with secret "secret/data/splunk/hec_token" -}}{{ .Data.data.value }}{{- end }}`,
"password": `{{- with secret "secret/data/splunk/password" -}}{{ .Data.data.value }}{{- end }}`,
"pass4SymmKey": `{{- with secret "secret/data/splunk/pass4SymmKey" -}}{{ .Data.data.value }}{{- end }}`,
"idxc": map[string]interface{}{
"secret": `{{- with secret "secret/data/splunk/idxc_secret" -}}{{ .Data.data.value }}{{- end }}`,
},
"shc": map[string]interface{}{
"secret": `{{- with secret "secret/data/splunk/shc_secret" -}}{{ .Data.data.value }}{{- end }}`,
},
},
}

splunkConfigYAML, err := yaml.Marshal(splunkConfig)
if err != nil {
return err
}
// Convert JSON to string for annotation
splunkConfigString := string(splunkConfigYAML)

// Adding annotations to indicate specific secrets to be injected as separate files
// Adding annotation for default configuration file
annotations["vault.hashicorp.com/agent-inject-file-defaults"] = "default.yml"
annotations["vault.hashicorp.com/secret-volume-path-defaults"] = "/mnt/splunk-secrets"
annotations["vault.hashicorp.com/agent-inject-template-defaults"] = splunkConfigString
for _, key := range secretKeyToEnv {
annotationKey := fmt.Sprintf("vault.hashicorp.com/agent-inject-secret-%s", key)
annotations[annotationKey] = fmt.Sprintf("%s/%s", secretPath, key)
annotationFile := fmt.Sprintf("vault.hashicorp.com/agent-inject-file-%s", key)
annotations[annotationFile] = key
annotationVolumeKey := fmt.Sprintf("vault.hashicorp.com/secret-volume-path-%s", key)
annotations[annotationVolumeKey] = fmt.Sprintf("/mnt/splunk-secrets/%s", key)
}

// Apply these annotations to the StatefulSet PodTemplateSpec without overwriting existing ones
if podTemplateSpec.ObjectMeta.Annotations == nil {
podTemplateSpec.ObjectMeta.Annotations = make(map[string]string)
}
for key, value := range annotations {
if existingValue, exists := latestStatefulSet.Spec.Template.ObjectMeta.Annotations[key]; !exists || existingValue == "" {
podTemplateSpec.ObjectMeta.Annotations[key] = value
} else {
podTemplateSpec.ObjectMeta.Annotations[key] = existingValue
}
}

logger.Info("Vault annotations added to PodTemplateSpec", "annotations", annotations)
return nil
}

// CheckAndRestartStatefulSet checks the versions of specified secrets in Vault and updates the StatefulSet
// annotations to trigger a rolling restart if any secret version has changed.
//
// Parameters:
// - ctx: The context for the operation.
// - kubeClient: The Kubernetes client to interact with the cluster.
// - statefulSet: The StatefulSet to be checked and potentially updated.
// - vaultIntegration: The Vault integration configuration containing the Vault address, role, and secret path.
//
// Returns:
// - error: An error if the operation fails, otherwise nil.
//
// The function performs the following steps:
// 1. Initializes a Vault client and reads the Kubernetes service account token.
// 2. Authenticates with Vault using the Kubernetes auth method.
// 3. Iterates over specified keys to check if any secret version has changed in Vault.
// 4. Updates the StatefulSet annotations to trigger a rolling restart if any secret version has changed.
func CheckAndRestartStatefulSet(ctx context.Context, kubeClient client.Client, statefulSet *appsv1.StatefulSet, vaultIntegration *enterpriseApi.VaultIntegration) (bool, error) {

logger.Info("CheckAndRestartStatefulSet called", "statefulSet", statefulSet.Name, "vaultIntegration", vaultIntegration)

// Get the latest version of the StatefulSet from the Kubernetes API
latestStatefulSet := &appsv1.StatefulSet{}
err := kubeClient.Get(ctx, client.ObjectKey{
Name: statefulSet.Name,
Namespace: statefulSet.Namespace,
}, latestStatefulSet)

if errors.IsNotFound(err) {
latestStatefulSet = statefulSet
} else if err != nil {
logger.Error(err, "Failed to get the latest StatefulSet", "statefulSet", statefulSet.Name)
return false, fmt.Errorf("failed to get the latest StatefulSet: %v", err)
}

// Initialize Vault client
client := resty.New()
client.SetDebug(true) //FIXME TODO remove once code complete

// Read the Kubernetes service account token
tokenFile := "/var/run/secrets/kubernetes.io/serviceaccount/token"
token, err := os.ReadFile(tokenFile)
if err != nil {
logger.Error(err, "Failed to read service account token")
return false, fmt.Errorf("failed to read service account token: %v", err)
}

// Authenticate with Vault using the Kubernetes auth method
data := map[string]interface{}{
"role": vaultIntegration.Role,
"jwt": string(token),
}
var authResponse map[string]interface{}
resp, err := client.R().
SetBody(data).
SetResult(&authResponse).
Post(fmt.Sprintf("%s/v1/auth/kubernetes/login", vaultIntegration.Address))
if err != nil {
logger.Error(err, "Failed to authenticate with Vault")
return false, fmt.Errorf("failed to authenticate with Vault: %v", err)
}
if resp.StatusCode() != 200 {
logger.Error(fmt.Errorf("failed to authenticate with Vault"), "Vault authentication failed", "response", resp.String())
return false, fmt.Errorf("failed to authenticate with Vault: %v", resp.String())
}

// Set the client token after successful authentication
tokenValue := authResponse["auth"].(map[string]interface{})["client_token"].(string)
logger.Info("Authenticated with Vault", "client_token", tokenValue)

// Define the keys to be checked.
keys := []string{"password", "hec_token", "idxc_secret", "pass4SymmKey", "shc_secret"}

// Iterate over each specified key and check if any version has changed
updated := false
for _, key := range keys {
// Construct the metadata path for each key
metadataPath := fmt.Sprintf("%s/%s", vaultIntegration.SecretPath, key)
if vaultIntegration.SecretPath[len(vaultIntegration.SecretPath)-1] == '/' {
metadataPath = fmt.Sprintf("%smetadata/%s", vaultIntegration.SecretPath, key)
}
vaultError := &VaultError{}
// Read the secret metadata from Vault to get the version
var metadataResponse VaultResponse
resp, err := client.R().
SetHeader("X-Vault-Token", tokenValue).
SetResult(&metadataResponse).
SetError(vaultError).
ForceContentType("application/json").
Get(fmt.Sprintf("%s/v1/%s", vaultIntegration.Address, metadataPath))
if err != nil {
logger.Error(err, "Failed to read secret metadata from Vault", "metadataPath", metadataPath)
return false, fmt.Errorf("failed to read secret metadata from Vault: %v", err)
}
if resp.StatusCode() != 200 {
logger.Error(fmt.Errorf("failed to read secret metadata from Vault"), "Vault metadata read failed", "response", vaultError)
return false, fmt.Errorf("failed to read secret metadata from Vault: %v", vaultError)
}

version := metadataResponse.Metadata.Version

// Get the current version from the StatefulSet annotations
annotationKey := fmt.Sprintf("vault-secret-version-%s", key)
currentVersion := latestStatefulSet.Spec.Template.Annotations[annotationKey]

// If the version has changed, update the StatefulSet to trigger a rolling restart
if currentVersion != strconv.Itoa(int(version)) {
if statefulSet.Spec.Template.Annotations == nil {
statefulSet.Spec.Template.Annotations = make(map[string]string)
}
statefulSet.Spec.Template.Annotations[annotationKey] = strconv.Itoa(int(version))
updated = true
logger.Info("Secret version changed", "key", key, "newVersion", version, "oldVersion", currentVersion)
}
}

// If any secret version has changed, update the StatefulSet to trigger a rolling restart
if updated {
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
statefulSet.Spec.Template.Annotations["checksum/config"] = fmt.Sprintf("%d", rng.Int())
logger.Info("StatefulSet updated to trigger rolling restart", "statefulSet", statefulSet.Name)
return true, nil
}

return false, nil
}
9 changes: 2 additions & 7 deletions pkg/splunk/enterprise/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -806,13 +806,7 @@ func updateSplunkPodTemplateWithConfig(ctx context.Context, client splcommon.Con
splunkDefaults = fmt.Sprintf("%s,%s", "/mnt/splunk-defaults/default.yml", splunkDefaults)
}

if spec.VaultIntegration.Enable {
//The InjectVaultSecret function is responsible for injecting secrets from HashiCorp Vault into the specified pod template. This function takes the following parameters:
InjectVaultSecret(ctx, client, podTemplateSpec, &spec.VaultIntegration)
// Injects the secret monitor sidecar container into the PodTemplateSpec.
// This function is responsible for adding a sidecar container to the PodTemplateSpec that monitors the specified secret for changes.
AddSecretMonitorSidecarContainer(ctx, client, cr.GetNamespace(), podTemplateSpec)
} else {
if !spec.VaultIntegration.Enable {
// Explicitly set the default value here so we can compare for changes correctly with current statefulset.
secretVolDefaultMode := int32(corev1.SecretVolumeSourceDefaultMode)
addSplunkVolumeToTemplate(podTemplateSpec, "mnt-splunk-secrets", "/mnt/splunk-secrets", corev1.VolumeSource{
Expand Down Expand Up @@ -1070,6 +1064,7 @@ func updateSplunkPodTemplateWithConfig(ctx context.Context, client splcommon.Con
},
}
}

}

func removeDuplicateEnvVars(sliceList []corev1.EnvVar) []corev1.EnvVar {
Expand Down
Loading

0 comments on commit e83b4a2

Please sign in to comment.