| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403 |
- package openbao
- import (
- "context"
- "crypto/rand"
- "encoding/base64"
- "encoding/json"
- "fmt"
- "strings"
- "time"
- vault "github.com/hashicorp/vault/api"
- "github.com/seaweedfs/seaweedfs/weed/glog"
- seaweedkms "github.com/seaweedfs/seaweedfs/weed/kms"
- "github.com/seaweedfs/seaweedfs/weed/util"
- )
- func init() {
- // Register the OpenBao/Vault KMS provider
- seaweedkms.RegisterProvider("openbao", NewOpenBaoKMSProvider)
- seaweedkms.RegisterProvider("vault", NewOpenBaoKMSProvider) // Alias for compatibility
- }
- // OpenBaoKMSProvider implements the KMSProvider interface using OpenBao/Vault Transit engine
- type OpenBaoKMSProvider struct {
- client *vault.Client
- transitPath string // Transit engine mount path (default: "transit")
- address string
- }
- // OpenBaoKMSConfig contains configuration for the OpenBao/Vault KMS provider
- type OpenBaoKMSConfig struct {
- Address string `json:"address"` // Vault address (e.g., "http://localhost:8200")
- Token string `json:"token"` // Vault token for authentication
- RoleID string `json:"role_id"` // AppRole role ID (alternative to token)
- SecretID string `json:"secret_id"` // AppRole secret ID (alternative to token)
- TransitPath string `json:"transit_path"` // Transit engine mount path (default: "transit")
- TLSSkipVerify bool `json:"tls_skip_verify"` // Skip TLS verification (for testing)
- CACert string `json:"ca_cert"` // Path to CA certificate
- ClientCert string `json:"client_cert"` // Path to client certificate
- ClientKey string `json:"client_key"` // Path to client private key
- RequestTimeout int `json:"request_timeout"` // Request timeout in seconds (default: 30)
- }
- // NewOpenBaoKMSProvider creates a new OpenBao/Vault KMS provider
- func NewOpenBaoKMSProvider(config util.Configuration) (seaweedkms.KMSProvider, error) {
- if config == nil {
- return nil, fmt.Errorf("OpenBao/Vault KMS configuration is required")
- }
- // Extract configuration
- address := config.GetString("address")
- if address == "" {
- address = "http://localhost:8200" // Default OpenBao address
- }
- token := config.GetString("token")
- roleID := config.GetString("role_id")
- secretID := config.GetString("secret_id")
- transitPath := config.GetString("transit_path")
- if transitPath == "" {
- transitPath = "transit" // Default transit path
- }
- tlsSkipVerify := config.GetBool("tls_skip_verify")
- caCert := config.GetString("ca_cert")
- clientCert := config.GetString("client_cert")
- clientKey := config.GetString("client_key")
- requestTimeout := config.GetInt("request_timeout")
- if requestTimeout == 0 {
- requestTimeout = 30 // Default 30 seconds
- }
- // Create Vault client configuration
- vaultConfig := vault.DefaultConfig()
- vaultConfig.Address = address
- vaultConfig.Timeout = time.Duration(requestTimeout) * time.Second
- // Configure TLS
- if tlsSkipVerify || caCert != "" || (clientCert != "" && clientKey != "") {
- tlsConfig := &vault.TLSConfig{
- Insecure: tlsSkipVerify,
- }
- if caCert != "" {
- tlsConfig.CACert = caCert
- }
- if clientCert != "" && clientKey != "" {
- tlsConfig.ClientCert = clientCert
- tlsConfig.ClientKey = clientKey
- }
- if err := vaultConfig.ConfigureTLS(tlsConfig); err != nil {
- return nil, fmt.Errorf("failed to configure TLS: %w", err)
- }
- }
- // Create Vault client
- client, err := vault.NewClient(vaultConfig)
- if err != nil {
- return nil, fmt.Errorf("failed to create OpenBao/Vault client: %w", err)
- }
- // Authenticate
- if token != "" {
- client.SetToken(token)
- glog.V(1).Infof("OpenBao KMS: Using token authentication")
- } else if roleID != "" && secretID != "" {
- if err := authenticateAppRole(client, roleID, secretID); err != nil {
- return nil, fmt.Errorf("failed to authenticate with AppRole: %w", err)
- }
- glog.V(1).Infof("OpenBao KMS: Using AppRole authentication")
- } else {
- return nil, fmt.Errorf("either token or role_id+secret_id must be provided")
- }
- provider := &OpenBaoKMSProvider{
- client: client,
- transitPath: transitPath,
- address: address,
- }
- glog.V(1).Infof("OpenBao/Vault KMS provider initialized at %s", address)
- return provider, nil
- }
- // authenticateAppRole authenticates using AppRole method
- func authenticateAppRole(client *vault.Client, roleID, secretID string) error {
- data := map[string]interface{}{
- "role_id": roleID,
- "secret_id": secretID,
- }
- secret, err := client.Logical().Write("auth/approle/login", data)
- if err != nil {
- return fmt.Errorf("AppRole authentication failed: %w", err)
- }
- if secret == nil || secret.Auth == nil {
- return fmt.Errorf("AppRole authentication returned empty token")
- }
- client.SetToken(secret.Auth.ClientToken)
- return nil
- }
- // GenerateDataKey generates a new data encryption key using OpenBao/Vault Transit
- func (p *OpenBaoKMSProvider) GenerateDataKey(ctx context.Context, req *seaweedkms.GenerateDataKeyRequest) (*seaweedkms.GenerateDataKeyResponse, error) {
- if req == nil {
- return nil, fmt.Errorf("GenerateDataKeyRequest cannot be nil")
- }
- if req.KeyID == "" {
- return nil, fmt.Errorf("KeyID is required")
- }
- // Validate key spec
- var keySize int
- switch req.KeySpec {
- case seaweedkms.KeySpecAES256:
- keySize = 32 // 256 bits
- default:
- return nil, fmt.Errorf("unsupported key spec: %s", req.KeySpec)
- }
- // Generate data key locally (similar to Azure/GCP approach)
- dataKey := make([]byte, keySize)
- if _, err := rand.Read(dataKey); err != nil {
- return nil, fmt.Errorf("failed to generate random data key: %w", err)
- }
- // Encrypt the data key using OpenBao/Vault Transit
- glog.V(4).Infof("OpenBao KMS: Encrypting data key using key %s", req.KeyID)
- // Prepare encryption data
- encryptData := map[string]interface{}{
- "plaintext": base64.StdEncoding.EncodeToString(dataKey),
- }
- // Add encryption context if provided
- if len(req.EncryptionContext) > 0 {
- contextJSON, err := json.Marshal(req.EncryptionContext)
- if err != nil {
- return nil, fmt.Errorf("failed to marshal encryption context: %w", err)
- }
- encryptData["context"] = base64.StdEncoding.EncodeToString(contextJSON)
- }
- // Call OpenBao/Vault Transit encrypt endpoint
- path := fmt.Sprintf("%s/encrypt/%s", p.transitPath, req.KeyID)
- secret, err := p.client.Logical().WriteWithContext(ctx, path, encryptData)
- if err != nil {
- return nil, p.convertVaultError(err, req.KeyID)
- }
- if secret == nil || secret.Data == nil {
- return nil, fmt.Errorf("no data returned from OpenBao/Vault encrypt operation")
- }
- ciphertext, ok := secret.Data["ciphertext"].(string)
- if !ok {
- return nil, fmt.Errorf("invalid ciphertext format from OpenBao/Vault")
- }
- // Create standardized envelope format for consistent API behavior
- envelopeBlob, err := seaweedkms.CreateEnvelope("openbao", req.KeyID, ciphertext, nil)
- if err != nil {
- return nil, fmt.Errorf("failed to create ciphertext envelope: %w", err)
- }
- response := &seaweedkms.GenerateDataKeyResponse{
- KeyID: req.KeyID,
- Plaintext: dataKey,
- CiphertextBlob: envelopeBlob, // Store in standardized envelope format
- }
- glog.V(4).Infof("OpenBao KMS: Generated and encrypted data key using key %s", req.KeyID)
- return response, nil
- }
- // Decrypt decrypts an encrypted data key using OpenBao/Vault Transit
- func (p *OpenBaoKMSProvider) Decrypt(ctx context.Context, req *seaweedkms.DecryptRequest) (*seaweedkms.DecryptResponse, error) {
- if req == nil {
- return nil, fmt.Errorf("DecryptRequest cannot be nil")
- }
- if len(req.CiphertextBlob) == 0 {
- return nil, fmt.Errorf("CiphertextBlob cannot be empty")
- }
- // Parse the ciphertext envelope to extract key information
- envelope, err := seaweedkms.ParseEnvelope(req.CiphertextBlob)
- if err != nil {
- return nil, fmt.Errorf("failed to parse ciphertext envelope: %w", err)
- }
- keyID := envelope.KeyID
- if keyID == "" {
- return nil, fmt.Errorf("envelope missing key ID")
- }
- // Use the ciphertext from envelope
- ciphertext := envelope.Ciphertext
- // Prepare decryption data
- decryptData := map[string]interface{}{
- "ciphertext": ciphertext,
- }
- // Add encryption context if provided
- if len(req.EncryptionContext) > 0 {
- contextJSON, err := json.Marshal(req.EncryptionContext)
- if err != nil {
- return nil, fmt.Errorf("failed to marshal encryption context: %w", err)
- }
- decryptData["context"] = base64.StdEncoding.EncodeToString(contextJSON)
- }
- // Call OpenBao/Vault Transit decrypt endpoint
- path := fmt.Sprintf("%s/decrypt/%s", p.transitPath, keyID)
- glog.V(4).Infof("OpenBao KMS: Decrypting data key using key %s", keyID)
- secret, err := p.client.Logical().WriteWithContext(ctx, path, decryptData)
- if err != nil {
- return nil, p.convertVaultError(err, keyID)
- }
- if secret == nil || secret.Data == nil {
- return nil, fmt.Errorf("no data returned from OpenBao/Vault decrypt operation")
- }
- plaintextB64, ok := secret.Data["plaintext"].(string)
- if !ok {
- return nil, fmt.Errorf("invalid plaintext format from OpenBao/Vault")
- }
- plaintext, err := base64.StdEncoding.DecodeString(plaintextB64)
- if err != nil {
- return nil, fmt.Errorf("failed to decode plaintext from OpenBao/Vault: %w", err)
- }
- response := &seaweedkms.DecryptResponse{
- KeyID: keyID,
- Plaintext: plaintext,
- }
- glog.V(4).Infof("OpenBao KMS: Decrypted data key using key %s", keyID)
- return response, nil
- }
- // DescribeKey validates that a key exists and returns its metadata
- func (p *OpenBaoKMSProvider) DescribeKey(ctx context.Context, req *seaweedkms.DescribeKeyRequest) (*seaweedkms.DescribeKeyResponse, error) {
- if req == nil {
- return nil, fmt.Errorf("DescribeKeyRequest cannot be nil")
- }
- if req.KeyID == "" {
- return nil, fmt.Errorf("KeyID is required")
- }
- // Get key information from OpenBao/Vault
- path := fmt.Sprintf("%s/keys/%s", p.transitPath, req.KeyID)
- glog.V(4).Infof("OpenBao KMS: Describing key %s", req.KeyID)
- secret, err := p.client.Logical().ReadWithContext(ctx, path)
- if err != nil {
- return nil, p.convertVaultError(err, req.KeyID)
- }
- if secret == nil || secret.Data == nil {
- return nil, &seaweedkms.KMSError{
- Code: seaweedkms.ErrCodeNotFoundException,
- Message: fmt.Sprintf("Key not found: %s", req.KeyID),
- KeyID: req.KeyID,
- }
- }
- response := &seaweedkms.DescribeKeyResponse{
- KeyID: req.KeyID,
- ARN: fmt.Sprintf("openbao:%s:key:%s", p.address, req.KeyID),
- Description: "OpenBao/Vault Transit engine key",
- }
- // Check key type and set usage
- if keyType, ok := secret.Data["type"].(string); ok {
- if keyType == "aes256-gcm96" || keyType == "aes128-gcm96" || keyType == "chacha20-poly1305" {
- response.KeyUsage = seaweedkms.KeyUsageEncryptDecrypt
- } else {
- // Default to data key generation if not an encrypt/decrypt type
- response.KeyUsage = seaweedkms.KeyUsageGenerateDataKey
- }
- } else {
- // If type is missing, default to data key generation
- response.KeyUsage = seaweedkms.KeyUsageGenerateDataKey
- }
- // OpenBao/Vault keys are enabled by default (no disabled state in transit)
- response.KeyState = seaweedkms.KeyStateEnabled
- // Keys in OpenBao/Vault transit are service-managed
- response.Origin = seaweedkms.KeyOriginOpenBao
- glog.V(4).Infof("OpenBao KMS: Described key %s (state: %s)", req.KeyID, response.KeyState)
- return response, nil
- }
- // GetKeyID resolves a key name (already the full key ID in OpenBao/Vault)
- func (p *OpenBaoKMSProvider) GetKeyID(ctx context.Context, keyIdentifier string) (string, error) {
- if keyIdentifier == "" {
- return "", fmt.Errorf("key identifier cannot be empty")
- }
- // Use DescribeKey to validate the key exists
- descReq := &seaweedkms.DescribeKeyRequest{KeyID: keyIdentifier}
- descResp, err := p.DescribeKey(ctx, descReq)
- if err != nil {
- return "", fmt.Errorf("failed to resolve key identifier %s: %w", keyIdentifier, err)
- }
- return descResp.KeyID, nil
- }
- // Close cleans up any resources used by the provider
- func (p *OpenBaoKMSProvider) Close() error {
- // OpenBao/Vault client doesn't require explicit cleanup
- glog.V(2).Infof("OpenBao/Vault KMS provider closed")
- return nil
- }
- // convertVaultError converts OpenBao/Vault errors to our standard KMS errors
- func (p *OpenBaoKMSProvider) convertVaultError(err error, keyID string) error {
- errMsg := err.Error()
- if strings.Contains(errMsg, "not found") || strings.Contains(errMsg, "no handler") {
- return &seaweedkms.KMSError{
- Code: seaweedkms.ErrCodeNotFoundException,
- Message: fmt.Sprintf("Key not found in OpenBao/Vault: %v", err),
- KeyID: keyID,
- }
- }
- if strings.Contains(errMsg, "permission") || strings.Contains(errMsg, "denied") || strings.Contains(errMsg, "forbidden") {
- return &seaweedkms.KMSError{
- Code: seaweedkms.ErrCodeAccessDenied,
- Message: fmt.Sprintf("Access denied to OpenBao/Vault: %v", err),
- KeyID: keyID,
- }
- }
- if strings.Contains(errMsg, "disabled") || strings.Contains(errMsg, "unavailable") {
- return &seaweedkms.KMSError{
- Code: seaweedkms.ErrCodeKeyUnavailable,
- Message: fmt.Sprintf("Key unavailable in OpenBao/Vault: %v", err),
- KeyID: keyID,
- }
- }
- // For unknown errors, wrap as internal failure
- return &seaweedkms.KMSError{
- Code: seaweedkms.ErrCodeKMSInternalFailure,
- Message: fmt.Sprintf("OpenBao/Vault error: %v", err),
- KeyID: keyID,
- }
- }
|