provider.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  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 ovh implements a provider that enables synchronization with OVHcloud's Secret Manager.
  14. package ovh
  15. import (
  16. "context"
  17. "crypto/tls"
  18. "crypto/x509"
  19. "errors"
  20. "fmt"
  21. "net/http"
  22. "net/url"
  23. "reflect"
  24. "time"
  25. "github.com/google/uuid"
  26. "github.com/ovh/okms-sdk-go"
  27. "github.com/ovh/okms-sdk-go/types"
  28. kclient "sigs.k8s.io/controller-runtime/pkg/client"
  29. "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
  30. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  31. v1 "github.com/external-secrets/external-secrets/apis/meta/v1"
  32. "github.com/external-secrets/external-secrets/runtime/esutils"
  33. "github.com/external-secrets/external-secrets/runtime/esutils/resolvers"
  34. )
  35. const (
  36. emptyTokenSecretRef = "ovh store auth.token.tokenSecretRef cannot be empty"
  37. emptyKeySecretRef = "ovh store auth.mtls.keySecretRef cannot be empty"
  38. emptyCertSecretRef = "ovh store auth.mtls.certSecretRef cannot be empty"
  39. createOvhProviderError = "failed to create new ovh provider client"
  40. createOkmsClientError = "failed to create new okms client"
  41. configureTokenOkmsClientError = "failed to configure token okms client"
  42. configureMtlsOkmsClientError = "failed to configure mtls okms client"
  43. )
  44. // Provider implements the ESO Provider interface for OVHcloud.
  45. type Provider struct {
  46. secretKeyResolver SecretKeyResolver
  47. }
  48. // OkmsClient defines an interface for interacting with the OVH OKMS service.
  49. // It allows for both real API calls and mocking for unit tests.
  50. type OkmsClient interface {
  51. GetSecretV2(ctx context.Context, okmsID uuid.UUID, path string, version *uint32, includeData *bool) (*types.GetSecretV2Response, error)
  52. ListSecretV2(ctx context.Context, okmsID uuid.UUID, pageSize *uint32, pageCursor *string) (*types.ListSecretV2ResponseWithPagination, error)
  53. PostSecretV2(ctx context.Context, okmsID uuid.UUID, body types.PostSecretV2Request) (*types.PostSecretV2Response, error)
  54. PutSecretV2(ctx context.Context, okmsID uuid.UUID, path string, cas *uint32, body types.PutSecretV2Request) (*types.PutSecretV2Response, error)
  55. DeleteSecretV2(ctx context.Context, okmsID uuid.UUID, path string) error
  56. WithCustomHeader(key, value string) *okms.Client
  57. GetSecretsMetadata(ctx context.Context, okmsID uuid.UUID, path string, list bool) (*types.GetMetadataResponse, error)
  58. }
  59. // SecretKeyResolver resolves the value of a key from a Kubernetes Secret.
  60. // It is defined as an interface to allow different implementations, including mocks for testing.
  61. type SecretKeyResolver interface {
  62. Resolve(ctx context.Context, kube kclient.Client, ovhStoreKind string, ovhStoreNameSpace string, secretRef v1.SecretKeySelector) (string, error)
  63. }
  64. // DefaultSecretKeyResolver is the default implementation for resolving keys from Kubernetes Secrets.
  65. type DefaultSecretKeyResolver struct{}
  66. type ovhClient struct {
  67. ovhStoreNameSpace string
  68. ovhStoreKind string
  69. kube kclient.Client
  70. okmsID uuid.UUID
  71. cas bool
  72. okmsTimeout time.Duration
  73. okmsClient OkmsClient
  74. }
  75. var _ esv1.SecretsClient = &ovhClient{}
  76. // Resolve returns the value of the referenced key from a Kubernetes Secret.
  77. func (r DefaultSecretKeyResolver) Resolve(ctx context.Context, kube kclient.Client, ovhStoreKind, ovhStoreNameSpace string, secretRef v1.SecretKeySelector) (string, error) {
  78. return resolvers.SecretKeyRef(ctx, kube, ovhStoreKind, ovhStoreNameSpace, &secretRef)
  79. }
  80. // NewClient creates a new Provider client.
  81. func (p *Provider) NewClient(ctx context.Context, store esv1.GenericStore, kube kclient.Client, namespace string) (esv1.SecretsClient, error) {
  82. // Validate Store before creating a client from it.
  83. _, err := p.ValidateStore(store)
  84. if err != nil {
  85. return nil, fmt.Errorf("%s: store validation failed: %w", createOvhProviderError, err)
  86. }
  87. if kube == nil {
  88. return nil, fmt.Errorf("%s: controller-runtime client is nil", createOvhProviderError)
  89. }
  90. ovhStore := store.GetSpec().Provider.OVHcloud
  91. // ovhClient configuration.
  92. okmsID, err := uuid.Parse(ovhStore.OkmsID)
  93. if err != nil {
  94. return nil, fmt.Errorf("%s: could not parse okmsID: %w", createOvhProviderError, err)
  95. }
  96. cas := false
  97. if ovhStore.CasRequired != nil {
  98. cas = *ovhStore.CasRequired
  99. }
  100. okmsTimeout := 30 * time.Second
  101. if ovhStore.OkmsTimeout != nil {
  102. okmsTimeout = time.Duration(*ovhStore.OkmsTimeout) * time.Second
  103. }
  104. cl := &ovhClient{
  105. ovhStoreNameSpace: namespace,
  106. ovhStoreKind: store.GetKind(),
  107. kube: kube,
  108. okmsID: okmsID,
  109. cas: cas,
  110. okmsTimeout: okmsTimeout,
  111. }
  112. // Authentication configuration: token or mTLS.
  113. if ovhStore.Auth.ClientToken != nil {
  114. err = configureHTTPTokenClient(ctx, p, cl,
  115. ovhStore.Server, ovhStore.Auth.ClientToken)
  116. } else if ovhStore.Auth.ClientMTLS != nil {
  117. err = configureHTTPMTLSClient(ctx, p, cl,
  118. ovhStore.Server, ovhStore.Auth.ClientMTLS)
  119. }
  120. if err != nil {
  121. return nil, fmt.Errorf("%s: %w", createOvhProviderError, err)
  122. }
  123. return cl, nil
  124. }
  125. // configureHTTPTokenClient clientConfigure the client to use the provided token for HTTP requests.
  126. func configureHTTPTokenClient(ctx context.Context, p *Provider, cl *ovhClient, server string, clientToken *esv1.OvhClientToken) error {
  127. token, err := getToken(ctx, p, cl, clientToken)
  128. if err != nil {
  129. return fmt.Errorf("%s: could not retrieve token: %w", configureTokenOkmsClientError, err)
  130. }
  131. bearerToken := fmt.Sprintf("Bearer %s", token)
  132. // Request a new OKMS client from the OVH SDK.
  133. httpClient := &http.Client{
  134. Timeout: cl.okmsTimeout,
  135. }
  136. cl.okmsClient, err = okms.NewRestAPIClientWithHttp(server, httpClient)
  137. if err != nil {
  138. return fmt.Errorf("%s: %s: %w", configureTokenOkmsClientError, createOkmsClientError, err)
  139. }
  140. if cl.okmsClient == nil {
  141. return fmt.Errorf("%s: okms client is nil", configureTokenOkmsClientError)
  142. }
  143. // Add a custom header.
  144. cl.okmsClient.WithCustomHeader("Authorization", bearerToken)
  145. cl.okmsClient.WithCustomHeader("Content-type", "application/json")
  146. return nil
  147. }
  148. // getToken retrieves the token value from the Kubernetes secret.
  149. func getToken(ctx context.Context, p *Provider, cl *ovhClient, clientToken *esv1.OvhClientToken) (string, error) {
  150. // ClienTokenSecret refers to the Kubernetes secret that stores the token.
  151. tokenSecretRef := clientToken.ClientTokenSecret
  152. // Retrieve the token value.
  153. token, err := p.secretKeyResolver.Resolve(ctx, cl.kube,
  154. cl.ovhStoreKind, cl.ovhStoreNameSpace, tokenSecretRef)
  155. if err != nil {
  156. return "", fmt.Errorf("failed to resolve token secret ref: %w", err)
  157. }
  158. if token == "" {
  159. return "", errors.New(emptyTokenSecretRef)
  160. }
  161. return token, nil
  162. }
  163. // configureHTTPMTLSClient configures the client to use mTLS for HTTP requests.
  164. func configureHTTPMTLSClient(ctx context.Context, p *Provider, cl *ovhClient, server string, clientMTLS *esv1.OvhClientMTLS) error {
  165. httpClient, err := newHTTPClientWithMTLS(ctx, p, cl, clientMTLS)
  166. if err != nil {
  167. return fmt.Errorf("%s: could not create http client:%w", configureMtlsOkmsClientError, err)
  168. }
  169. // Request a new OKMS client from the OVH SDK (mTLS configured).
  170. cl.okmsClient, err = okms.NewRestAPIClientWithHttp(server, httpClient)
  171. if err != nil {
  172. return fmt.Errorf("%s: %s: %w", configureMtlsOkmsClientError, createOkmsClientError, err)
  173. }
  174. if cl.okmsClient == nil {
  175. return fmt.Errorf("%s: okms client is nil", configureMtlsOkmsClientError)
  176. }
  177. return nil
  178. }
  179. // getClientConfig creates an HTTP client configured for MTLS using the provided
  180. // client certificate and key, and optionally adds a custom CA from CAProvider or CABundle.
  181. func newHTTPClientWithMTLS(ctx context.Context, p *Provider, cl *ovhClient, clientMTLS *esv1.OvhClientMTLS) (*http.Client, error) {
  182. cert, err := buildX509Certificate(ctx, cl, p, clientMTLS)
  183. if err != nil {
  184. return nil, fmt.Errorf("failed to build x509 certificate: %w", err)
  185. }
  186. // Create an HTTP transport for mTLS, enforcing TLS 1.2+ and using the client certificate.
  187. transport := http.DefaultTransport.(*http.Transport).Clone()
  188. transport.TLSClientConfig = &tls.Config{
  189. MinVersion: tls.VersionTLS12,
  190. Certificates: []tls.Certificate{cert},
  191. }
  192. // Configure custom CA for the TLS client if provided via CAProvider or CABundle.
  193. if clientMTLS.CAProvider != nil || len(clientMTLS.CABundle) != 0 {
  194. caCertPool := x509.NewCertPool()
  195. ca, err := esutils.FetchCACertFromSource(ctx, esutils.CreateCertOpts{
  196. CABundle: clientMTLS.CABundle,
  197. CAProvider: clientMTLS.CAProvider,
  198. StoreKind: cl.ovhStoreKind,
  199. Namespace: cl.ovhStoreNameSpace,
  200. Client: cl.kube,
  201. })
  202. if err != nil {
  203. return nil, fmt.Errorf("failed to fetch CA cert: %w", err)
  204. }
  205. if !caCertPool.AppendCertsFromPEM(ca) {
  206. return nil, fmt.Errorf("failed to append CA")
  207. }
  208. transport.TLSClientConfig.RootCAs = caCertPool
  209. }
  210. // Build the HTTP client with configured transport and timeout.
  211. httpClient := http.Client{
  212. Timeout: cl.okmsTimeout,
  213. Transport: transport,
  214. }
  215. return &httpClient, nil
  216. }
  217. // buildX509Certificate retrieves client certificate and key to build X509 Certificate.
  218. func buildX509Certificate(ctx context.Context, cl *ovhClient, p *Provider, clientMTLS *esv1.OvhClientMTLS) (tls.Certificate, error) {
  219. clientKey, err := resolveSecretValue(ctx, cl, p, clientMTLS.ClientKey, emptyKeySecretRef)
  220. if err != nil {
  221. return tls.Certificate{}, fmt.Errorf("failed to resolve client key: %w", err)
  222. }
  223. clientCert, err := resolveSecretValue(ctx, cl, p, clientMTLS.ClientCertificate, emptyCertSecretRef)
  224. if err != nil {
  225. return tls.Certificate{}, fmt.Errorf("failed to resolve client certificate: %w", err)
  226. }
  227. cert, err := tls.X509KeyPair([]byte(clientCert), []byte(clientKey))
  228. if err != nil {
  229. return tls.Certificate{}, fmt.Errorf("failed to create x509 key pair: %w", err)
  230. }
  231. return cert, nil
  232. }
  233. // resolveSecret retrieves the value of the client certificate and key.
  234. func resolveSecretValue(ctx context.Context, cl *ovhClient, p *Provider, ref v1.SecretKeySelector, errMsg string) (string, error) {
  235. // ref refers to the Kubernetes secret object.
  236. // Retrieve the value of ref.
  237. secret, err := p.secretKeyResolver.Resolve(ctx, cl.kube,
  238. cl.ovhStoreKind, cl.ovhStoreNameSpace, ref)
  239. if err != nil {
  240. return "", fmt.Errorf("failed to resolve secret value: %w", err)
  241. }
  242. if secret == "" {
  243. return "", errors.New(errMsg)
  244. }
  245. return secret, nil
  246. }
  247. // ValidateStore statically validate the Secret Store specification.
  248. func (p *Provider) ValidateStore(store esv1.GenericStore) (admission.Warnings, error) {
  249. // Nil checks.
  250. if store == nil || reflect.ValueOf(store).IsNil() {
  251. return nil, errors.New("store is nil")
  252. }
  253. spec := store.GetSpec()
  254. if spec == nil {
  255. return nil, errors.New("store spec is nil")
  256. }
  257. provider := spec.Provider
  258. if provider == nil {
  259. return nil, errors.New("store provider is nil")
  260. }
  261. if provider.OVHcloud == nil {
  262. return nil, errors.New("ovh store provider is nil")
  263. }
  264. if provider.OVHcloud.OkmsTimeout != nil && *provider.OVHcloud.OkmsTimeout <= 0 {
  265. return nil, errors.New("ovh store okmsTimeout must be greater than 0")
  266. }
  267. if provider.OVHcloud.Server == "" {
  268. return nil, errors.New("ovh store server is required")
  269. }
  270. if _, err := url.Parse(provider.OVHcloud.Server); err != nil {
  271. return nil, fmt.Errorf("ovh store server must contain a valid url: %w", err)
  272. }
  273. if provider.OVHcloud.OkmsID == "" {
  274. return nil, errors.New("ovh store okmsID is required")
  275. }
  276. // Validate the provider's authentication method.
  277. return p.validateAuth(provider)
  278. }
  279. func (p *Provider) validateAuth(provider *esv1.SecretStoreProvider) (admission.Warnings, error) {
  280. auth := provider.OVHcloud.Auth
  281. if auth.ClientMTLS == nil && auth.ClientToken == nil {
  282. return nil, errors.New("missing authentication method")
  283. } else if auth.ClientMTLS != nil && auth.ClientToken != nil {
  284. return nil, errors.New("only one authentication method allowed (mtls | token)")
  285. }
  286. if auth.ClientToken != nil && auth.ClientToken.ClientTokenSecret == (v1.SecretKeySelector{}) {
  287. return nil, errors.New("missing token secret for token authentication")
  288. }
  289. if auth.ClientMTLS != nil && (auth.ClientMTLS.ClientCertificate == (v1.SecretKeySelector{}) || auth.ClientMTLS.ClientKey == (v1.SecretKeySelector{})) {
  290. return nil, errors.New("missing tls certificate or key for mtls authentication")
  291. }
  292. return nil, nil
  293. }
  294. // Capabilities return the provider supported capabilities (ReadOnly, WriteOnly, ReadWrite).
  295. func (p *Provider) Capabilities() esv1.SecretStoreCapabilities {
  296. return esv1.SecretStoreReadWrite
  297. }
  298. // NewProvider creates a new Provider instance.
  299. func NewProvider() esv1.Provider {
  300. return &Provider{
  301. secretKeyResolver: DefaultSecretKeyResolver{},
  302. }
  303. }
  304. // ProviderSpec returns the provider specification for registration.
  305. func ProviderSpec() *esv1.SecretStoreProvider {
  306. return &esv1.SecretStoreProvider{
  307. OVHcloud: &esv1.OvhProvider{},
  308. }
  309. }
  310. // MaintenanceStatus returns the maintenance status of the provider.
  311. func MaintenanceStatus() esv1.MaintenanceStatus {
  312. return esv1.MaintenanceStatusMaintained
  313. }