openbao_kms.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. package openbao
  2. import (
  3. "context"
  4. "crypto/rand"
  5. "encoding/base64"
  6. "encoding/json"
  7. "fmt"
  8. "strings"
  9. "time"
  10. vault "github.com/hashicorp/vault/api"
  11. "github.com/seaweedfs/seaweedfs/weed/glog"
  12. seaweedkms "github.com/seaweedfs/seaweedfs/weed/kms"
  13. "github.com/seaweedfs/seaweedfs/weed/util"
  14. )
  15. func init() {
  16. // Register the OpenBao/Vault KMS provider
  17. seaweedkms.RegisterProvider("openbao", NewOpenBaoKMSProvider)
  18. seaweedkms.RegisterProvider("vault", NewOpenBaoKMSProvider) // Alias for compatibility
  19. }
  20. // OpenBaoKMSProvider implements the KMSProvider interface using OpenBao/Vault Transit engine
  21. type OpenBaoKMSProvider struct {
  22. client *vault.Client
  23. transitPath string // Transit engine mount path (default: "transit")
  24. address string
  25. }
  26. // OpenBaoKMSConfig contains configuration for the OpenBao/Vault KMS provider
  27. type OpenBaoKMSConfig struct {
  28. Address string `json:"address"` // Vault address (e.g., "http://localhost:8200")
  29. Token string `json:"token"` // Vault token for authentication
  30. RoleID string `json:"role_id"` // AppRole role ID (alternative to token)
  31. SecretID string `json:"secret_id"` // AppRole secret ID (alternative to token)
  32. TransitPath string `json:"transit_path"` // Transit engine mount path (default: "transit")
  33. TLSSkipVerify bool `json:"tls_skip_verify"` // Skip TLS verification (for testing)
  34. CACert string `json:"ca_cert"` // Path to CA certificate
  35. ClientCert string `json:"client_cert"` // Path to client certificate
  36. ClientKey string `json:"client_key"` // Path to client private key
  37. RequestTimeout int `json:"request_timeout"` // Request timeout in seconds (default: 30)
  38. }
  39. // NewOpenBaoKMSProvider creates a new OpenBao/Vault KMS provider
  40. func NewOpenBaoKMSProvider(config util.Configuration) (seaweedkms.KMSProvider, error) {
  41. if config == nil {
  42. return nil, fmt.Errorf("OpenBao/Vault KMS configuration is required")
  43. }
  44. // Extract configuration
  45. address := config.GetString("address")
  46. if address == "" {
  47. address = "http://localhost:8200" // Default OpenBao address
  48. }
  49. token := config.GetString("token")
  50. roleID := config.GetString("role_id")
  51. secretID := config.GetString("secret_id")
  52. transitPath := config.GetString("transit_path")
  53. if transitPath == "" {
  54. transitPath = "transit" // Default transit path
  55. }
  56. tlsSkipVerify := config.GetBool("tls_skip_verify")
  57. caCert := config.GetString("ca_cert")
  58. clientCert := config.GetString("client_cert")
  59. clientKey := config.GetString("client_key")
  60. requestTimeout := config.GetInt("request_timeout")
  61. if requestTimeout == 0 {
  62. requestTimeout = 30 // Default 30 seconds
  63. }
  64. // Create Vault client configuration
  65. vaultConfig := vault.DefaultConfig()
  66. vaultConfig.Address = address
  67. vaultConfig.Timeout = time.Duration(requestTimeout) * time.Second
  68. // Configure TLS
  69. if tlsSkipVerify || caCert != "" || (clientCert != "" && clientKey != "") {
  70. tlsConfig := &vault.TLSConfig{
  71. Insecure: tlsSkipVerify,
  72. }
  73. if caCert != "" {
  74. tlsConfig.CACert = caCert
  75. }
  76. if clientCert != "" && clientKey != "" {
  77. tlsConfig.ClientCert = clientCert
  78. tlsConfig.ClientKey = clientKey
  79. }
  80. if err := vaultConfig.ConfigureTLS(tlsConfig); err != nil {
  81. return nil, fmt.Errorf("failed to configure TLS: %w", err)
  82. }
  83. }
  84. // Create Vault client
  85. client, err := vault.NewClient(vaultConfig)
  86. if err != nil {
  87. return nil, fmt.Errorf("failed to create OpenBao/Vault client: %w", err)
  88. }
  89. // Authenticate
  90. if token != "" {
  91. client.SetToken(token)
  92. glog.V(1).Infof("OpenBao KMS: Using token authentication")
  93. } else if roleID != "" && secretID != "" {
  94. if err := authenticateAppRole(client, roleID, secretID); err != nil {
  95. return nil, fmt.Errorf("failed to authenticate with AppRole: %w", err)
  96. }
  97. glog.V(1).Infof("OpenBao KMS: Using AppRole authentication")
  98. } else {
  99. return nil, fmt.Errorf("either token or role_id+secret_id must be provided")
  100. }
  101. provider := &OpenBaoKMSProvider{
  102. client: client,
  103. transitPath: transitPath,
  104. address: address,
  105. }
  106. glog.V(1).Infof("OpenBao/Vault KMS provider initialized at %s", address)
  107. return provider, nil
  108. }
  109. // authenticateAppRole authenticates using AppRole method
  110. func authenticateAppRole(client *vault.Client, roleID, secretID string) error {
  111. data := map[string]interface{}{
  112. "role_id": roleID,
  113. "secret_id": secretID,
  114. }
  115. secret, err := client.Logical().Write("auth/approle/login", data)
  116. if err != nil {
  117. return fmt.Errorf("AppRole authentication failed: %w", err)
  118. }
  119. if secret == nil || secret.Auth == nil {
  120. return fmt.Errorf("AppRole authentication returned empty token")
  121. }
  122. client.SetToken(secret.Auth.ClientToken)
  123. return nil
  124. }
  125. // GenerateDataKey generates a new data encryption key using OpenBao/Vault Transit
  126. func (p *OpenBaoKMSProvider) GenerateDataKey(ctx context.Context, req *seaweedkms.GenerateDataKeyRequest) (*seaweedkms.GenerateDataKeyResponse, error) {
  127. if req == nil {
  128. return nil, fmt.Errorf("GenerateDataKeyRequest cannot be nil")
  129. }
  130. if req.KeyID == "" {
  131. return nil, fmt.Errorf("KeyID is required")
  132. }
  133. // Validate key spec
  134. var keySize int
  135. switch req.KeySpec {
  136. case seaweedkms.KeySpecAES256:
  137. keySize = 32 // 256 bits
  138. default:
  139. return nil, fmt.Errorf("unsupported key spec: %s", req.KeySpec)
  140. }
  141. // Generate data key locally (similar to Azure/GCP approach)
  142. dataKey := make([]byte, keySize)
  143. if _, err := rand.Read(dataKey); err != nil {
  144. return nil, fmt.Errorf("failed to generate random data key: %w", err)
  145. }
  146. // Encrypt the data key using OpenBao/Vault Transit
  147. glog.V(4).Infof("OpenBao KMS: Encrypting data key using key %s", req.KeyID)
  148. // Prepare encryption data
  149. encryptData := map[string]interface{}{
  150. "plaintext": base64.StdEncoding.EncodeToString(dataKey),
  151. }
  152. // Add encryption context if provided
  153. if len(req.EncryptionContext) > 0 {
  154. contextJSON, err := json.Marshal(req.EncryptionContext)
  155. if err != nil {
  156. return nil, fmt.Errorf("failed to marshal encryption context: %w", err)
  157. }
  158. encryptData["context"] = base64.StdEncoding.EncodeToString(contextJSON)
  159. }
  160. // Call OpenBao/Vault Transit encrypt endpoint
  161. path := fmt.Sprintf("%s/encrypt/%s", p.transitPath, req.KeyID)
  162. secret, err := p.client.Logical().WriteWithContext(ctx, path, encryptData)
  163. if err != nil {
  164. return nil, p.convertVaultError(err, req.KeyID)
  165. }
  166. if secret == nil || secret.Data == nil {
  167. return nil, fmt.Errorf("no data returned from OpenBao/Vault encrypt operation")
  168. }
  169. ciphertext, ok := secret.Data["ciphertext"].(string)
  170. if !ok {
  171. return nil, fmt.Errorf("invalid ciphertext format from OpenBao/Vault")
  172. }
  173. // Create standardized envelope format for consistent API behavior
  174. envelopeBlob, err := seaweedkms.CreateEnvelope("openbao", req.KeyID, ciphertext, nil)
  175. if err != nil {
  176. return nil, fmt.Errorf("failed to create ciphertext envelope: %w", err)
  177. }
  178. response := &seaweedkms.GenerateDataKeyResponse{
  179. KeyID: req.KeyID,
  180. Plaintext: dataKey,
  181. CiphertextBlob: envelopeBlob, // Store in standardized envelope format
  182. }
  183. glog.V(4).Infof("OpenBao KMS: Generated and encrypted data key using key %s", req.KeyID)
  184. return response, nil
  185. }
  186. // Decrypt decrypts an encrypted data key using OpenBao/Vault Transit
  187. func (p *OpenBaoKMSProvider) Decrypt(ctx context.Context, req *seaweedkms.DecryptRequest) (*seaweedkms.DecryptResponse, error) {
  188. if req == nil {
  189. return nil, fmt.Errorf("DecryptRequest cannot be nil")
  190. }
  191. if len(req.CiphertextBlob) == 0 {
  192. return nil, fmt.Errorf("CiphertextBlob cannot be empty")
  193. }
  194. // Parse the ciphertext envelope to extract key information
  195. envelope, err := seaweedkms.ParseEnvelope(req.CiphertextBlob)
  196. if err != nil {
  197. return nil, fmt.Errorf("failed to parse ciphertext envelope: %w", err)
  198. }
  199. keyID := envelope.KeyID
  200. if keyID == "" {
  201. return nil, fmt.Errorf("envelope missing key ID")
  202. }
  203. // Use the ciphertext from envelope
  204. ciphertext := envelope.Ciphertext
  205. // Prepare decryption data
  206. decryptData := map[string]interface{}{
  207. "ciphertext": ciphertext,
  208. }
  209. // Add encryption context if provided
  210. if len(req.EncryptionContext) > 0 {
  211. contextJSON, err := json.Marshal(req.EncryptionContext)
  212. if err != nil {
  213. return nil, fmt.Errorf("failed to marshal encryption context: %w", err)
  214. }
  215. decryptData["context"] = base64.StdEncoding.EncodeToString(contextJSON)
  216. }
  217. // Call OpenBao/Vault Transit decrypt endpoint
  218. path := fmt.Sprintf("%s/decrypt/%s", p.transitPath, keyID)
  219. glog.V(4).Infof("OpenBao KMS: Decrypting data key using key %s", keyID)
  220. secret, err := p.client.Logical().WriteWithContext(ctx, path, decryptData)
  221. if err != nil {
  222. return nil, p.convertVaultError(err, keyID)
  223. }
  224. if secret == nil || secret.Data == nil {
  225. return nil, fmt.Errorf("no data returned from OpenBao/Vault decrypt operation")
  226. }
  227. plaintextB64, ok := secret.Data["plaintext"].(string)
  228. if !ok {
  229. return nil, fmt.Errorf("invalid plaintext format from OpenBao/Vault")
  230. }
  231. plaintext, err := base64.StdEncoding.DecodeString(plaintextB64)
  232. if err != nil {
  233. return nil, fmt.Errorf("failed to decode plaintext from OpenBao/Vault: %w", err)
  234. }
  235. response := &seaweedkms.DecryptResponse{
  236. KeyID: keyID,
  237. Plaintext: plaintext,
  238. }
  239. glog.V(4).Infof("OpenBao KMS: Decrypted data key using key %s", keyID)
  240. return response, nil
  241. }
  242. // DescribeKey validates that a key exists and returns its metadata
  243. func (p *OpenBaoKMSProvider) DescribeKey(ctx context.Context, req *seaweedkms.DescribeKeyRequest) (*seaweedkms.DescribeKeyResponse, error) {
  244. if req == nil {
  245. return nil, fmt.Errorf("DescribeKeyRequest cannot be nil")
  246. }
  247. if req.KeyID == "" {
  248. return nil, fmt.Errorf("KeyID is required")
  249. }
  250. // Get key information from OpenBao/Vault
  251. path := fmt.Sprintf("%s/keys/%s", p.transitPath, req.KeyID)
  252. glog.V(4).Infof("OpenBao KMS: Describing key %s", req.KeyID)
  253. secret, err := p.client.Logical().ReadWithContext(ctx, path)
  254. if err != nil {
  255. return nil, p.convertVaultError(err, req.KeyID)
  256. }
  257. if secret == nil || secret.Data == nil {
  258. return nil, &seaweedkms.KMSError{
  259. Code: seaweedkms.ErrCodeNotFoundException,
  260. Message: fmt.Sprintf("Key not found: %s", req.KeyID),
  261. KeyID: req.KeyID,
  262. }
  263. }
  264. response := &seaweedkms.DescribeKeyResponse{
  265. KeyID: req.KeyID,
  266. ARN: fmt.Sprintf("openbao:%s:key:%s", p.address, req.KeyID),
  267. Description: "OpenBao/Vault Transit engine key",
  268. }
  269. // Check key type and set usage
  270. if keyType, ok := secret.Data["type"].(string); ok {
  271. if keyType == "aes256-gcm96" || keyType == "aes128-gcm96" || keyType == "chacha20-poly1305" {
  272. response.KeyUsage = seaweedkms.KeyUsageEncryptDecrypt
  273. } else {
  274. // Default to data key generation if not an encrypt/decrypt type
  275. response.KeyUsage = seaweedkms.KeyUsageGenerateDataKey
  276. }
  277. } else {
  278. // If type is missing, default to data key generation
  279. response.KeyUsage = seaweedkms.KeyUsageGenerateDataKey
  280. }
  281. // OpenBao/Vault keys are enabled by default (no disabled state in transit)
  282. response.KeyState = seaweedkms.KeyStateEnabled
  283. // Keys in OpenBao/Vault transit are service-managed
  284. response.Origin = seaweedkms.KeyOriginOpenBao
  285. glog.V(4).Infof("OpenBao KMS: Described key %s (state: %s)", req.KeyID, response.KeyState)
  286. return response, nil
  287. }
  288. // GetKeyID resolves a key name (already the full key ID in OpenBao/Vault)
  289. func (p *OpenBaoKMSProvider) GetKeyID(ctx context.Context, keyIdentifier string) (string, error) {
  290. if keyIdentifier == "" {
  291. return "", fmt.Errorf("key identifier cannot be empty")
  292. }
  293. // Use DescribeKey to validate the key exists
  294. descReq := &seaweedkms.DescribeKeyRequest{KeyID: keyIdentifier}
  295. descResp, err := p.DescribeKey(ctx, descReq)
  296. if err != nil {
  297. return "", fmt.Errorf("failed to resolve key identifier %s: %w", keyIdentifier, err)
  298. }
  299. return descResp.KeyID, nil
  300. }
  301. // Close cleans up any resources used by the provider
  302. func (p *OpenBaoKMSProvider) Close() error {
  303. // OpenBao/Vault client doesn't require explicit cleanup
  304. glog.V(2).Infof("OpenBao/Vault KMS provider closed")
  305. return nil
  306. }
  307. // convertVaultError converts OpenBao/Vault errors to our standard KMS errors
  308. func (p *OpenBaoKMSProvider) convertVaultError(err error, keyID string) error {
  309. errMsg := err.Error()
  310. if strings.Contains(errMsg, "not found") || strings.Contains(errMsg, "no handler") {
  311. return &seaweedkms.KMSError{
  312. Code: seaweedkms.ErrCodeNotFoundException,
  313. Message: fmt.Sprintf("Key not found in OpenBao/Vault: %v", err),
  314. KeyID: keyID,
  315. }
  316. }
  317. if strings.Contains(errMsg, "permission") || strings.Contains(errMsg, "denied") || strings.Contains(errMsg, "forbidden") {
  318. return &seaweedkms.KMSError{
  319. Code: seaweedkms.ErrCodeAccessDenied,
  320. Message: fmt.Sprintf("Access denied to OpenBao/Vault: %v", err),
  321. KeyID: keyID,
  322. }
  323. }
  324. if strings.Contains(errMsg, "disabled") || strings.Contains(errMsg, "unavailable") {
  325. return &seaweedkms.KMSError{
  326. Code: seaweedkms.ErrCodeKeyUnavailable,
  327. Message: fmt.Sprintf("Key unavailable in OpenBao/Vault: %v", err),
  328. KeyID: keyID,
  329. }
  330. }
  331. // For unknown errors, wrap as internal failure
  332. return &seaweedkms.KMSError{
  333. Code: seaweedkms.ErrCodeKMSInternalFailure,
  334. Message: fmt.Sprintf("OpenBao/Vault error: %v", err),
  335. KeyID: keyID,
  336. }
  337. }