Go e HashiCorp Vault: Gerenciamento de Segredos

HashiCorp Vault é a solução líder para gerenciamento de segredos em ambientes cloud-native. Sua integração com Go permite proteger credenciais, APIs keys, certificados e dados sensíveis de forma segura e auditável.

Neste guia completo, você aprenderá a integrar Vault com aplicações Go, implementar padrões de segurança, usar secrets dinâmicos e automatizar rotação de credenciais.

Índice

  1. Fundamentos do Vault
  2. Instalação e Configuração
  3. Cliente Go do Vault
  4. Autenticação
  5. Leitura e Escrita de Secrets
  6. Secrets Dinâmicos
  7. Rotação Automática
  8. Padrões de Produção

Fundamentos do Vault

Arquitetura

┌─────────────────────────────────────────────────────────────┐
│                      Aplicação Go                          │
│  ┌─────────────────────────────────────────────────────┐   │
│  │              Vault Client Go                        │   │
│  │  • Autenticação                                     │   │
│  │  • Cache de tokens                                  │   │
│  │  • Renovação automática                             │   │
│  └─────────────────────────┬───────────────────────────┘   │
└────────────────────────────┼────────────────────────────────┘
                             │ HTTPS/TLS
┌─────────────────────────────────────────────────────────────┐
│                    Vault Server                            │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐      │
│  │   Autenticação│  │   Policies   │  │  Secret      │      │
│  │   Methods     │  │   (ACL)      │  │  Engines     │      │
│  └──────────────┘  └──────────────┘  └──────────────┘      │
│                                                             │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐      │
│  │   Transit    │  │   Database   │  │   PKI        │      │
│  │   (Encryption)│  │   (Dynamic)  │  │  (Certificates)     │
│  └──────────────┘  └──────────────┘  └──────────────┘      │
└─────────────────────────────────────────────────────────────┘

Key Features

1. Secrets Estáticos Key-Value store para credenciais, API keys, certificados.

2. Secrets Dinâmicos Credenciais geradas sob demanda com TTL automático (AWS, DB, etc).

3. Encryption as a Service Criptografe dados sem gerenciar chaves via Transit secret engine.

4. PKI as a Service Emita e gerencie certificados TLS automaticamente.


Instalação e Configuração

Instalação do Cliente Go

go get github.com/hashicorp/vault/api

Configuração Básica

package vault

import (
    "context"
    "fmt"
    "time"

    vault "github.com/hashicorp/vault/api"
)

// Config representa a configuração do cliente Vault
type Config struct {
    Address     string        // Vault server URL
    Token       string        // Token de autenticação
    Timeout     time.Duration // Timeout para requisições
    MaxRetries  int
    RetryDelay  time.Duration
}

// Client é um wrapper seguro para o cliente Vault
type Client struct {
    client *vault.Client
    config Config
}

// NewClient cria um novo cliente Vault
func NewClient(cfg Config) (*Client, error) {
    config := vault.DefaultConfig()
    config.Address = cfg.Address
    config.Timeout = cfg.Timeout

    // Configura retry
    if cfg.MaxRetries > 0 {
        config.MaxRetries = cfg.MaxRetries
    }

    client, err := vault.NewClient(config)
    if err != nil {
        return nil, fmt.Errorf("falha ao criar cliente Vault: %w", err)
    }

    // Configura token inicial
    if cfg.Token != "" {
        client.SetToken(cfg.Token)
    }

    return &Client{
        client: client,
        config: cfg,
    }, nil
}

// SetToken atualiza o token de autenticação
func (c *Client) SetToken(token string) {
    c.client.SetToken(token)
    c.config.Token = token
}

// Health verifica o status do servidor Vault
func (c *Client) Health(ctx context.Context) (*vault.HealthResponse, error) {
    health, err := c.client.Sys().Health()
    if err != nil {
        return nil, fmt.Errorf("falha ao verificar health: %w", err)
    }
    return health, nil
}

Autenticação

Múltiplos Métodos de Autenticação

package vault

import (
    "context"
    "fmt"
    "os"

    vault "github.com/hashicorp/vault/api"
)

// AuthManager gerencia autenticação com Vault
type AuthManager struct {
    client *vault.Client
}

