chore: clean up vault process

This commit is contained in:
eding 2025-08-30 16:45:57 +02:00
parent fc904df7f0
commit 93f37bc9b6
2 changed files with 203 additions and 269 deletions

View file

@ -89,7 +89,7 @@ func runInitRepo(cmd *cobra.Command, args []string) {
} }
func runCreateDockerSecret(cmd *cobra.Command, args []string) { func runCreateDockerSecret(cmd *cobra.Command, args []string) {
if err := createDockerRegistrySecret(); err != nil { if err := createAndStoreSecrets(); err != nil {
fmt.Printf("[ERROR] Failed to create Docker registry secret: %v\n", err) fmt.Printf("[ERROR] Failed to create Docker registry secret: %v\n", err)
os.Exit(1) os.Exit(1)
} }

470
vault.go
View file

@ -13,6 +13,22 @@ import (
"golang.org/x/term" "golang.org/x/term"
) )
// --- Constants for Vault and Kubernetes ---
const (
vaultNamespace = "vault"
vaultPodLabelSelector = "app.kubernetes.io/name=vault"
vaultRootTokenSecretName = "vault-unseal-keys"
vaultRootTokenSecretKey = "vault-root"
ghcrSecretPath = "secret/ghcr-auth"
orgManagementSecretPath = "secret/org-management-pat"
dockerConfigJSONKey = ".dockerconfigjson"
orgManagementPATKey = "ORG_MANAGEMENT_PAT"
ghcrRegistry = "ghcr.io"
defaultVaultAddress = "https://127.0.0.1:8200"
)
// --- Structs for Docker Config ---
// DockerConfig represents the structure of a Docker config.json // DockerConfig represents the structure of a Docker config.json
type DockerConfig struct { type DockerConfig struct {
Auths map[string]DockerAuth `json:"auths"` Auths map[string]DockerAuth `json:"auths"`
@ -25,327 +41,245 @@ type DockerAuth struct {
Auth string `json:"auth"` Auth string `json:"auth"`
} }
// VaultSecret represents the structure for storing in Vault // --- VaultManager to Encapsulate Logic ---
type VaultSecret struct {
DockerConfigJSON string `json:".dockerconfigjson"` // VaultManager handles all interactions with Kubernetes and Vault.
type VaultManager struct {
podName string
rootToken string
} }
// OrgManagementSecret represents the organization management PAT secret // NewVaultManager creates and initializes a new VaultManager.
type OrgManagementSecret struct { func NewVaultManager() (*VaultManager, error) {
OrgManagementPAT string `json:"ORG_MANAGEMENT_PAT"` vm := &VaultManager{}
}
// createDockerRegistrySecret creates a Docker registry secret in Vault fmt.Println("[INFO] Checking dependencies (kubectl)...")
func createDockerRegistrySecret() error {
// Check dependencies
fmt.Println("[INFO] Checking dependencies (kubectl and vault)...")
if !commandExists("kubectl") { if !commandExists("kubectl") {
return fmt.Errorf("'kubectl' must be installed and in your PATH") return nil, fmt.Errorf("'kubectl' must be installed and in your PATH")
} }
// Check kubectl context fmt.Println("[INFO] Finding Vault pod...")
fmt.Println("[INFO] Checking Kubernetes context...") podName, err := vm.getVaultPodName()
if err := runCommandQuiet("kubectl", "get", "nodes"); err != nil {
return fmt.Errorf("unable to connect to Kubernetes cluster. Please check your kubectl configuration")
}
// Check if vault namespace exists
fmt.Println("[INFO] Checking if 'vault' namespace exists...")
if err := runCommandQuiet("kubectl", "get", "namespace", "vault"); err != nil {
fmt.Println("[WARNING] 'vault' namespace does not exist. Please ensure Vault is properly installed in your cluster.")
return fmt.Errorf("vault namespace not found")
}
// Get user input
username, token, orgToken, err := getUserInput()
if err != nil { if err != nil {
return fmt.Errorf("failed to get user input: %v", err) return nil, err
} }
vm.podName = podName
fmt.Printf("[INFO] Using Vault pod: %s\n", vm.podName)
// Create Docker config fmt.Println("[INFO] Retrieving Vault root token...")
dockerConfig, err := createDockerConfig(username, token) rootToken, err := vm.getVaultRootToken()
if err != nil { if err != nil {
return fmt.Errorf("failed to create Docker config: %v", err) return nil, err
}
vm.rootToken = rootToken
fmt.Println("[SUCCESS] Found Vault root token.")
fmt.Println("[INFO] Verifying Vault connection and token...")
if err := vm.verifyVaultToken(); err != nil {
return nil, fmt.Errorf("failed to validate Vault token: %w", err)
}
fmt.Println("[SUCCESS] Vault token is valid.")
return vm, nil
}
// getVaultPodName finds a running Vault pod in the cluster.
func (vm *VaultManager) getVaultPodName() (string, error) {
// Use `runCommandQuiet` to check for the existence of common pod names.
commonNames := []string{"vault-0", "vault-1", "vault-2"}
for _, name := range commonNames {
if err := runCommandQuiet("kubectl", "get", "pod", name, "-n", vaultNamespace); err == nil {
return name, nil
}
} }
// Store Docker registry secret in Vault // For getting the name via label, we MUST capture the command's output,
fmt.Println("[INFO] Storing Docker registry secret in Vault...") // so we use exec.Command().Output() directly.
if err := storeInVault(dockerConfig); err != nil { cmd := exec.Command("kubectl", "get", "pods", "-n", vaultNamespace, "-l", vaultPodLabelSelector, "-o", "jsonpath={.items[0].metadata.name}")
return fmt.Errorf("failed to store Docker registry secret in Vault: %v", err) output, err := cmd.Output()
if err == nil && len(output) > 0 {
return string(output), nil
} }
// Store organization management PAT in Vault return "", fmt.Errorf("could not find any running Vault pods in the '%s' namespace", vaultNamespace)
fmt.Println("[INFO] Storing organization management PAT in Vault...") }
if err := storeOrgManagementPAT(orgToken); err != nil {
return fmt.Errorf("failed to store organization management PAT in Vault: %v", err) // getVaultRootToken retrieves the Vault root token from the Kubernetes secret.
func (vm *VaultManager) getVaultRootToken() (string, error) {
// We need to capture the secret data from stdout, so we must use exec.Command().Output().
// The utility functions in utils.go do not support this.
cmd := exec.Command("kubectl", "get", "secret", vaultRootTokenSecretName, "-n", vaultNamespace, "-o", fmt.Sprintf("jsonpath={.data.%s}", vaultRootTokenSecretKey))
output, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("failed to get '%s' secret from '%s' namespace: %w", vaultRootTokenSecretName, vaultNamespace, err)
}
if len(output) == 0 {
return "", fmt.Errorf("key '%s' not found in secret '%s'", vaultRootTokenSecretKey, vaultRootTokenSecretName)
}
decoded, err := base64.StdEncoding.DecodeString(string(output))
if err != nil {
return "", fmt.Errorf("failed to decode base64 root token: %w", err)
}
return strings.TrimSpace(string(decoded)), nil
}
// runVaultCommand executes a command inside the Vault pod using the `runCommand` utility.
func (vm *VaultManager) runVaultCommand(command string) error {
envSetup := fmt.Sprintf("export VAULT_SKIP_VERIFY=true && export VAULT_ADDR='%s' && export VAULT_TOKEN='%s'", defaultVaultAddress, vm.rootToken)
fullCmd := fmt.Sprintf("%s && %s", envSetup, command)
// Use the runCommand utility to show command output to the user.
return runCommand("kubectl", "exec", "-n", vaultNamespace, vm.podName, "--", "sh", "-c", fullCmd)
}
// verifyVaultToken checks if the root token is valid.
func (vm *VaultManager) verifyVaultToken() error {
return vm.runVaultCommand("vault token lookup")
}
// StoreSecret stores a key-value secret at the specified path in Vault.
func (vm *VaultManager) StoreSecret(path string, data map[string]string) error {
fmt.Printf("[INFO] Storing secret in Vault at path: %s...\n", path)
var kvPairs []string
for key, value := range data {
// Escape single quotes for shell command robustness.
escapedValue := strings.ReplaceAll(value, "'", "'\"'\"'")
kvPairs = append(kvPairs, fmt.Sprintf("%s='%s'", key, escapedValue))
}
vaultCmd := fmt.Sprintf("vault kv put %s %s", path, strings.Join(kvPairs, " "))
if err := vm.runVaultCommand(vaultCmd); err != nil {
return fmt.Errorf("failed to store secret in Vault: %w", err)
}
fmt.Println("[INFO] Verifying secret storage...")
verifyCmd := fmt.Sprintf("vault kv get %s", path)
if err := vm.runVaultCommand(verifyCmd); err != nil {
fmt.Printf("[WARNING] Could not verify secret storage at '%s', but it may have been created successfully.\n", path)
} else {
fmt.Printf("[SUCCESS] Secret at '%s' stored and verified in Vault!\n", path)
} }
return nil return nil
} }
// getUserInput prompts the user for their GitHub username, registry token, and organization management token // --- Main Workflow Function ---
func getUserInput() (string, string, string, error) { func createAndStoreSecrets() error {
// 1. Get User Input
username, registryToken, orgToken, err := getUserInput()
if err != nil {
return err
}
// 2. Initialize Vault Manager
// This handles kubectl checks, pod discovery, and token retrieval.
vm, err := NewVaultManager()
if err != nil {
return fmt.Errorf("failed to initialize Vault manager: %w", err)
}
// 3. Create and Store Docker Registry Secret
dockerConfigJSON, err := createDockerConfig(username, registryToken)
if err != nil {
return fmt.Errorf("failed to create Docker config: %w", err)
}
dockerSecretData := map[string]string{dockerConfigJSONKey: dockerConfigJSON}
if err := vm.StoreSecret(ghcrSecretPath, dockerSecretData); err != nil {
return fmt.Errorf("failed to store Docker registry secret: %w", err)
}
// 4. Create and Store Organization Management PAT
orgSecretData := map[string]string{orgManagementPATKey: orgToken}
if err := vm.StoreSecret(orgManagementSecretPath, orgSecretData); err != nil {
return fmt.Errorf("failed to store organization management PAT: %w", err)
}
return nil
}
// --- User Input and Helper Functions ---
func getUserInput() (username, registryToken, orgToken string, err error) {
reader := bufio.NewReader(os.Stdin) reader := bufio.NewReader(os.Stdin)
// Get username
fmt.Print("Enter your GitHub username: ") fmt.Print("Enter your GitHub username: ")
username, err := reader.ReadString('\n') username, err = reader.ReadString('\n')
if err != nil { if err != nil {
return "", "", "", err return
} }
username = strings.TrimSpace(username) username = strings.TrimSpace(username)
if username == "" { if username == "" {
return "", "", "", fmt.Errorf("username cannot be empty") err = fmt.Errorf("username cannot be empty")
return
} }
// Get registry token (hidden input) registryToken, err = promptForToken(reader, "Enter your GitHub personal access token for container registry")
fmt.Print("Enter your GitHub personal access token for container registry: ")
tokenBytes, err := term.ReadPassword(int(syscall.Stdin))
if err != nil { if err != nil {
return "", "", "", err return
}
orgToken, err = promptForToken(reader, "Enter your GitHub personal access token for organization management")
if err != nil {
return
}
return
}
// promptForToken provides a generic way to ask for a token with validation.
func promptForToken(reader *bufio.Reader, prompt string) (string, error) {
fmt.Printf("%s: ", prompt)
tokenBytes, err := term.ReadPassword(int(syscall.Stdin))
fmt.Println()
if err != nil {
return "", err
} }
fmt.Println() // Print newline after hidden input
token := string(tokenBytes) token := string(tokenBytes)
if token == "" { if token == "" {
return "", "", "", fmt.Errorf("registry token cannot be empty") return "", fmt.Errorf("token cannot be empty")
} }
// Validate registry token format validPrefixes := []string{"ghp_", "gho_", "ghu_", "ghs_", "ghr_", "github_pat_"}
if !strings.HasPrefix(token, "ghp_") && !strings.HasPrefix(token, "gho_") && hasValidPrefix := false
!strings.HasPrefix(token, "ghu_") && !strings.HasPrefix(token, "ghs_") && for _, prefix := range validPrefixes {
!strings.HasPrefix(token, "ghr_") { if strings.HasPrefix(token, prefix) {
fmt.Println("[WARNING] The registry token doesn't appear to be a GitHub personal access token (should start with ghp_, gho_, ghu_, ghs_, or ghr_)") hasValidPrefix = true
break
}
}
if !hasValidPrefix {
fmt.Printf("[WARNING] The token does not appear to be a valid GitHub personal access token (should start with one of %v)\n", validPrefixes)
fmt.Print("Do you want to continue anyway? (y/N): ") fmt.Print("Do you want to continue anyway? (y/N): ")
confirm, _ := reader.ReadString('\n')
confirm, err := reader.ReadString('\n') if strings.TrimSpace(strings.ToLower(confirm)) != "y" {
if err != nil { return "", fmt.Errorf("operation cancelled by user")
return "", "", "", err
}
confirm = strings.TrimSpace(strings.ToLower(confirm))
if confirm != "y" && confirm != "yes" {
return "", "", "", fmt.Errorf("operation cancelled by user")
} }
} }
// Get organization management token (hidden input) return token, nil
fmt.Print("Enter your GitHub personal access token for organization management: ")
orgTokenBytes, err := term.ReadPassword(int(syscall.Stdin))
if err != nil {
return "", "", "", err
}
fmt.Println() // Print newline after hidden input
orgToken := string(orgTokenBytes)
if orgToken == "" {
return "", "", "", fmt.Errorf("organization management token cannot be empty")
}
// Validate organization management token format
if !strings.HasPrefix(orgToken, "ghp_") && !strings.HasPrefix(orgToken, "gho_") &&
!strings.HasPrefix(orgToken, "ghu_") && !strings.HasPrefix(orgToken, "ghs_") &&
!strings.HasPrefix(orgToken, "ghr_") && !strings.HasPrefix(orgToken, "github_pat_") {
fmt.Println("[WARNING] The organization management token doesn't appear to be a GitHub personal access token (should start with ghp_, gho_, ghu_, ghs_, or ghr_)")
fmt.Print("Do you want to continue anyway? (y/N): ")
confirm, err := reader.ReadString('\n')
if err != nil {
return "", "", "", err
}
confirm = strings.TrimSpace(strings.ToLower(confirm))
if confirm != "y" && confirm != "yes" {
return "", "", "", fmt.Errorf("operation cancelled by user")
}
}
return username, token, orgToken, nil
} }
// createDockerConfig creates a Docker config JSON structure // createDockerConfig creates a Docker config JSON string.
func createDockerConfig(username, token string) (string, error) { func createDockerConfig(username, token string) (string, error) {
// Create base64 encoded auth string
auth := base64.StdEncoding.EncodeToString([]byte(username + ":" + token)) auth := base64.StdEncoding.EncodeToString([]byte(username + ":" + token))
// Create Docker config structure
config := DockerConfig{ config := DockerConfig{
Auths: map[string]DockerAuth{ Auths: map[string]DockerAuth{
"ghcr.io": { ghcrRegistry: {
Username: username, Username: username,
Password: token, Password: token,
Auth: auth, Auth: auth,
}, },
}, },
} }
// Marshal to JSON
configJSON, err := json.Marshal(config) configJSON, err := json.Marshal(config)
if err != nil { if err != nil {
return "", err return "", fmt.Errorf("failed to marshal docker config: %w", err)
} }
return string(configJSON), nil return string(configJSON), nil
} }
// storeInVault stores the Docker config in Vault KV v2
func storeInVault(dockerConfigJSON string) error {
fmt.Println("[INFO] Setting up Vault connection...")
// Get the vault pod name
fmt.Println("[INFO] Finding Vault pod...")
vaultPod, err := getVaultPodName()
if err != nil {
return fmt.Errorf("failed to find Vault pod: %v", err)
}
fmt.Printf("[INFO] Using Vault pod: %s\n", vaultPod)
// Get the root token
fmt.Println("[INFO] Retrieving Vault root token...")
rootToken, err := getVaultRootToken()
if err != nil {
return fmt.Errorf("failed to get Vault root token: %v", err)
}
// Set environment and authenticate
fmt.Println("[INFO] Setting up Vault environment...")
envSetupCmd := fmt.Sprintf("export VAULT_SKIP_VERIFY=true && export VAULT_ADDR='https://127.0.0.1:8200' && export VAULT_TOKEN='%s'", rootToken)
// Test authentication
authTestCmd := fmt.Sprintf("%s && vault token lookup", envSetupCmd)
if err := runCommand("kubectl", "exec", "-n", "vault", vaultPod, "--", "sh", "-c", authTestCmd); err != nil {
return fmt.Errorf("failed to validate Vault token: %v", err)
}
fmt.Println("[SUCCESS] Vault token is valid")
// Store the secret using kubectl exec to vault pod
fmt.Println("[INFO] Storing Docker registry secret in Vault KV store...")
// Use kubectl exec to run vault command inside the pod with proper env vars and token
vaultCmd := fmt.Sprintf("%s && vault kv put secret/ghcr-auth .dockerconfigjson='%s'", envSetupCmd, dockerConfigJSON)
if err := runCommand("kubectl", "exec", "-n", "vault", vaultPod, "--", "sh", "-c", vaultCmd); err != nil {
return fmt.Errorf("failed to store Docker registry secret in Vault: %v", err)
}
// Verify the secret was stored
fmt.Println("[INFO] Verifying Docker registry secret storage...")
verifyCmd := fmt.Sprintf("%s && vault kv get secret/ghcr-auth", envSetupCmd)
if err := runCommand("kubectl", "exec", "-n", "vault", vaultPod, "--", "sh", "-c", verifyCmd); err != nil {
fmt.Println("[WARNING] Could not verify Docker registry secret storage, but it may have been created successfully")
fmt.Printf("[INFO] You can manually verify with: kubectl exec -n vault %s -- sh -c \"%s\"\n", vaultPod, verifyCmd)
} else {
fmt.Println("[SUCCESS] Docker registry secret stored and verified in Vault!")
}
return nil
}
// storeOrgManagementPAT stores the organization management PAT in Vault KV v2
func storeOrgManagementPAT(token string) error {
// Get the vault pod name
vaultPod, err := getVaultPodName()
if err != nil {
return fmt.Errorf("failed to find Vault pod: %v", err)
}
// Get the root token
rootToken, err := getVaultRootToken()
if err != nil {
return fmt.Errorf("failed to get Vault root token: %v", err)
}
// Set environment and authenticate
envSetupCmd := fmt.Sprintf("export VAULT_SKIP_VERIFY=true && export VAULT_ADDR='https://127.0.0.1:8200' && export VAULT_TOKEN='%s'", rootToken)
// Store the organization management PAT
fmt.Println("[INFO] Storing organization management PAT in Vault KV store...")
// Escape single quotes in token for shell command
escapedToken := strings.ReplaceAll(token, "'", "'\"'\"'")
// Use kubectl exec to run vault command inside the pod
vaultCmd := fmt.Sprintf("%s && vault kv put secret/org-management-pat ORG_MANAGEMENT_PAT='%s'", envSetupCmd, escapedToken)
if err := runCommand("kubectl", "exec", "-n", "vault", vaultPod, "--", "sh", "-c", vaultCmd); err != nil {
return fmt.Errorf("failed to store organization management PAT in Vault: %v", err)
}
// Verify the secret was stored
fmt.Println("[INFO] Verifying organization management PAT storage...")
verifyCmd := fmt.Sprintf("%s && vault kv get secret/org-management-pat", envSetupCmd)
if err := runCommand("kubectl", "exec", "-n", "vault", vaultPod, "--", "sh", "-c", verifyCmd); err != nil {
fmt.Println("[WARNING] Could not verify organization management PAT storage, but it may have been created successfully")
fmt.Printf("[INFO] You can manually verify with: kubectl exec -n vault %s -- sh -c \"%s\"\n", vaultPod, verifyCmd)
} else {
fmt.Println("[SUCCESS] Organization management PAT stored and verified in Vault!")
}
return nil
}
// getVaultPodName gets the name of a running Vault pod
func getVaultPodName() (string, error) {
// Try to get vault-0 first (common StatefulSet naming)
if err := runCommandQuiet("kubectl", "get", "pod", "vault-0", "-n", "vault"); err == nil {
return "vault-0", nil
}
// Try other common Vault pod names
commonNames := []string{"vault-0", "vault-1", "vault-2"}
for _, name := range commonNames {
if err := runCommandQuiet("kubectl", "get", "pod", name, "-n", "vault"); err == nil {
return name, nil
}
}
// Try to find any vault pod using labels
cmd := exec.Command("kubectl", "get", "pods", "-n", "vault", "-l", "app.kubernetes.io/name=vault", "-o", "jsonpath={.items[0].metadata.name}")
output, err := cmd.Output()
if err == nil && len(output) > 0 {
return string(output), nil
}
// Last resort: try to find any running pod in vault namespace
cmd = exec.Command("kubectl", "get", "pods", "-n", "vault", "--field-selector=status.phase=Running", "-o", "jsonpath={.items[0].metadata.name}")
output, err = cmd.Output()
if err == nil && len(output) > 0 {
return string(output), nil
}
return "", fmt.Errorf("could not find any running Vault pods in the 'vault' namespace. Please ensure Vault is properly deployed and running")
}
// getVaultRootToken retrieves the Vault root token from Kubernetes secrets
func getVaultRootToken() (string, error) {
fmt.Println("[INFO] Looking for Vault root token in vault-unseal-keys secret...")
// Primary location: secret/vault-unseal-keys key vault-root in vault namespace
cmd := exec.Command("kubectl", "get", "secret", "vault-unseal-keys", "-n", "vault", "-o", "jsonpath={.data.vault-root}")
output, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("failed to get vault-unseal-keys secret from vault namespace: %v. Please ensure Vault is properly initialized", err)
}
if len(output) == 0 {
return "", fmt.Errorf("vault-root key not found in vault-unseal-keys secret")
}
// The token is base64 encoded in the Kubernetes secret, decode it
decoded, err := base64.StdEncoding.DecodeString(string(output))
if err != nil {
return "", fmt.Errorf("failed to decode base64 root token: %v", err)
}
if len(decoded) == 0 {
return "", fmt.Errorf("decoded root token is empty")
}
token := strings.TrimSpace(string(decoded))
fmt.Println("[SUCCESS] Found Vault root token")
return token, nil
}