Files
notifier/internal/auth/bootstrap.go
T
igodwin eda033ff9b
CI / Lint (push) Successful in 2m29s
Build and Publish Container / build-and-publish (push) Successful in 2m58s
CI / Vulnerability scan (push) Successful in 44s
CI / Test (push) Successful in 1m45s
fix: clear golangci-lint backlog and make lint job blocking
Addresses errcheck, gosec, revive, staticcheck, and unused findings
across the codebase (unchecked error returns, unsafe file inclusion
warnings on operator/test-controlled paths, missing package comments,
unused parameters, deprecated API usage). Also fixes two suppression
comments that were silently no-ops due to wrong syntax (#nosec needs
a leading '#', nolint reasons need '//' not '--').

With the backlog clear, drop continue-on-error from the CI lint job
per the plan left in b4b4806.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-18 10:32:51 -07:00

309 lines
11 KiB
Go

package auth
import (
"context"
"fmt"
"os"
"strings"
"time"
"github.com/igodwin/notifier/internal/logging"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
)
// BootstrapConfig holds configuration for bootstrap operations
type BootstrapConfig struct {
// Enabled triggers automatic bootstrap key creation on first startup
Enabled bool
// AdminKeyFileName is where to store the generated admin key
AdminKeyFileName string
// PrintToStdout prints the admin key to stdout (DANGEROUS - only for setup)
PrintToStdout bool
}
// BootstrapAdminKeyInMemory creates an initial admin API key on first startup (in-memory store)
// This is a simpler version for in-memory APIKeyStore (without database persistence)
func BootstrapAdminKeyInMemory(keyStore *APIKeyStore, cfg *BootstrapConfig, logger *logging.Logger) (*APIKey, error) {
if !cfg.Enabled {
return nil, fmt.Errorf("bootstrap is disabled")
}
// Check if bootstrap has already been done
if cfg.AdminKeyFileName != "" {
if _, err := os.Stat(cfg.AdminKeyFileName); err == nil {
// File exists, bootstrap already done
logger.Infof("Bootstrap key file exists at %s, skipping bootstrap", cfg.AdminKeyFileName)
return nil, fmt.Errorf("bootstrap already completed")
}
}
// Create admin key with all roles
adminRoles := []string{"admin", "notify-email", "notify-slack", "notify-ntfy"}
apiKey, err := keyStore.CreateKey(
"admin-bootstrap",
adminRoles,
0, // Unlimited rate limit
nil, // No expiration
)
if err != nil {
return nil, fmt.Errorf("failed to create bootstrap admin key: %w", err)
}
// Save key to file if configured
if cfg.AdminKeyFileName != "" {
keyContent := fmt.Sprintf(`# Notifier Admin Key
# Created: %s
# This key has full admin permissions
# KEEP THIS SECRET!
%s
`, time.Now().Format(time.RFC3339), apiKey.Key)
if err := os.WriteFile(cfg.AdminKeyFileName, []byte(keyContent), 0600); err != nil {
logger.Warnf("Failed to save admin key to file: %v", err)
} else {
logger.Infof("Admin key saved to %s", cfg.AdminKeyFileName)
}
}
// Print to stdout if configured (DANGEROUS - only for interactive setup)
if cfg.PrintToStdout {
separator := strings.Repeat("=", 60)
fmt.Println("\n" + separator)
fmt.Println("NOTIFIER BOOTSTRAP: ADMIN KEY CREATED")
fmt.Println(separator)
fmt.Printf("Key: %s\n", apiKey.Key)
fmt.Println("\nSave this key in a secure location. You will not be able to see it again.")
fmt.Println("Use this key to create additional API keys via the key management API.")
fmt.Println(separator + "\n")
}
logger.Infof("Bootstrap admin key created successfully")
return apiKey, nil
}
// RegisterAdminKeyInMemory registers a pre-existing admin API key in the keystore
// Used when loading from Kubernetes secret or environment variable
func RegisterAdminKeyInMemory(keyStore *APIKeyStore, adminKey string, logger *logging.Logger) (*APIKey, error) {
if adminKey == "" {
return nil, fmt.Errorf("admin key value is empty")
}
// Validate key format (should start with "nk_")
if !strings.HasPrefix(adminKey, "nk_") {
return nil, fmt.Errorf("invalid admin key format: must start with 'nk_'")
}
// Create APIKey object with the provided key
adminRoles := []string{"admin", "notify-email", "notify-slack", "notify-ntfy"}
now := time.Now().UTC()
apiKey := &APIKey{
Key: adminKey,
KeyHash: HashKey(adminKey),
KeyPreview: keyPreview(adminKey),
ClientID: "admin-bootstrap",
Roles: adminRoles,
CreatedAt: now,
IsActive: true,
RateLimit: 0, // Unlimited
Name: fmt.Sprintf("admin-bootstrap-%d", now.Unix()),
}
keyStore.RegisterKey(apiKey)
logger.Infof("Registered existing admin key from Kubernetes secret")
return apiKey, nil
}
// BootstrapAdminKey creates an initial admin API key on first startup (with database persistence)
// This should be called once per deployment when using HybridKeyStore with database
func BootstrapAdminKey(ctx context.Context, keyStore *HybridKeyStore, cfg *BootstrapConfig, logger *logging.Logger) (*APIKey, error) {
if !cfg.Enabled {
return nil, fmt.Errorf("bootstrap is disabled")
}
// Check if bootstrap has already been done
if cfg.AdminKeyFileName != "" {
if _, err := os.Stat(cfg.AdminKeyFileName); err == nil {
// File exists, bootstrap already done
logger.Infof("Bootstrap key file exists at %s, skipping bootstrap", cfg.AdminKeyFileName)
return nil, fmt.Errorf("bootstrap already completed")
}
}
// Create admin key with all roles
adminRoles := []string{"admin", "notify-email", "notify-slack", "notify-ntfy"}
apiKey, err := keyStore.CreateKey(
ctx,
"admin-bootstrap",
adminRoles,
0, // Unlimited rate limit
nil, // No expiration
"system",
)
if err != nil {
return nil, fmt.Errorf("failed to create bootstrap admin key: %w", err)
}
// Save key to file if configured
if cfg.AdminKeyFileName != "" {
keyContent := fmt.Sprintf(`# Notifier Admin Key
# Created: %s
# This key has full admin permissions
# KEEP THIS SECRET!
%s
`, time.Now().Format(time.RFC3339), apiKey.Key)
if err := os.WriteFile(cfg.AdminKeyFileName, []byte(keyContent), 0600); err != nil {
logger.Warnf("Failed to save admin key to file: %v", err)
} else {
logger.Infof("Admin key saved to %s", cfg.AdminKeyFileName)
}
}
// Print to stdout if configured (DANGEROUS - only for interactive setup)
if cfg.PrintToStdout {
separator := strings.Repeat("=", 60)
fmt.Println("\n" + separator)
fmt.Println("NOTIFIER BOOTSTRAP: ADMIN KEY CREATED")
fmt.Println(separator)
fmt.Printf("Key: %s\n", apiKey.Key)
fmt.Println("\nSave this key in a secure location. You will not be able to see it again.")
fmt.Println("Use this key to create additional API keys via the key management API.")
fmt.Println(separator + "\n")
}
logger.Infof("Bootstrap admin key created successfully")
return apiKey, nil
}
// LoadBootstrapKeyFromEnv checks if a bootstrap key was provided via environment variable
// This allows injecting a pre-generated key via CI/CD
func LoadBootstrapKeyFromEnv(_ context.Context, _ *HybridKeyStore, logger *logging.Logger) error {
bootstrapKey := os.Getenv("NOTIFIER_BOOTSTRAP_ADMIN_KEY")
if bootstrapKey == "" {
return nil // Not set, skip
}
// Check if key already exists in database
// For now, we skip if environment variable is set
// In production, you'd want to verify the key is already in the database
logger.Infof("Bootstrap key detected from environment variable")
return nil
}
// getKubernetesNamespace reads the pod's namespace from the service account token
func getKubernetesNamespace() (string, error) {
const namespacePath = "/var/run/secrets/kubernetes.io/serviceaccount/namespace"
data, err := os.ReadFile(namespacePath)
if err != nil {
return "", fmt.Errorf("failed to read namespace from service account: %w", err)
}
return strings.TrimSpace(string(data)), nil
}
// LoadAdminKeyFromKubernetesSecret attempts to load an existing admin key from a Kubernetes secret
// Returns the key string if found, empty string if secret doesn't exist, or error on failure
func LoadAdminKeyFromKubernetesSecret(ctx context.Context, secretName, secretKey string, logger *logging.Logger) (string, error) {
// Try to create Kubernetes client (will fail gracefully if not in cluster)
config, err := rest.InClusterConfig()
if err != nil {
logger.Debugf("Not running in Kubernetes cluster or in-cluster config unavailable: %v", err)
return "", nil // Not in Kubernetes, return empty (not an error)
}
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
logger.Warnf("Failed to create Kubernetes client: %v", err)
return "", nil // Failed to create client, but not a fatal error
}
namespace, err := getKubernetesNamespace()
if err != nil {
logger.Warnf("Failed to determine pod namespace: %v", err)
return "", nil // Failed to get namespace, but not a fatal error
}
// Try to get the secret
secret, err := clientset.CoreV1().Secrets(namespace).Get(ctx, secretName, metav1.GetOptions{})
if err != nil {
// Secret doesn't exist or other error occurred
logger.Debugf("Admin key secret not found in namespace %s: %v", namespace, err)
return "", nil // Secret not found is not an error
}
// Extract the key value from the secret
if secretValue, exists := secret.Data[secretKey]; exists {
logger.Infof("Found existing admin key in Kubernetes secret %s/%s", namespace, secretName)
return string(secretValue), nil
}
logger.Warnf("Kubernetes secret %s/%s exists but key %q not found", namespace, secretName, secretKey)
return "", nil
}
// CreateKubernetesSecret creates or updates a Kubernetes secret with the admin key
func CreateKubernetesSecret(ctx context.Context, secretName, secretKey, adminKey string, logger *logging.Logger) error {
// Try to create Kubernetes client (will fail gracefully if not in cluster)
config, err := rest.InClusterConfig()
if err != nil {
logger.Debugf("Not running in Kubernetes cluster, skipping secret creation: %v", err)
return nil // Not in Kubernetes, skip (not an error)
}
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
logger.Warnf("Failed to create Kubernetes client, skipping secret creation: %v", err)
return nil // Failed to create client, but not a fatal error
}
namespace, err := getKubernetesNamespace()
if err != nil {
logger.Warnf("Failed to determine pod namespace, skipping secret creation: %v", err)
return nil // Failed to get namespace, but not a fatal error
}
// Create or update the secret
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: namespace,
Labels: map[string]string{
"app": "notifier",
},
},
Type: corev1.SecretTypeOpaque,
Data: map[string][]byte{
secretKey: []byte(adminKey),
},
}
// Try to get existing secret first
existingSecret, err := clientset.CoreV1().Secrets(namespace).Get(ctx, secretName, metav1.GetOptions{})
if err == nil {
// Secret exists, update it
secret.ResourceVersion = existingSecret.ResourceVersion
_, err = clientset.CoreV1().Secrets(namespace).Update(ctx, secret, metav1.UpdateOptions{})
if err != nil {
logger.Warnf("Failed to update Kubernetes secret %s/%s: %v", namespace, secretName, err)
return nil // Log warning but don't fail
}
logger.Infof("Updated admin key in Kubernetes secret %s/%s", namespace, secretName)
} else {
// Secret doesn't exist, create it
_, err = clientset.CoreV1().Secrets(namespace).Create(ctx, secret, metav1.CreateOptions{})
if err != nil {
logger.Warnf("Failed to create Kubernetes secret %s/%s: %v", namespace, secretName, err)
return nil // Log warning but don't fail
}
logger.Infof("Created Kubernetes secret %s/%s with admin key", namespace, secretName)
}
return nil
}