func NewAuthManager(client *vault.Client) *AuthManager {
    return &AuthManager{client: client}
}

// ============================================================
// 1. Token Authentication (Desenvolvimento)
// ============================================================

func (am *AuthManager) LoginWithToken(token string) error {
    if token == "" {
        // Tenta obter do ambiente
        token = os.Getenv("VAULT_TOKEN")
    }
    
    if token == "" {
        return fmt.Errorf("token não fornecido")
    }

    am.client.SetToken(token)
    
    // Valida token
    _, err := am.client.Auth().Token().LookupSelf()
    if err != nil {
        return fmt.Errorf("token inválido: %w", err)
    }

    return nil
}

// ============================================================
// 2. AppRole Authentication (Production)
// ============================================================

type AppRoleCredentials struct {
    RoleID   string
    SecretID string
}

func (am *AuthManager) LoginWithAppRole(ctx context.Context, creds AppRoleCredentials) (*vault.Secret, error) {
    data := map[string]interface{}{
        "role_id":   creds.RoleID,
        "secret_id": creds.SecretID,
    }

    secret, err := am.client.Logical().WriteWithContext(ctx, "auth/approle/login", data)
    if err != nil {
        return nil, fmt.Errorf("falha na autenticação AppRole: %w", err)
    }

    if secret == nil || secret.Auth == nil {
        return nil, fmt.Errorf("resposta de autenticação inválida")
    }

    // Define o token obtido
    am.client.SetToken(secret.Auth.ClientToken)
    
    return secret, nil
}

// ============================================================
// 3. Kubernetes Authentication (K8s clusters)
// ============================================================

func (am *AuthManager) LoginWithKubernetes(ctx context.Context, role string, jwtPath string) (*vault.Secret, error) {
    if jwtPath == "" {
        jwtPath = "/var/run/secrets/kubernetes.io/serviceaccount/token"
    }

    // Lê JWT do service account
    jwt, err := os.ReadFile(jwtPath)
    if err != nil {
        return nil, fmt.Errorf("falha ao ler JWT: %w", err)
    }

    data := map[string]interface{}{
        "jwt":  string(jwt),
        "role": role,
    }

    secret, err := am.client.Logical().WriteWithContext(ctx, "auth/kubernetes/login", data)
    if err != nil {
        return nil, fmt.Errorf("falha na autenticação Kubernetes: %w", err)
    }

    am.client.SetToken(secret.Auth.ClientToken)
    return secret, nil
}

// ============================================================
// 4. AWS IAM Authentication
// ============================================================

func (am *AuthManager) LoginWithAWS(ctx context.Context, role string) (*vault.Secret, error) {
    // Usa AWS SDK para obter headers assinados
    // ou assume role credentials
    
    // Exemplo simplificado - em produção use aws-sdk-go-v2
    data := map[string]interface{}{
        "role": role,
        "iam_http_request_method": "POST",
        "iam_request_url":         "...",
        "iam_request_body":        "...",
        "iam_request_headers":     "...",
    }

    secret, err := am.client.Logical().WriteWithContext(ctx, "auth/aws/login", data)
    if err != nil {
        return nil, fmt.Errorf("falha na autenticação AWS: %w", err)
    }

    am.client.SetToken(secret.Auth.ClientToken)
    return secret, nil
}

// ============================================================
// 3. Token Renewal
// ============================================================

type TokenRenewer struct {
    client     *vault.Client
    secret     *vault.Secret
    stopCh     chan struct{}
    isRenewing bool
}

func NewTokenRenewer(client *vault.Client, secret *vault.Secret) *TokenRenewer {
    return &TokenRenewer{
        client: client,
        secret: secret,
        stopCh: make(chan struct{}),
    }
}

func (tr *TokenRenewer) Start() {
    if tr.isRenewing {
        return
    }

    tr.isRenewing = true
    go tr.renewLoop()
}

func (tr *TokenRenewer) Stop() {
    close(tr.stopCh)
    tr.isRenewing = false
}

