passbolt.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. /*
  2. Copyright © The ESO Authors
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. https://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. // Package passbolt implements a provider for Passbolt password manager.
  14. // It allows fetching secrets stored in Passbolt using their REST API.
  15. package passbolt
  16. import (
  17. "context"
  18. "crypto/tls"
  19. "crypto/x509"
  20. "errors"
  21. "fmt"
  22. "net/http"
  23. "net/url"
  24. "regexp"
  25. "strings"
  26. "github.com/passbolt/go-passbolt/api"
  27. "github.com/passbolt/go-passbolt/helper"
  28. corev1 "k8s.io/api/core/v1"
  29. ctrl "sigs.k8s.io/controller-runtime"
  30. kclient "sigs.k8s.io/controller-runtime/pkg/client"
  31. "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
  32. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  33. "github.com/external-secrets/external-secrets/runtime/esutils"
  34. "github.com/external-secrets/external-secrets/runtime/esutils/resolvers"
  35. )
  36. var log = ctrl.Log.WithName("provider").WithName("passbolt")
  37. var errPassboltCustomFieldNotFound = errors.New("custom field not found")
  38. const (
  39. customFieldPrefix = "custom_fields."
  40. errPassboltStoreMissingProvider = "missing: spec.provider.passbolt"
  41. errPassboltStoreMissingAuth = "missing: spec.provider.passbolt.auth"
  42. errPassboltStoreMissingAuthPassword = "missing: spec.provider.passbolt.auth.passwordSecretRef"
  43. errPassboltStoreMissingAuthPrivateKey = "missing: spec.provider.passbolt.auth.privateKeySecretRef"
  44. errPassboltStoreMissingHost = "missing: spec.provider.passbolt.host"
  45. errPassboltExternalSecretMissingFindNameRegExp = "missing: find.name.regexp"
  46. errPassboltStoreHostSchemeNotHTTPS = "host Url has to be https scheme"
  47. errPassboltSecretPropertyInvalid = "property must be one of name, username, uri, password, description, or " + customFieldPrefix + "<name>"
  48. errPassboltCAInvalid = "failed to parse CA certificate for Passbolt provider"
  49. errPassboltUnexpectedTransport = "unexpected default http transport type"
  50. errNotImplemented = "not implemented"
  51. )
  52. // ProviderPassbolt implements the External Secrets provider interface for Passbolt.
  53. type ProviderPassbolt struct {
  54. client *api.Client
  55. }
  56. // Capabilities return the provider supported capabilities (ReadOnly, WriteOnly, ReadWrite).
  57. func (provider *ProviderPassbolt) Capabilities() esv1.SecretStoreCapabilities {
  58. return esv1.SecretStoreReadOnly
  59. }
  60. // NewClient constructs a new secrets client based on the provided store.
  61. func (provider *ProviderPassbolt) NewClient(ctx context.Context, store esv1.GenericStore, kube kclient.Client, namespace string) (esv1.SecretsClient, error) {
  62. config := store.GetSpec().Provider.Passbolt
  63. password, err := resolvers.SecretKeyRef(
  64. ctx,
  65. kube,
  66. store.GetKind(),
  67. namespace,
  68. config.Auth.PasswordSecretRef,
  69. )
  70. if err != nil {
  71. return nil, err
  72. }
  73. privateKey, err := resolvers.SecretKeyRef(
  74. ctx,
  75. kube,
  76. store.GetKind(),
  77. namespace,
  78. config.Auth.PrivateKeySecretRef,
  79. )
  80. if err != nil {
  81. return nil, err
  82. }
  83. httpClient, err := buildHTTPClient(ctx, config, kube, store.GetKind(), namespace)
  84. if err != nil {
  85. return nil, err
  86. }
  87. client, err := api.NewClient(httpClient, "", config.Host, privateKey, password)
  88. if err != nil {
  89. return nil, err
  90. }
  91. // Login immediately (like CLI does)
  92. if err := client.Login(ctx); err != nil {
  93. return nil, err
  94. }
  95. // Prefetch caches for V5 metadata decryption performance (CLI pattern)
  96. // This caches session keys and metadata keys for fast V5 decryption
  97. if _, _, err := client.PreFetchCaches(ctx); err != nil {
  98. log.V(1).Info("prefetch caches failed (non-fatal)", "error", err)
  99. }
  100. provider.client = client
  101. return provider, nil
  102. }
  103. // SecretExists checks if a secret exists in Passbolt.
  104. func (provider *ProviderPassbolt) SecretExists(_ context.Context, _ esv1.PushSecretRemoteRef) (bool, error) {
  105. return false, errors.New(errNotImplemented)
  106. }
  107. // GetSecret retrieves a secret from Passbolt.
  108. func (provider *ProviderPassbolt) GetSecret(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) ([]byte, error) {
  109. if err := assureLoggedIn(ctx, provider.client); err != nil {
  110. return nil, err
  111. }
  112. secret, err := provider.getPassboltSecret(ctx, ref.Key)
  113. if err != nil {
  114. return nil, err
  115. }
  116. if ref.Property == "" {
  117. return esutils.JSONMarshal(secret)
  118. }
  119. return secret.GetProp(ref.Property)
  120. }
  121. // PushSecret is not implemented for Passbolt as it is read-only.
  122. func (provider *ProviderPassbolt) PushSecret(_ context.Context, _ *corev1.Secret, _ esv1.PushSecretData) error {
  123. return errors.New(errNotImplemented)
  124. }
  125. // DeleteSecret is not implemented for Passbolt as it is read-only.
  126. func (provider *ProviderPassbolt) DeleteSecret(_ context.Context, _ esv1.PushSecretRemoteRef) error {
  127. return errors.New(errNotImplemented)
  128. }
  129. // Validate performs validation of the Passbolt provider configuration.
  130. func (provider *ProviderPassbolt) Validate() (esv1.ValidationResult, error) {
  131. return esv1.ValidationResultUnknown, nil
  132. }
  133. // GetSecretMap retrieves a secret and returns it as a map of key/value pairs.
  134. func (provider *ProviderPassbolt) GetSecretMap(_ context.Context, _ esv1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
  135. return nil, errors.New(errNotImplemented)
  136. }
  137. // GetAllSecrets retrieves all secrets from Passbolt that match the given criteria.
  138. func (provider *ProviderPassbolt) GetAllSecrets(ctx context.Context, ref esv1.ExternalSecretFind) (map[string][]byte, error) {
  139. res := make(map[string][]byte)
  140. if ref.Name == nil || ref.Name.RegExp == "" {
  141. return res, errors.New(errPassboltExternalSecretMissingFindNameRegExp)
  142. }
  143. if err := assureLoggedIn(ctx, provider.client); err != nil {
  144. return nil, err
  145. }
  146. resources, err := provider.client.GetResources(ctx, &api.GetResourcesOptions{})
  147. if err != nil {
  148. return nil, err
  149. }
  150. nameRegexp, err := regexp.Compile(ref.Name.RegExp)
  151. if err != nil {
  152. return nil, err
  153. }
  154. // NOTE: For V5 resources, metadata (including name) is encrypted, so we must
  155. // decrypt each resource before filtering. This means all secrets are decrypted
  156. // even if they don't match the filter, which may impact performance with large
  157. // secret stores.
  158. for _, resource := range resources {
  159. secret, err := provider.secretFromResource(ctx, &resource)
  160. if err != nil {
  161. return nil, err
  162. }
  163. // Filter by decrypted name (works for both V4 and V5)
  164. if !nameRegexp.MatchString(secret.Name) {
  165. continue
  166. }
  167. marshaled, err := esutils.JSONMarshal(secret)
  168. if err != nil {
  169. return nil, err
  170. }
  171. res[resource.ID] = marshaled
  172. }
  173. return res, nil
  174. }
  175. // Close implements cleanup operations for the Passbolt provider.
  176. func (provider *ProviderPassbolt) Close(ctx context.Context) error {
  177. // Save any pending session keys discovered during decryption (CLI pattern)
  178. // This improves performance for future connections
  179. _, _ = provider.client.SavePendingSessionKeys(ctx) // Best effort
  180. return provider.client.Logout(ctx)
  181. }
  182. // ValidateStore validates the Passbolt SecretStore resource configuration.
  183. func (provider *ProviderPassbolt) ValidateStore(store esv1.GenericStore) (admission.Warnings, error) {
  184. config := store.GetSpec().Provider.Passbolt
  185. if config == nil {
  186. return nil, errors.New(errPassboltStoreMissingProvider)
  187. }
  188. if config.Auth == nil {
  189. return nil, errors.New(errPassboltStoreMissingAuth)
  190. }
  191. if config.Auth.PasswordSecretRef == nil || config.Auth.PasswordSecretRef.Name == "" || config.Auth.PasswordSecretRef.Key == "" {
  192. return nil, errors.New(errPassboltStoreMissingAuthPassword)
  193. }
  194. if config.Auth.PrivateKeySecretRef == nil || config.Auth.PrivateKeySecretRef.Name == "" || config.Auth.PrivateKeySecretRef.Key == "" {
  195. return nil, errors.New(errPassboltStoreMissingAuthPrivateKey)
  196. }
  197. if config.Host == "" {
  198. return nil, errors.New(errPassboltStoreMissingHost)
  199. }
  200. host, err := url.Parse(config.Host)
  201. if err != nil {
  202. return nil, err
  203. }
  204. if host.Scheme != "https" {
  205. return nil, errors.New(errPassboltStoreHostSchemeNotHTTPS)
  206. }
  207. return nil, nil
  208. }
  209. // Secret represents a Passbolt secret with its properties.
  210. type Secret struct {
  211. Name string `json:"name"`
  212. Username string `json:"username"`
  213. Password string `json:"password"`
  214. URI string `json:"uri"`
  215. Description string `json:"description"`
  216. // CustomFields holds any custom fields defined on the resource, keyed by
  217. // the field's display name. Fields whose name is stored encrypted
  218. // (secret_key rather than metadata_key) are keyed by their decrypted name.
  219. CustomFields map[string]string `json:"custom_fields,omitempty"`
  220. }
  221. // GetProp retrieves a specific property from the Passbolt secret.
  222. //
  223. // Supported properties: name, username, uri, password, description.
  224. // Custom fields are accessed via the "custom_fields.<name>" prefix, where
  225. // <name> is the field's metadata_key (display name) as configured in Passbolt.
  226. func (ps Secret) GetProp(key string) ([]byte, error) {
  227. switch key {
  228. case "name":
  229. return []byte(ps.Name), nil
  230. case "username":
  231. return []byte(ps.Username), nil
  232. case "uri":
  233. return []byte(ps.URI), nil
  234. case "password":
  235. return []byte(ps.Password), nil
  236. case "description":
  237. return []byte(ps.Description), nil
  238. default:
  239. if fieldName, ok := strings.CutPrefix(key, customFieldPrefix); ok {
  240. val, exists := ps.CustomFields[fieldName]
  241. if !exists {
  242. return nil, fmt.Errorf("%w: %s", errPassboltCustomFieldNotFound, fieldName)
  243. }
  244. return []byte(val), nil
  245. }
  246. return nil, errors.New(errPassboltSecretPropertyInvalid)
  247. }
  248. }
  249. func (provider *ProviderPassbolt) getPassboltSecret(ctx context.Context, id string) (*Secret, error) {
  250. resource, err := provider.client.GetResource(ctx, id)
  251. if err != nil {
  252. return nil, err
  253. }
  254. return provider.secretFromResource(ctx, resource)
  255. }
  256. // secretFromResource decrypts an already-fetched resource into a Secret,
  257. // sparing callers that hold the resource a redundant GetResource call.
  258. func (provider *ProviderPassbolt) secretFromResource(ctx context.Context, resource *api.Resource) (*Secret, error) {
  259. rType, err := provider.client.GetResourceType(ctx, resource.ResourceTypeID)
  260. if err != nil {
  261. return nil, err
  262. }
  263. secretData, err := provider.client.GetSecret(ctx, resource.ID)
  264. if err != nil {
  265. return nil, err
  266. }
  267. _, metaFields, secretFields, err := helper.GetResourceFieldMaps(provider.client, *resource, *secretData, *rType, true)
  268. if err != nil {
  269. return nil, err
  270. }
  271. return &Secret{
  272. Name: helper.GetStringField(metaFields, "name"),
  273. Username: helper.GetStringField(metaFields, "username"),
  274. URI: helper.GetStringField(metaFields, "uri"),
  275. Password: helper.GetStringField(secretFields, "password"),
  276. Description: helper.GetStringField(metaFields, "description"),
  277. CustomFields: helper.ParseCustomFields(metaFields, secretFields).Map(),
  278. }, nil
  279. }
  280. func assureLoggedIn(ctx context.Context, client *api.Client) error {
  281. if client.CheckSession(ctx) {
  282. return nil
  283. }
  284. return client.Login(ctx)
  285. }
  286. // buildHTTPClient returns an *http.Client configured with the provider's CA bundle
  287. // or CA provider, if either is set. When neither is set it returns nil so that the
  288. // underlying SDK uses its default HTTP client (and the system root CAs).
  289. func buildHTTPClient(ctx context.Context, config *esv1.PassboltProvider, kube kclient.Client, storeKind, namespace string) (*http.Client, error) {
  290. if len(config.CABundle) == 0 && config.CAProvider == nil {
  291. return nil, nil
  292. }
  293. caCert, err := esutils.FetchCACertFromSource(ctx, esutils.CreateCertOpts{
  294. CABundle: config.CABundle,
  295. CAProvider: config.CAProvider,
  296. StoreKind: storeKind,
  297. Namespace: namespace,
  298. Client: kube,
  299. })
  300. if err != nil {
  301. return nil, err
  302. }
  303. caCertPool := x509.NewCertPool()
  304. if !caCertPool.AppendCertsFromPEM(caCert) {
  305. return nil, errors.New(errPassboltCAInvalid)
  306. }
  307. // Clone the default transport so we keep its proxy/dialer/HTTP2/idle
  308. // connection settings and only override the TLS configuration.
  309. defaultTransport, ok := http.DefaultTransport.(*http.Transport)
  310. if !ok {
  311. return nil, errors.New(errPassboltUnexpectedTransport)
  312. }
  313. transport := defaultTransport.Clone()
  314. if transport.TLSClientConfig == nil {
  315. transport.TLSClientConfig = &tls.Config{}
  316. } else {
  317. transport.TLSClientConfig = transport.TLSClientConfig.Clone()
  318. }
  319. transport.TLSClientConfig.RootCAs = caCertPool
  320. transport.TLSClientConfig.MinVersion = tls.VersionTLS12
  321. return &http.Client{
  322. Transport: transport,
  323. }, nil
  324. }
  325. // NewProvider creates a new Provider instance.
  326. func NewProvider() esv1.Provider {
  327. return &ProviderPassbolt{}
  328. }
  329. // ProviderSpec returns the provider specification for registration.
  330. func ProviderSpec() *esv1.SecretStoreProvider {
  331. return &esv1.SecretStoreProvider{
  332. Passbolt: &esv1.PassboltProvider{},
  333. }
  334. }
  335. // MaintenanceStatus returns the maintenance status of the provider.
  336. func MaintenanceStatus() esv1.MaintenanceStatus {
  337. return esv1.MaintenanceStatusMaintained
  338. }