You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
403 lines
13 KiB
403 lines
13 KiB
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,
|
|
}
|
|
}
|