func (tr *TokenRenewer) renewLoop() {
    // Renova em 2/3 do tempo de expiração
    renewInterval := tr.secret.Auth.LeaseDuration * 2 / 3
    ticker := time.NewTicker(time.Duration(renewInterval) * time.Second)
    defer ticker.Stop()

    for {
        select {
        case <-ticker.C:
            _, err := tr.client.Auth().Token().RenewSelf(tr.secret.Auth.LeaseDuration)
            if err != nil {
                fmt.Printf("Falha ao renovar token: %v\n", err)
                // Em produção: alertar e re-autenticar
            }
        case <-tr.stopCh:
            return
        }
    }
}

Leitura e Escrita de Secrets

KV Secrets Engine v2

package vault

import (
    "context"
    "encoding/json"
    "fmt"
    "path"

    vault "github.com/hashicorp/vault/api"
)

// SecretsManager gerencia operações de secrets
type SecretsManager struct {
    client    *vault.Client
    mountPath string // e.g., "secret" ou "kv"
}

func NewSecretsManager(client *vault.Client, mountPath string) *SecretsManager {
    if mountPath == "" {
        mountPath = "secret"
    }
    return &SecretsManager{
        client:    client,
        mountPath: mountPath,
    }
}

// ============================================================
// Leitura de Secrets
// ============================================================

// SecretData contém dados deserializados
type SecretData map[string]interface{}

func (sm *SecretsManager) GetSecret(ctx context.Context, path string) (SecretData, error) {
    fullPath := sm.buildPath(path)
    
    secret, err := sm.client.Logical().ReadWithContext(ctx, fullPath)
    if err != nil {
        return nil, fmt.Errorf("falha ao ler secret: %w", err)
    }

    if secret == nil {
        return nil, fmt.Errorf("secret não encontrado: %s", path)
    }

    // Extrai dados (KV v2 tem estrutura diferente)
    data, ok := secret.Data["data"].(map[string]interface{})
    if !ok {
        // Pode ser KV v1
        data = secret.Data
    }

    return SecretData(data), nil
}

// GetSecretString retorna um valor específico como string
func (sm *SecretsManager) GetSecretString(ctx context.Context, path, key string) (string, error) {
    data, err := sm.GetSecret(ctx, path)
    if err != nil {
        return "", err
    }

    value, ok := data[key].(string)
    if !ok {
        return "", fmt.Errorf("chave %s não encontrada ou não é string", key)
    }

    return value, nil
}

// GetSecretAs deserializa secret em struct
func (sm *SecretsManager) GetSecretAs(ctx context.Context, path string, target interface{}) error {
    data, err := sm.GetSecret(ctx, path)
    if err != nil {
        return err
    }

    // Converte para JSON e depois para struct
    jsonBytes, err := json.Marshal(data)
    if err != nil {
        return fmt.Errorf("falha ao serializar dados: %w", err)
    }

    if err := json.Unmarshal(jsonBytes, target); err != nil {
        return fmt.Errorf("falha ao deserializar dados: %w", err)
    }

    return nil
}

// ============================================================
// Escrita de Secrets
// ============================================================

func (sm *SecretsManager) PutSecret(ctx context.Context, path string, data SecretData) error {
    fullPath := sm.buildPath(path)
    
    // KV v2 usa data wrapper
    payload := map[string]interface{}{
        "data": data,
    }

    _, err := sm.client.Logical().WriteWithContext(ctx, fullPath, payload)
    if err != nil {
        return fmt.Errorf("falha ao escrever secret: %w", err)
    }

    return nil
}

// PutSecretStruct serializa struct e salva
func (sm *SecretsManager) PutSecretStruct(ctx context.Context, path string, data interface{}) error {
    jsonBytes, err := json.Marshal(data)
    if err != nil {
        return err
    }

    var jsonData map[string]interface{}
    if err := json.Unmarshal(jsonBytes, &jsonData); err != nil {
        return err
    }

    return sm.PutSecret(ctx, path, jsonData)
}

// ============================================================
// Deleção e Versionamento
// ============================================================

func (sm *SecretsManager) DeleteSecret(ctx context.Context, path string) error {
    fullPath := sm.buildPath(path)
    _, err := sm.client.Logical().DeleteWithContext(ctx, fullPath)
    return err
}

