maidn-cli/vault.go

352 lines
12 KiB
Go

package main
import (
"bufio"
"encoding/base64"
"encoding/json"
"fmt"
"os"
"os/exec"
"strings"
"syscall"
"golang.org/x/term"
)
// DockerConfig represents the structure of a Docker config.json
type DockerConfig struct {
Auths map[string]DockerAuth `json:"auths"`
}
// DockerAuth represents the authentication info for a Docker registry
type DockerAuth struct {
Username string `json:"username"`
Password string `json:"password"`
Auth string `json:"auth"`
}
// VaultSecret represents the structure for storing in Vault
type VaultSecret struct {
DockerConfigJSON string `json:".dockerconfigjson"`
}
// OrgManagementSecret represents the organization management PAT secret
type OrgManagementSecret struct {
OrgManagementPAT string `json:"ORG_MANAGEMENT_PAT"`
}
// createDockerRegistrySecret creates a Docker registry secret in Vault
func createDockerRegistrySecret() error {
// Check dependencies
fmt.Println("[INFO] Checking dependencies (kubectl and vault)...")
if !commandExists("kubectl") {
return 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()
if err != nil {
return fmt.Errorf("failed to get user input: %v", err)
}
// Create Docker config
dockerConfig, err := createDockerConfig(username, token)
if err != nil {
return fmt.Errorf("failed to create Docker config: %v", err)
}
// 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)
}
// 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 nil
}
// getUserInput prompts the user for their GitHub username, registry token, and organization management token
func getUserInput() (string, string, string, error) {
reader := bufio.NewReader(os.Stdin)
// Get username
fmt.Print("Enter your GitHub username: ")
username, err := reader.ReadString('\n')
if err != nil {
return "", "", "", err
}
username = strings.TrimSpace(username)
if username == "" {
return "", "", "", fmt.Errorf("username cannot be empty")
}
// Get registry token (hidden input)
fmt.Print("Enter your GitHub personal access token for container registry: ")
tokenBytes, err := term.ReadPassword(int(syscall.Stdin))
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")
}
// 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_)")
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")
}
}
// 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
}
// createDockerConfig creates a Docker config JSON structure
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": {
Username: username,
Password: token,
Auth: auth,
},
},
}
// Marshal to JSON
configJSON, err := json.Marshal(config)
if err != nil {
return "", 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
}