From 93f37bc9b6c0838faa5bcb94b358739ef6ef8541 Mon Sep 17 00:00:00 2001 From: eding Date: Sat, 30 Aug 2025 16:45:57 +0200 Subject: [PATCH] chore: clean up vault process --- main.go | 2 +- vault.go | 470 ++++++++++++++++++++++++------------------------------- 2 files changed, 203 insertions(+), 269 deletions(-) diff --git a/main.go b/main.go index 5c1ab8f..e7a13d6 100644 --- a/main.go +++ b/main.go @@ -89,7 +89,7 @@ func runInitRepo(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) os.Exit(1) } diff --git a/vault.go b/vault.go index d14a2a1..8192976 100644 --- a/vault.go +++ b/vault.go @@ -13,6 +13,22 @@ import ( "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 type DockerConfig struct { Auths map[string]DockerAuth `json:"auths"` @@ -25,327 +41,245 @@ type DockerAuth struct { Auth string `json:"auth"` } -// VaultSecret represents the structure for storing in Vault -type VaultSecret struct { - DockerConfigJSON string `json:".dockerconfigjson"` +// --- VaultManager to Encapsulate Logic --- + +// VaultManager handles all interactions with Kubernetes and Vault. +type VaultManager struct { + podName string + rootToken string } -// OrgManagementSecret represents the organization management PAT secret -type OrgManagementSecret struct { - OrgManagementPAT string `json:"ORG_MANAGEMENT_PAT"` -} +// NewVaultManager creates and initializes a new VaultManager. +func NewVaultManager() (*VaultManager, error) { + vm := &VaultManager{} -// createDockerRegistrySecret creates a Docker registry secret in Vault -func createDockerRegistrySecret() error { - // Check dependencies - fmt.Println("[INFO] Checking dependencies (kubectl and vault)...") + fmt.Println("[INFO] Checking dependencies (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] Checking Kubernetes context...") - 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() + fmt.Println("[INFO] Finding Vault pod...") + podName, err := vm.getVaultPodName() 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 - dockerConfig, err := createDockerConfig(username, token) + fmt.Println("[INFO] Retrieving Vault root token...") + rootToken, err := vm.getVaultRootToken() 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 - fmt.Println("[INFO] Storing Docker registry secret in Vault...") - if err := storeInVault(dockerConfig); err != nil { - return fmt.Errorf("failed to store Docker registry secret in Vault: %v", err) + // For getting the name via label, we MUST capture the command's output, + // so we use exec.Command().Output() directly. + cmd := exec.Command("kubectl", "get", "pods", "-n", vaultNamespace, "-l", vaultPodLabelSelector, "-o", "jsonpath={.items[0].metadata.name}") + output, err := cmd.Output() + if err == nil && len(output) > 0 { + return string(output), nil } - // Store organization management PAT in Vault - 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) + return "", fmt.Errorf("could not find any running Vault pods in the '%s' namespace", vaultNamespace) +} + +// 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 } -// getUserInput prompts the user for their GitHub username, registry token, and organization management token -func getUserInput() (string, string, string, error) { +// --- Main Workflow Function --- +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) - // Get username fmt.Print("Enter your GitHub username: ") - username, err := reader.ReadString('\n') + username, err = reader.ReadString('\n') if err != nil { - return "", "", "", err + return } username = strings.TrimSpace(username) - if username == "" { - return "", "", "", fmt.Errorf("username cannot be empty") + err = fmt.Errorf("username cannot be empty") + return } - // Get registry token (hidden input) - fmt.Print("Enter your GitHub personal access token for container registry: ") - tokenBytes, err := term.ReadPassword(int(syscall.Stdin)) + registryToken, err = promptForToken(reader, "Enter your GitHub personal access token for container registry") 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) if token == "" { - return "", "", "", fmt.Errorf("registry token cannot be empty") + return "", fmt.Errorf("token cannot be empty") } - // Validate registry token format - if !strings.HasPrefix(token, "ghp_") && !strings.HasPrefix(token, "gho_") && - !strings.HasPrefix(token, "ghu_") && !strings.HasPrefix(token, "ghs_") && - !strings.HasPrefix(token, "ghr_") { - 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_)") + validPrefixes := []string{"ghp_", "gho_", "ghu_", "ghs_", "ghr_", "github_pat_"} + hasValidPrefix := false + for _, prefix := range validPrefixes { + if strings.HasPrefix(token, prefix) { + 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): ") - - 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") + confirm, _ := reader.ReadString('\n') + if strings.TrimSpace(strings.ToLower(confirm)) != "y" { + return "", fmt.Errorf("operation cancelled by user") } } - // Get organization management token (hidden input) - 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 + return token, nil } -// createDockerConfig creates a Docker config JSON structure +// createDockerConfig creates a Docker config JSON string. func createDockerConfig(username, token string) (string, error) { - // Create base64 encoded auth string auth := base64.StdEncoding.EncodeToString([]byte(username + ":" + token)) - - // Create Docker config structure config := DockerConfig{ Auths: map[string]DockerAuth{ - "ghcr.io": { + ghcrRegistry: { Username: username, Password: token, Auth: auth, }, }, } - - // Marshal to JSON configJSON, err := json.Marshal(config) if err != nil { - return "", err + return "", fmt.Errorf("failed to marshal docker config: %w", err) } - 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 -}