// DeleteSecretVersion deleta versão específica (KV v2)
func (sm *SecretsManager) DeleteSecretVersion(ctx context.Context, path string, version int) error {
    fullPath := sm.buildPath(path)
    
    payload := map[string]interface{}{
        "versions": []int{version},
    }

    _, err := sm.client.Logical().DeleteWithDataWithContext(ctx, fullPath, payload)
    return err
}

// ListSecrets lista secrets em um path
func (sm *SecretsManager) ListSecrets(ctx context.Context, path string) ([]string, error) {
    fullPath := sm.buildPath(path)
    
    secret, err := sm.client.Logical().ListWithContext(ctx, fullPath)
    if err != nil {
        return nil, err
    }

    if secret == nil {
        return []string{}, nil
    }

    keys, ok := secret.Data["keys"].([]interface{})
    if !ok {
        return []string{}, nil
    }

    var result []string
    for _, key := range keys {
        if str, ok := key.(string); ok {
            result = append(result, str)
        }
    }

    return result, nil
}

// Helper para construir path
func (sm *SecretsManager) buildPath(p string) string {
    // KV v2: secret/data/path
    return path.Join(sm.mountPath, "data", p)
}

Secrets Dinâmicos

Database Secrets Engine

package vault

import (
    "context"
    "database/sql"
    "fmt"
    "time"

    _ "github.com/lib/pq"
    vault "github.com/hashicorp/vault/api"
)

// DatabaseCredentials representa credenciais dinâmicas
type DatabaseCredentials struct {
    Username  string
    Password  string
    Host      string
    Port      int
    Database  string
    LeaseID   string
    LeaseDuration int
    Renewable bool
}

// ConnectionString retorna string de conexão
func (dc *DatabaseCredentials) ConnectionString() string {
    return fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=require",
        dc.Username, dc.Password, dc.Host, dc.Port, dc.Database)
}

// DatabaseManager gerencia credenciais dinâmicas de DB
type DatabaseManager struct {
    client     *vault.Client
    dbHost     string
    dbPort     int
    dbName     string
}

func NewDatabaseManager(client *vault.Client, host string, port int, dbName string) *DatabaseManager {
    return &DatabaseManager{
        client: client,
        dbHost: host,
        dbPort: port,
        dbName: dbName,
    }
}

// GenerateCredentials obtém credenciais dinâmicas do Vault
func (dm *DatabaseManager) GenerateCredentials(ctx context.Context, role string) (*DatabaseCredentials, error) {
    path := fmt.Sprintf("database/creds/%s", role)
    
    secret, err := dm.client.Logical().ReadWithContext(ctx, path)
    if err != nil {
        return nil, fmt.Errorf("falha ao gerar credenciais: %w", err)
    }

    if secret == nil {
        return nil, fmt.Errorf("role não encontrado: %s", role)
    }

    creds := &DatabaseCredentials{
        Username:      secret.Data["username"].(string),
        Password:      secret.Data["password"].(string),
        Host:          dm.dbHost,
        Port:          dm.dbPort,
        Database:      dm.dbName,
        LeaseID:       secret.LeaseID,
        LeaseDuration: secret.LeaseDuration,
        Renewable:     secret.Renewable,
    }

    return creds, nil
}

// CreateConnection cria conexão SQL com credenciais dinâmicas
func (dm *DatabaseManager) CreateConnection(ctx context.Context, role string) (*sql.DB, *DatabaseCredentials, error) {
    creds, err := dm.GenerateCredentials(ctx, role)
    if err != nil {
        return nil, nil, err
    }

    db, err := sql.Open("postgres", creds.ConnectionString())
    if err != nil {
        return nil, nil, err
    }

    if err := db.PingContext(ctx); err != nil {
        return nil, nil, fmt.Errorf("falha ao conectar: %w", err)
    }

    return db, creds, nil
}

// ============================================================
// AWS Dynamic Credentials
// ============================================================

type AWSCredentials struct {
    AccessKeyID     string
    SecretAccessKey string
    SessionToken    string
    LeaseID         string
    LeaseDuration   int
}

func (dm *DatabaseManager) GenerateAWSCredentials(ctx context.Context, role string) (*AWSCredentials, error) {
    path := fmt.Sprintf("aws/creds/%s", role)
    
    secret, err := dm.client.Logical().ReadWithContext(ctx, path)
    if err != nil {
        return nil, err
    }

    return &AWSCredentials{
        AccessKeyID:     secret.Data["access_key"].(string),
        SecretAccessKey: secret.Data["secret_key"].(string),
        SessionToken:    secret.Data["security_token"].(string),
        LeaseID:         secret.LeaseID,
        LeaseDuration:   secret.LeaseDuration,
    }, nil
}

Rotação Automática

Lease Renewal System

package vault

import (
    "context"
    "fmt"
    "sync"
    "time"

    vault "github.com/hashicorp/vault/api"
)

// LeaseManager gerencia renovação automática de leases
type LeaseManager struct {
    client      *vault.Client
    leases      map[string]*ManagedLease
    mu          sync.RWMutex
    stopCh      chan struct{}
    wg          sync.WaitGroup
}

type ManagedLease struct {
    LeaseID       string
    Renewable     bool
    LeaseDuration int
    LastRenewal   time.Time
    onRenewal     func() error
    onExpiry      func()
}

func NewLeaseManager(client *vault.Client) *LeaseManager {
    lm := &LeaseManager{
        client: client,
        leases: make(map[string]*ManagedLease),
        stopCh: make(chan struct{}),
    }
    
    go lm.renewalLoop()
    return lm
}

func (lm *LeaseManager) Stop() {
    close(lm.stopCh)
    lm.wg.Wait()
}

// RegisterLease adiciona lease para gerenciamento
func (lm *LeaseManager) RegisterLease(lease *ManagedLease) {
    lm.mu.Lock()
    defer lm.mu.Unlock()
    
    lm.leases[lease.LeaseID] = lease
}

// UnregisterLease remove lease do gerenciamento
func (lm *LeaseManager) UnregisterLease(leaseID string) {
    lm.mu.Lock()
    defer lm.mu.Unlock()
    
    delete(lm.leases, leaseID)
}

func (lm *LeaseManager) renewalLoop() {
    ticker := time.NewTicker(10 * time.Second)
    defer ticker.Stop()

    for {
        select {
        case <-ticker.C:
            lm.checkAndRenewLeases()
        case <-lm.stopCh:
            return
        }
    }
}

func (lm *LeaseManager) checkAndRenewLeases() {
    lm.mu.RLock()
    leases := make([]*ManagedLease, 0, len(lm.leases))
    for _, lease := range lm.leases {
        leases = append(leases, lease)
    }
    lm.mu.RUnlock()

    for _, lease := range leases {
        deadline := lease.LastRenewal.Add(
            time.Duration(lease.LeaseDuration*2/3) * time.Second,
        )

        if time.Now().After(deadline) && lease.Renewable {
            lm.wg.Add(1)
            go func(l *ManagedLease) {
                defer lm.wg.Done()
                lm.renewLease(l)
            }(lease)
        }
    }
}

func (lm *LeaseManager) renewLease(lease *ManagedLease) error {
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    secret, err := lm.client.Sys().RenewWithContext(ctx, lease.LeaseID, lease.LeaseDuration)
    if err != nil {
        fmt.Printf("Falha ao renovar lease %s: %v\n", lease.LeaseID, err)
        
        if lease.onExpiry != nil {
            lease.onExpiry()
        }
        lm.UnregisterLease(lease.LeaseID)
        return err
    }

    lease.LeaseDuration = secret.LeaseDuration
    lease.LastRenewal = time.Now()

    fmt.Printf("Lease %s renovado. Nova duração: %ds\n", lease.LeaseID, lease.LeaseDuration)

    if lease.onRenewal != nil {
        if err := lease.onRenewal(); err != nil {
            fmt.Printf("Callback de renovação falhou: %v\n", err)
        }
    }

    return nil
}

// RevokeLease revoga lease imediatamente
func (lm *LeaseManager) RevokeLease(leaseID string) error {
    err := lm.client.Sys().Revoke(leaseID)
    if err != nil {
        return err
    }

    lm.UnregisterLease(leaseID)
    return nil
}

// RevokeAll revoga todos os leases
func (lm *LeaseManager) RevokeAll() error {
    lm.mu.Lock()
    leases := make([]string, 0, len(lm.leases))
    for leaseID := range lm.leases {
        leases = append(leases, leaseID)
    }
    lm.leases = make(map[string]*ManagedLease)
    lm.mu.Unlock()

    for _, leaseID := range leases {
        if err := lm.client.Sys().Revoke(leaseID); err != nil {
            fmt.Printf("Falha ao revogar lease %s: %v\n", leaseID, err)
        }
    }

    return nil
}

Padrões de Produção

1. Secure Configuration

package config

import (
    "context"
    "fmt"
    "os"

    "golang-site/internal/vault"
)

// DatabaseConfig carrega configuração segura do Vault
type DatabaseConfig struct {
    Host     string `vault:"host"`
    Port     int    `vault:"port"`
    Database string `vault:"database"`
    Username string `vault:"username"`
    Password string `vault:"password"`
    SSLMode  string `vault:"sslmode"`
}

// LoadFromVault carrega configuração do Vault
func LoadFromVault(ctx context.Context, path string) (*DatabaseConfig, error) {
    client, err := vault.NewClient(vault.Config{
        Address:    os.Getenv("VAULT_ADDR"),
        Token:      os.Getenv("VAULT_TOKEN"),
    })
    if err != nil {
        return nil, err
    }

    sm := vault.NewSecretsManager(client.Client(), "secret")
    
    var config DatabaseConfig
    if err := sm.GetSecretAs(ctx, path, &config); err != nil {
        return nil, err
    }

    return &config, nil
}

2. Connection Pooling com Credenciais Dinâmicas

package database

import (
    "context"
    "database/sql"
    "sync"
    "time"

    _ "github.com/lib/pq"
    "golang-site/internal/vault"
)

type DynamicDB struct {
    vaultManager *vault.DatabaseManager
    role         string
    db           *sql.DB
    creds        *vault.DatabaseCredentials
    mu           sync.RWMutex
    leaseManager *vault.LeaseManager
}

func NewDynamicDB(vaultClient *vault.Client, role string, host string, port int, dbName string) *DynamicDB {
    return &DynamicDB{
        vaultManager: vault.NewDatabaseManager(vaultClient, host, port, dbName),
        role:         role,
        leaseManager: vault.NewLeaseManager(vaultClient),
    }
}

func (ddb *DynamicDB) Initialize(ctx context.Context) error {
    return ddb.rotateCredentials(ctx)
}

func (ddb *DynamicDB) GetDB() *sql.DB {
    ddb.mu.RLock()
    defer ddb.mu.RUnlock()
    return ddb.db
}

func (ddb *DynamicDB) rotateCredentials(ctx context.Context) error {
    // Fecha conexão anterior
    ddb.mu.Lock()
    if ddb.db != nil {
        ddb.db.Close()
    }
    ddb.mu.Unlock()

    // Gera novas credenciais
    db, creds, err := ddb.vaultManager.CreateConnection(ctx, ddb.role)
    if err != nil {
        return err
    }

    // Configura pool
    db.SetMaxOpenConns(25)
    db.SetMaxIdleConns(10)
    db.SetConnMaxLifetime(time.Duration(creds.LeaseDuration) * time.Second)

    ddb.mu.Lock()
    ddb.db = db
    ddb.creds = creds
    ddb.mu.Unlock()

    // Registra lease para renovação automática
    if creds.Renewable {
        ddb.leaseManager.RegisterLease(&vault.ManagedLease{
            LeaseID:       creds.LeaseID,
            Renewable:     creds.Renewable,
            LeaseDuration: creds.LeaseDuration,
            LastRenewal:   time.Now(),
            onExpiry: func() {
                // Rotaciona credenciais quando expirar
                ddb.rotateCredentials(context.Background())
            },
        })
    }

    return nil
}

func (ddb *DynamicDB) Close() {
    ddb.leaseManager.Stop()
    
    ddb.mu.Lock()
    if ddb.db != nil {
        ddb.db.Close()
    }
    ddb.mu.Unlock()
}

3. Observabilidade

package vault

import (
    "context"
    "time"

    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promauto"
)

var (
    vaultRequests = promauto.NewCounterVec(
        prometheus.CounterOpts{
            Name: "vault_requests_total",
            Help: "Total de requisições ao Vault",
        },
        []string{"operation", "status"},
    )

    vaultRequestDuration = promauto.NewHistogramVec(
        prometheus.HistogramOpts{
            Name:    "vault_request_duration_seconds",
            Help:    "Duração das requisições ao Vault",
            Buckets: prometheus.DefBuckets,
        },
        []string{"operation"},
    )

    vaultLeases = promauto.NewGauge(
        prometheus.GaugeOpts{
            Name: "vault_managed_leases",
            Help: "Número de leases gerenciados",
        },
    )

    vaultAuthRenewal = promauto.NewCounterVec(
        prometheus.CounterOpts{
            Name: "vault_auth_renewal_total",
            Help: "Total de renovações de autenticação",
        },
        []string{"result"},
    )
)

// instrumented wrapper para métricas
func instrumentOperation(operation string, fn func() error) error {
    start := time.Now()
    err := fn()
    duration := time.Since(start).Seconds()

    status := "success"
    if err != nil {
        status = "error"
    }

    vaultRequests.WithLabelValues(operation, status).Inc()
    vaultRequestDuration.WithLabelValues(operation).Observe(duration)

    return err
}

4. Error Handling

package vault

import (
    "errors"
    "fmt"
)

var (
    ErrSecretNotFound = errors.New("secret não encontrado")
    ErrAuthFailed     = errors.New("autenticação falhou")
    ErrLeaseExpired   = errors.New("lease expirado")
    ErrPermissionDenied = errors.New("permissão negada")
)

// VaultError encapsula erros do Vault
type VaultError struct {
    Op      string
    Path    string
    Err     error
    Wrapped error
}

func (ve *VaultError) Error() string {
    return fmt.Sprintf("vault %s %s: %v", ve.Op, ve.Path, ve.Err)
}

func (ve *VaultError) Unwrap() error {
    return ve.Wrapped
}

func isPermissionError(err error) bool {
    if err == nil {
        return false
    }
    // Verifica mensagens comuns de permissão
    msg := err.Error()
    return contains(msg, "permission denied") || 
           contains(msg, "forbidden") ||
           contains(msg, "403")
}

func contains(s, substr string) bool {
    return len(s) >= len(substr) && 
           (s == substr || 
            len(s) > len(substr) && 
            (s[:len(substr)] == substr || s[len(s)-len(substr):] == substr ||
             hasSubstr(s, substr)))
}

func hasSubstr(s, substr string) bool {
    for i := 0; i <= len(s)-len(substr); i++ {
        if s[i:i+len(substr)] == substr {
            return true
        }
    }
    return false
}

Conclusão

Neste guia completo, você aprendeu:

Fundamentos: Arquitetura e princípios do Vault ✅ Autenticação: Token, AppRole, Kubernetes, AWS ✅ Secrets: KV estático, leitura/escrita, versionamento ✅ Dinâmicos: Credenciais de banco de dados e cloud automáticas ✅ Rotação: Renovação automática de leases e tokens ✅ Produção: Pools dinâmicos, observabilidade, tratamento de erros

Próximos Passos

  1. Go e etcd - Distributed key-value store
  2. Go e Consul - Service mesh e discovery
  3. Go Security - Boas práticas de segurança

Recursos Adicionais


FAQ

Q: Por que usar Vault ao invés de variáveis de ambiente? R: Vault fornece rotação automática, audit logging, controle de acesso granular e secrets dinâmicos - impossível com env vars.

Q: Vault é production-ready? R: Sim. Usado em produção por empresas como HashiCorp, GitHub, Barclays, e muitas outras Fortune 500.

Q: Qual método de autenticação usar em Kubernetes? R: Kubernetes Auth é o ideal - usa service accounts nativamente, sem secrets para gerenciar.

Q: Como lidar com Vault indisponível? R: Implemente cache local, circuit breakers e fallback para credenciais temporárias. Sempre priorize resiliência.

Q: Secrets dinâmicos têm overhead? R: Mínimo (milisegundos). Benefícios de segurança superam amplamente o custo de criação.