Merge pull request #1426 from fluxcd/rfc-0010

[RFC-0010] Introduce object-level workload identity for KMS decryption
This commit is contained in:
Matheus Pimenta 2025-05-07 17:58:58 +01:00 committed by GitHub
commit d775ed3a19
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 453 additions and 242 deletions

View File

@ -205,7 +205,18 @@ type Decryption struct {
// +required // +required
Provider string `json:"provider"` Provider string `json:"provider"`
// ServiceAccountName is the name of the service account used to
// authenticate with KMS services from cloud providers. If a
// static credential for a given cloud provider is defined
// inside the Secret referenced by SecretRef, that static
// credential takes priority.
// +optional
ServiceAccountName string `json:"serviceAccountName,omitempty"`
// The secret name containing the private OpenPGP keys used for decryption. // The secret name containing the private OpenPGP keys used for decryption.
// A static credential for a cloud provider defined inside the Secret
// takes priority to secret-less authentication with the ServiceAccountName
// field.
// +optional // +optional
SecretRef *meta.LocalObjectReference `json:"secretRef,omitempty"` SecretRef *meta.LocalObjectReference `json:"secretRef,omitempty"`
} }

View File

@ -86,8 +86,11 @@ spec:
- sops - sops
type: string type: string
secretRef: secretRef:
description: The secret name containing the private OpenPGP keys description: |-
used for decryption. The secret name containing the private OpenPGP keys used for decryption.
A static credential for a cloud provider defined inside the Secret
takes priority to secret-less authentication with the ServiceAccountName
field.
properties: properties:
name: name:
description: Name of the referent. description: Name of the referent.
@ -95,6 +98,14 @@ spec:
required: required:
- name - name
type: object type: object
serviceAccountName:
description: |-
ServiceAccountName is the name of the service account used to
authenticate with KMS services from cloud providers. If a
static credential for a given cloud provider is defined
inside the Secret referenced by SecretRef, that static
credential takes priority.
type: string
required: required:
- provider - provider
type: object type: object

View File

@ -21,6 +21,12 @@ rules:
verbs: verbs:
- create - create
- patch - patch
- apiGroups:
- ""
resources:
- serviceaccounts/token
verbs:
- create
- apiGroups: - apiGroups:
- kustomize.toolkit.fluxcd.io - kustomize.toolkit.fluxcd.io
resources: resources:

View File

@ -574,6 +574,22 @@ string
</tr> </tr>
<tr> <tr>
<td> <td>
<code>serviceAccountName</code><br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>ServiceAccountName is the name of the service account used to
authenticate with KMS services from cloud providers. If a
static credential for a given cloud provider is defined
inside the Secret referenced by SecretRef, that static
credential takes priority.</p>
</td>
</tr>
<tr>
<td>
<code>secretRef</code><br> <code>secretRef</code><br>
<em> <em>
<a href="https://godoc.org/github.com/fluxcd/pkg/apis/meta#LocalObjectReference"> <a href="https://godoc.org/github.com/fluxcd/pkg/apis/meta#LocalObjectReference">
@ -583,7 +599,10 @@ github.com/fluxcd/pkg/apis/meta.LocalObjectReference
</td> </td>
<td> <td>
<em>(Optional)</em> <em>(Optional)</em>
<p>The secret name containing the private OpenPGP keys used for decryption.</p> <p>The secret name containing the private OpenPGP keys used for decryption.
A static credential for a cloud provider defined inside the Secret
takes priority to secret-less authentication with the ServiceAccountName
field.</p>
</td> </td>
</tr> </tr>
</tbody> </tbody>

View File

@ -823,33 +823,46 @@ For more information, see [remote clusters/Cluster-API](#remote-clusterscluster-
### Decryption ### Decryption
`.spec.decryption` is an optional field to specify the configuration to decrypt Storing Secrets in Git repositories in plain text or base64 is unsafe,
Secrets, ConfigMaps and patches that are a part of the Kustomization. regardless of the visibility or access restrictions of the repository.
Since Secrets are either plain text or `base64` encoded, it's unsafe to store In order to store Secrets safely in Git repositorioes you can use an
them in plain text in a public or private Git repository. In order to store encryption provider and the optional field `.spec.decryption` to
them safely, you can use [Mozilla SOPS](https://github.com/mozilla/sops) and configure decryption for Secrets that are a part of the Kustomization.
encrypt your Kubernetes Secret data with [age](https://age-encryption.org/v1/)
and/or [OpenPGP](https://www.openpgp.org) keys, or with provider implementations
like Azure Key Vault, GCP KMS or Hashicorp Vault.
Also, you may want to encrypt some parts of resources as well. In order to do that, The only supported encryption provider is [SOPS](https://getsops.io/).
you may encrypt patches as well. With SOPS you can encrypt your secrets with [age](https://github.com/FiloSottile/age)
or [OpenPGP](https://www.openpgp.org) keys, or with keys from Key Management Services
(KMS), like AWS KMS, Azure Key Vault, GCP KMS or Hashicorp Vault.
**Note:** You must leave `metadata`, `kind` or `apiVersion` in plain text. **Note:** You must leave `metadata`, `kind` or `apiVersion` in plain text.
An easy way to do this is to limit encrypted keys by appending `--encrypted-regex '^(data|stringData)$'` An easy way to do this is limiting the encrypted keys with the flag
to your `sops --encrypt` command. `--encrypted-regex '^(data|stringData)$'` in your `sops encrypt` command.
It has two fields: The `.spec.decryption` field has the following subfields:
- `.provider`: The secrets decryption provider to be used. This field is required and - `.provider`: The secrets decryption provider to be used. This field is required and
the only supported value is `sops`. the only supported value is `sops`.
- `.secretRef.name`: The name of the secret that contains the keys to be used for - `.secretRef.name`: The name of the secret that contains the keys or cloud provider
decryption. This field can be omitted when using the static credentials for KMS services to be used for decryption.
[global decryption](#controller-global-decryption) option. - `.serviceAccountName`: The name of the service account used for
secret-less authentication with KMS services from cloud providers.
See the [workload identity](/flux/installation/configuration/workload-identity/) docs
for how to configure a cloud provider identity for this service account.
If a static credential for a given cloud provider is defined inside the secret
referenced by `.secretRef`, that static credential takes priority over secret-less
authentication for that provider. If no static credentials are defined for a given
cloud provider inside the secret, secret-less authentication is attempted for that
provider.
If `.serviceAccountName` is specified for secret-less authentication,
it takes priority over [controller global decryption](#controller-global-decryption)
for all cloud providers.
Example:
```yaml ```yaml
---
apiVersion: kustomize.toolkit.fluxcd.io/v1 apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization kind: Kustomization
metadata: metadata:
@ -863,13 +876,11 @@ spec:
name: repository-with-secrets name: repository-with-secrets
decryption: decryption:
provider: sops provider: sops
serviceAccountName: sops-identity
secretRef: secretRef:
name: sops-keys name: sops-keys-and-credentials
``` ```
**Note:** For information on Secrets decryption at a controller level, please
refer to [controller global decryption](#controller-global-decryption).
The Secret's `.data` section is expected to contain entries with decryption The Secret's `.data` section is expected to contain entries with decryption
keys (for age and OpenPGP), or credentials (for any of the supported provider keys (for age and OpenPGP), or credentials (for any of the supported provider
implementations). The controller identifies the type of the entry by the suffix implementations). The controller identifies the type of the entry by the suffix
@ -880,7 +891,7 @@ of the key (e.g. `.agekey`), or a fixed key (e.g. `sops.vault-token`).
apiVersion: v1 apiVersion: v1
kind: Secret kind: Secret
metadata: metadata:
name: sops-keys name: sops-keys-and-credentials
namespace: default namespace: default
data: data:
# Exemplary age private key # Exemplary age private key
@ -937,9 +948,9 @@ metadata:
namespace: default namespace: default
data: data:
sops.aws-kms: | sops.aws-kms: |
aws_access_key_id: some-access-key-id aws_access_key_id: some-access-key-id
aws_secret_access_key: some-aws-secret-access-key aws_secret_access_key: some-aws-secret-access-key
aws_session_token: some-aws-session-token # this field is optional aws_session_token: some-aws-session-token # this field is optional
``` ```
#### Azure Key Vault Secret entry #### Azure Key Vault Secret entry
@ -1408,6 +1419,8 @@ it is possible to specify global decryption settings on the
kustomize-controller Pod. When the controller fails to find credentials on the kustomize-controller Pod. When the controller fails to find credentials on the
Kustomization object itself, it will fall back to these defaults. Kustomization object itself, it will fall back to these defaults.
See also the [workload identity](/flux/installation/configuration/workload-identity/) docs.
#### AWS KMS #### AWS KMS
While making use of the [IAM OIDC provider](https://eksctl.io/usage/iamserviceaccounts/) While making use of the [IAM OIDC provider](https://eksctl.io/usage/iamserviceaccounts/)

11
go.mod
View File

@ -9,10 +9,12 @@ replace github.com/fluxcd/kustomize-controller/api => ./api
replace github.com/opencontainers/go-digest => github.com/opencontainers/go-digest v1.0.1-0.20220411205349-bde1400a84be replace github.com/opencontainers/go-digest => github.com/opencontainers/go-digest v1.0.1-0.20220411205349-bde1400a84be
require ( require (
cloud.google.com/go/kms v1.21.2
filippo.io/age v1.2.1 filippo.io/age v1.2.1
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0
github.com/aws/aws-sdk-go-v2 v1.36.3
github.com/aws/aws-sdk-go-v2/credentials v1.17.67 github.com/aws/aws-sdk-go-v2/credentials v1.17.67
github.com/cyphar/filepath-securejoin v0.4.1 github.com/cyphar/filepath-securejoin v0.4.1
github.com/dimchansky/utfbom v1.1.1 github.com/dimchansky/utfbom v1.1.1
@ -22,6 +24,8 @@ require (
github.com/fluxcd/pkg/apis/event v0.17.0 github.com/fluxcd/pkg/apis/event v0.17.0
github.com/fluxcd/pkg/apis/kustomize v1.10.0 github.com/fluxcd/pkg/apis/kustomize v1.10.0
github.com/fluxcd/pkg/apis/meta v1.11.0 github.com/fluxcd/pkg/apis/meta v1.11.0
github.com/fluxcd/pkg/auth v0.12.0
github.com/fluxcd/pkg/cache v0.9.0
github.com/fluxcd/pkg/http/fetch v0.16.0 github.com/fluxcd/pkg/http/fetch v0.16.0
github.com/fluxcd/pkg/kustomize v1.17.0 github.com/fluxcd/pkg/kustomize v1.17.0
github.com/fluxcd/pkg/runtime v0.59.0 github.com/fluxcd/pkg/runtime v0.59.0
@ -36,6 +40,7 @@ require (
github.com/ory/dockertest/v3 v3.12.0 github.com/ory/dockertest/v3 v3.12.0
github.com/spf13/pflag v1.0.6 github.com/spf13/pflag v1.0.6
golang.org/x/net v0.39.0 golang.org/x/net v0.39.0
golang.org/x/oauth2 v0.29.0
k8s.io/api v0.33.0 k8s.io/api v0.33.0
k8s.io/apimachinery v0.33.0 k8s.io/apimachinery v0.33.0
k8s.io/client-go v0.33.0 k8s.io/client-go v0.33.0
@ -61,7 +66,6 @@ require (
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.6.0 // indirect cloud.google.com/go/compute/metadata v0.6.0 // indirect
cloud.google.com/go/iam v1.5.2 // indirect cloud.google.com/go/iam v1.5.2 // indirect
cloud.google.com/go/kms v1.21.2 // indirect
cloud.google.com/go/longrunning v0.6.7 // indirect cloud.google.com/go/longrunning v0.6.7 // indirect
cloud.google.com/go/monitoring v1.24.2 // indirect cloud.google.com/go/monitoring v1.24.2 // indirect
cloud.google.com/go/storage v1.51.0 // indirect cloud.google.com/go/storage v1.51.0 // indirect
@ -80,7 +84,6 @@ require (
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/ProtonMail/go-crypto v1.2.0 // indirect github.com/ProtonMail/go-crypto v1.2.0 // indirect
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect
github.com/aws/aws-sdk-go-v2/config v1.29.14 // indirect github.com/aws/aws-sdk-go-v2/config v1.29.14 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect
@ -89,6 +92,7 @@ require (
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 // indirect
github.com/aws/aws-sdk-go-v2/service/ecr v1.43.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 // indirect github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect
@ -112,6 +116,7 @@ require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/docker/cli v28.1.1+incompatible // indirect github.com/docker/cli v28.1.1+incompatible // indirect
github.com/docker/docker v28.1.1+incompatible // indirect github.com/docker/docker v28.1.1+incompatible // indirect
github.com/docker/docker-credential-helpers v0.8.2 // indirect
github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect
github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect
@ -144,6 +149,7 @@ require (
github.com/google/cel-go v0.23.2 // indirect github.com/google/cel-go v0.23.2 // indirect
github.com/google/gnostic-models v0.6.9 // indirect github.com/google/gnostic-models v0.6.9 // indirect
github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect
github.com/google/go-containerregistry v0.20.3 // indirect
github.com/google/s2a-go v0.1.9 // indirect github.com/google/s2a-go v0.1.9 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
@ -222,7 +228,6 @@ require (
go.uber.org/zap v1.27.0 // indirect go.uber.org/zap v1.27.0 // indirect
golang.org/x/crypto v0.37.0 // indirect golang.org/x/crypto v0.37.0 // indirect
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
golang.org/x/oauth2 v0.29.0 // indirect
golang.org/x/sync v0.13.0 // indirect golang.org/x/sync v0.13.0 // indirect
golang.org/x/sys v0.32.0 // indirect golang.org/x/sys v0.32.0 // indirect
golang.org/x/term v0.31.0 // indirect golang.org/x/term v0.31.0 // indirect

12
go.sum
View File

@ -91,6 +91,8 @@ github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 h1:ZNTqv4nIdE/DiBfUUfXcLZ/Spcuz+RjeziUtNJackkM= github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 h1:ZNTqv4nIdE/DiBfUUfXcLZ/Spcuz+RjeziUtNJackkM=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34/go.mod h1:zf7Vcd1ViW7cPqYWEHLHJkS50X0JS2IKz9Cgaj6ugrs= github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34/go.mod h1:zf7Vcd1ViW7cPqYWEHLHJkS50X0JS2IKz9Cgaj6ugrs=
github.com/aws/aws-sdk-go-v2/service/ecr v1.43.3 h1:YyH8Hk73bYzdbvf6S8NF5z/fb/1stpiMnFSfL6jSfRA=
github.com/aws/aws-sdk-go-v2/service/ecr v1.43.3/go.mod h1:iQ1skgw1XRK+6Lgkb0I9ODatAP72WoTILh0zXQ5DtbU=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 h1:lguz0bmOoGzozP9XfRJR1QIayEYo+2vP/No3OfLF0pU= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 h1:lguz0bmOoGzozP9XfRJR1QIayEYo+2vP/No3OfLF0pU=
@ -129,6 +131,8 @@ github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f h1:C5bqEmzEPLsHm9Mv73l
github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4=
github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
@ -148,6 +152,8 @@ github.com/docker/cli v28.1.1+incompatible h1:eyUemzeI45DY7eDPuwUcmDyDj1pM98oD5M
github.com/docker/cli v28.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v28.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/docker v28.1.1+incompatible h1:49M11BFLsVO1gxY9UX9p/zwkE/rswggs8AdFmXQw51I= github.com/docker/docker v28.1.1+incompatible h1:49M11BFLsVO1gxY9UX9p/zwkE/rswggs8AdFmXQw51I=
github.com/docker/docker v28.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v28.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
@ -182,6 +188,10 @@ github.com/fluxcd/pkg/apis/kustomize v1.10.0 h1:47EeSzkQvlQZdH92vHMe2lK2iR8aOSEJ
github.com/fluxcd/pkg/apis/kustomize v1.10.0/go.mod h1:UsqMV4sqNa1Yg0pmTsdkHRJr7bafBOENIJoAN+3ezaQ= github.com/fluxcd/pkg/apis/kustomize v1.10.0/go.mod h1:UsqMV4sqNa1Yg0pmTsdkHRJr7bafBOENIJoAN+3ezaQ=
github.com/fluxcd/pkg/apis/meta v1.11.0 h1:h8q95k6ZEK1HCfsLkt8Np3i6ktb6ZzcWJ6hg++oc9w0= github.com/fluxcd/pkg/apis/meta v1.11.0 h1:h8q95k6ZEK1HCfsLkt8Np3i6ktb6ZzcWJ6hg++oc9w0=
github.com/fluxcd/pkg/apis/meta v1.11.0/go.mod h1:+son1Va60x2eiDcTwd7lcctbI6C+K3gM7R+ULmEq1SI= github.com/fluxcd/pkg/apis/meta v1.11.0/go.mod h1:+son1Va60x2eiDcTwd7lcctbI6C+K3gM7R+ULmEq1SI=
github.com/fluxcd/pkg/auth v0.12.0 h1:35o0ziYMLZVgJwNvJBGsv/wd903B2fMagcrnm1ptUjc=
github.com/fluxcd/pkg/auth v0.12.0/go.mod h1:gQD2VT5OhIR1E8ZTEsTaho3bDQZidr9P10smH/awcew=
github.com/fluxcd/pkg/cache v0.9.0 h1:EGKfOLMG3fOwWnH/4Axl5xd425mxoQbZzlZoLfd8PDk=
github.com/fluxcd/pkg/cache v0.9.0/go.mod h1:jMwabjWfsC5lW8hE7NM3wtGNwSJ38Javx6EKbEi7INU=
github.com/fluxcd/pkg/envsubst v1.4.0 h1:pYsb6wrmXOSfHXuXQHaaBBMt3LumhgCb8SMdBNAwV/U= github.com/fluxcd/pkg/envsubst v1.4.0 h1:pYsb6wrmXOSfHXuXQHaaBBMt3LumhgCb8SMdBNAwV/U=
github.com/fluxcd/pkg/envsubst v1.4.0/go.mod h1:zSDFO3Wawi+vI2NPxsMQp+EkIsz/85MNg/s1Wzmqt+s= github.com/fluxcd/pkg/envsubst v1.4.0/go.mod h1:zSDFO3Wawi+vI2NPxsMQp+EkIsz/85MNg/s1Wzmqt+s=
github.com/fluxcd/pkg/http/fetch v0.16.0 h1:XzhBTSK5HNdAPEnEGMJHwtoN2LfqQ9QFDsu3DGzl908= github.com/fluxcd/pkg/http/fetch v0.16.0 h1:XzhBTSK5HNdAPEnEGMJHwtoN2LfqQ9QFDsu3DGzl908=
@ -254,6 +264,8 @@ github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcb
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-containerregistry v0.20.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI=
github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lwNWK+cxgCuX1vd3PIBDNI=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=

29
internal/cache/operations.go vendored Normal file
View File

@ -0,0 +1,29 @@
/*
Copyright 2025 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package intcache
const (
OperationDecryptWithAWS = "decrypt_with_aws"
OperationDecryptWithAzure = "decrypt_with_azure"
OperationDecryptWithGCP = "decrypt_with_gcp"
)
var AllOperations = []string{
OperationDecryptWithAWS,
OperationDecryptWithAzure,
OperationDecryptWithGCP,
}

View File

@ -27,8 +27,6 @@ import (
"time" "time"
securejoin "github.com/cyphar/filepath-securejoin" securejoin "github.com/cyphar/filepath-securejoin"
"github.com/fluxcd/pkg/ssa/normalize"
ssautil "github.com/fluxcd/pkg/ssa/utils"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors" apierrors "k8s.io/apimachinery/pkg/api/errors"
apimeta "k8s.io/apimachinery/pkg/api/meta" apimeta "k8s.io/apimachinery/pkg/api/meta"
@ -54,6 +52,7 @@ import (
apiacl "github.com/fluxcd/pkg/apis/acl" apiacl "github.com/fluxcd/pkg/apis/acl"
eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1" eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1"
"github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/cache"
"github.com/fluxcd/pkg/http/fetch" "github.com/fluxcd/pkg/http/fetch"
generator "github.com/fluxcd/pkg/kustomize" generator "github.com/fluxcd/pkg/kustomize"
"github.com/fluxcd/pkg/runtime/acl" "github.com/fluxcd/pkg/runtime/acl"
@ -66,11 +65,14 @@ import (
"github.com/fluxcd/pkg/runtime/predicates" "github.com/fluxcd/pkg/runtime/predicates"
"github.com/fluxcd/pkg/runtime/statusreaders" "github.com/fluxcd/pkg/runtime/statusreaders"
"github.com/fluxcd/pkg/ssa" "github.com/fluxcd/pkg/ssa"
"github.com/fluxcd/pkg/ssa/normalize"
ssautil "github.com/fluxcd/pkg/ssa/utils"
"github.com/fluxcd/pkg/tar" "github.com/fluxcd/pkg/tar"
sourcev1 "github.com/fluxcd/source-controller/api/v1" sourcev1 "github.com/fluxcd/source-controller/api/v1"
sourcev1b2 "github.com/fluxcd/source-controller/api/v1beta2" sourcev1b2 "github.com/fluxcd/source-controller/api/v1beta2"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
intcache "github.com/fluxcd/kustomize-controller/internal/cache"
"github.com/fluxcd/kustomize-controller/internal/decryptor" "github.com/fluxcd/kustomize-controller/internal/decryptor"
"github.com/fluxcd/kustomize-controller/internal/inventory" "github.com/fluxcd/kustomize-controller/internal/inventory"
) )
@ -81,6 +83,7 @@ import (
// +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=buckets;ocirepositories;gitrepositories,verbs=get;list;watch // +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=buckets;ocirepositories;gitrepositories,verbs=get;list;watch
// +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=buckets/status;ocirepositories/status;gitrepositories/status,verbs=get // +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=buckets/status;ocirepositories/status;gitrepositories/status,verbs=get
// +kubebuilder:rbac:groups="",resources=configmaps;secrets;serviceaccounts,verbs=get;list;watch // +kubebuilder:rbac:groups="",resources=configmaps;secrets;serviceaccounts,verbs=get;list;watch
// +kubebuilder:rbac:groups="",resources=serviceaccounts/token,verbs=create
// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch // +kubebuilder:rbac:groups="",resources=events,verbs=create;patch
// KustomizationReconciler reconciles a Kustomization object // KustomizationReconciler reconciles a Kustomization object
@ -106,6 +109,7 @@ type KustomizationReconciler struct {
DisallowedFieldManagers []string DisallowedFieldManagers []string
StrictSubstitutions bool StrictSubstitutions bool
GroupChangeLog bool GroupChangeLog bool
TokenCache *cache.TokenCache
} }
// KustomizationReconcilerOptions contains options for the KustomizationReconciler. // KustomizationReconcilerOptions contains options for the KustomizationReconciler.
@ -626,17 +630,20 @@ func (r *KustomizationReconciler) generate(obj unstructured.Unstructured,
func (r *KustomizationReconciler) build(ctx context.Context, func (r *KustomizationReconciler) build(ctx context.Context,
obj *kustomizev1.Kustomization, u unstructured.Unstructured, obj *kustomizev1.Kustomization, u unstructured.Unstructured,
workDir, dirPath string) ([]byte, error) { workDir, dirPath string) ([]byte, error) {
dec, cleanup, err := decryptor.NewTempDecryptor(workDir, r.Client, obj) dec, cleanup, err := decryptor.NewTempDecryptor(workDir, r.Client, obj, r.TokenCache)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer cleanup() defer cleanup()
// Import decryption keys // Import keys and static credentials for decryption.
if err := dec.ImportKeys(ctx); err != nil { if err := dec.ImportKeys(ctx); err != nil {
return nil, err return nil, err
} }
// Set options for secret-less authentication with cloud providers for decryption.
dec.SetAuthOptions(ctx)
// Decrypt Kustomize EnvSources files before build // Decrypt Kustomize EnvSources files before build
if err = dec.DecryptSources(dirPath); err != nil { if err = dec.DecryptSources(dirPath); err != nil {
return nil, fmt.Errorf("error decrypting sources: %w", err) return nil, fmt.Errorf("error decrypting sources: %w", err)
@ -1090,6 +1097,12 @@ func (r *KustomizationReconciler) finalize(ctx context.Context,
// Remove our finalizer from the list and update it // Remove our finalizer from the list and update it
controllerutil.RemoveFinalizer(obj, kustomizev1.KustomizationFinalizer) controllerutil.RemoveFinalizer(obj, kustomizev1.KustomizationFinalizer)
// Cleanup caches.
for _, op := range intcache.AllOperations {
r.TokenCache.DeleteEventsForObject(kustomizev1.KustomizationKind, obj.GetName(), obj.GetNamespace(), op)
}
// Stop reconciliation as the object is being deleted // Stop reconciliation as the object is being deleted
return ctrl.Result{}, nil return ctrl.Result{}, nil
} }

View File

@ -29,17 +29,25 @@ import (
"sync" "sync"
"time" "time"
gcpkmsapi "cloud.google.com/go/kms/apiv1"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
awssdk "github.com/aws/aws-sdk-go-v2/aws"
securejoin "github.com/cyphar/filepath-securejoin" securejoin "github.com/cyphar/filepath-securejoin"
"github.com/fluxcd/pkg/auth"
"github.com/fluxcd/pkg/auth/aws"
"github.com/fluxcd/pkg/auth/azure"
"github.com/fluxcd/pkg/auth/gcp"
"github.com/fluxcd/pkg/cache"
"github.com/getsops/sops/v3" "github.com/getsops/sops/v3"
"github.com/getsops/sops/v3/aes" "github.com/getsops/sops/v3/aes"
"github.com/getsops/sops/v3/age" "github.com/getsops/sops/v3/age"
"github.com/getsops/sops/v3/azkv"
"github.com/getsops/sops/v3/cmd/sops/common" "github.com/getsops/sops/v3/cmd/sops/common"
"github.com/getsops/sops/v3/cmd/sops/formats" "github.com/getsops/sops/v3/cmd/sops/formats"
"github.com/getsops/sops/v3/config" "github.com/getsops/sops/v3/config"
"github.com/getsops/sops/v3/keyservice" "github.com/getsops/sops/v3/keyservice"
awskms "github.com/getsops/sops/v3/kms"
"github.com/getsops/sops/v3/pgp" "github.com/getsops/sops/v3/pgp"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors" apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@ -51,6 +59,7 @@ import (
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
intcache "github.com/fluxcd/kustomize-controller/internal/cache"
intawskms "github.com/fluxcd/kustomize-controller/internal/sops/awskms" intawskms "github.com/fluxcd/kustomize-controller/internal/sops/awskms"
intazkv "github.com/fluxcd/kustomize-controller/internal/sops/azkv" intazkv "github.com/fluxcd/kustomize-controller/internal/sops/azkv"
intkeyservice "github.com/fluxcd/kustomize-controller/internal/sops/keyservice" intkeyservice "github.com/fluxcd/kustomize-controller/internal/sops/keyservice"
@ -127,6 +136,8 @@ type Decryptor struct {
// injected into most resources, causing the integrity check to fail. // injected into most resources, causing the integrity check to fail.
// Mostly kept around for feature completeness and documentation purposes. // Mostly kept around for feature completeness and documentation purposes.
checkSopsMac bool checkSopsMac bool
// tokenCache is the cache for token credentials.
tokenCache *cache.TokenCache
// gnuPGHome is the absolute path of the GnuPG home directory used to // gnuPGHome is the absolute path of the GnuPG home directory used to
// decrypt PGP data. When empty, the systems' GnuPG keyring is used. // decrypt PGP data. When empty, the systems' GnuPG keyring is used.
@ -137,15 +148,15 @@ type Decryptor struct {
// vaultToken is the Hashicorp Vault token used to authenticate towards // vaultToken is the Hashicorp Vault token used to authenticate towards
// any Vault server. // any Vault server.
vaultToken string vaultToken string
// awsCredsProvider is the AWS credentials provider object used to authenticate // awsCredentialsProvider is the AWS credentials provider object used to authenticate
// towards any AWS KMS. // towards any AWS KMS.
awsCredsProvider *awskms.CredentialsProvider awsCredentialsProvider func(region string) awssdk.CredentialsProvider
// azureToken is the Azure credential token used to authenticate towards // azureTokenCredential is the Azure credential token used to authenticate towards
// any Azure Key Vault. // any Azure Key Vault.
azureToken *azkv.TokenCredential azureTokenCredential azcore.TokenCredential
// gcpCredsJSON is the JSON credential file of the service account used to // gcpTokenSource is the GCP token source used to authenticate towards
// authenticate towards any GCP KMS. // any GCP KMS.
gcpCredsJSON []byte gcpTokenSource oauth2.TokenSource
// keyServices are the SOPS keyservice.KeyServiceClient's available to the // keyServices are the SOPS keyservice.KeyServiceClient's available to the
// decryptor. // decryptor.
@ -155,25 +166,28 @@ type Decryptor struct {
// NewDecryptor creates a new Decryptor for the given kustomization. // NewDecryptor creates a new Decryptor for the given kustomization.
// gnuPGHome can be empty, in which case the systems' keyring is used. // gnuPGHome can be empty, in which case the systems' keyring is used.
func NewDecryptor(root string, client client.Client, kustomization *kustomizev1.Kustomization, maxFileSize int64, gnuPGHome string) *Decryptor { func NewDecryptor(root string, client client.Client, kustomization *kustomizev1.Kustomization,
maxFileSize int64, gnuPGHome string, tokenCache *cache.TokenCache) *Decryptor {
return &Decryptor{ return &Decryptor{
root: root, root: root,
client: client, client: client,
kustomization: kustomization, kustomization: kustomization,
maxFileSize: maxFileSize, maxFileSize: maxFileSize,
gnuPGHome: pgp.GnuPGHome(gnuPGHome), gnuPGHome: pgp.GnuPGHome(gnuPGHome),
tokenCache: tokenCache,
} }
} }
// NewTempDecryptor creates a new Decryptor, with a temporary GnuPG // NewTempDecryptor creates a new Decryptor, with a temporary GnuPG
// home directory to Decryptor.ImportKeys() into. // home directory to Decryptor.ImportKeys() into.
func NewTempDecryptor(root string, client client.Client, kustomization *kustomizev1.Kustomization) (*Decryptor, func(), error) { func NewTempDecryptor(root string, client client.Client, kustomization *kustomizev1.Kustomization,
tokenCache *cache.TokenCache) (*Decryptor, func(), error) {
gnuPGHome, err := pgp.NewGnuPGHome() gnuPGHome, err := pgp.NewGnuPGHome()
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("cannot create decryptor: %w", err) return nil, nil, fmt.Errorf("cannot create decryptor: %w", err)
} }
cleanup := func() { _ = os.RemoveAll(gnuPGHome.String()) } cleanup := func() { _ = os.RemoveAll(gnuPGHome.String()) }
return NewDecryptor(root, client, kustomization, maxEncryptedFileSize, gnuPGHome.String()), cleanup, nil return NewDecryptor(root, client, kustomization, maxEncryptedFileSize, gnuPGHome.String(), tokenCache), cleanup, nil
} }
// IsEncryptedSecret checks if the given object is a Kubernetes Secret encrypted // IsEncryptedSecret checks if the given object is a Kubernetes Secret encrypted
@ -228,7 +242,6 @@ func (d *Decryptor) ImportKeys(ctx context.Context) error {
return fmt.Errorf("failed to import '%s' data from %s decryption Secret '%s': %w", name, provider, secretName, err) return fmt.Errorf("failed to import '%s' data from %s decryption Secret '%s': %w", name, provider, secretName, err)
} }
case filepath.Ext(DecryptionVaultTokenFileName): case filepath.Ext(DecryptionVaultTokenFileName):
// Make sure we have the absolute name
if name == DecryptionVaultTokenFileName { if name == DecryptionVaultTokenFileName {
token := string(value) token := string(value)
token = strings.Trim(strings.TrimSpace(token), "\n") token = strings.Trim(strings.TrimSpace(token), "\n")
@ -240,10 +253,9 @@ func (d *Decryptor) ImportKeys(ctx context.Context) error {
if err != nil { if err != nil {
return fmt.Errorf("failed to import '%s' data from %s decryption Secret '%s': %w", name, provider, secretName, err) return fmt.Errorf("failed to import '%s' data from %s decryption Secret '%s': %w", name, provider, secretName, err)
} }
d.awsCredsProvider = awskms.NewCredentialsProvider(awsCreds) d.awsCredentialsProvider = func(string) awssdk.CredentialsProvider { return awsCreds }
} }
case filepath.Ext(DecryptionAzureAuthFile): case filepath.Ext(DecryptionAzureAuthFile):
// Make sure we have the absolute name
if name == DecryptionAzureAuthFile { if name == DecryptionAzureAuthFile {
conf := intazkv.AADConfig{} conf := intazkv.AADConfig{}
if err = intazkv.LoadAADConfigFromBytes(value, &conf); err != nil { if err = intazkv.LoadAADConfigFromBytes(value, &conf); err != nil {
@ -253,11 +265,16 @@ func (d *Decryptor) ImportKeys(ctx context.Context) error {
if err != nil { if err != nil {
return fmt.Errorf("failed to import '%s' data from %s decryption Secret '%s': %w", name, provider, secretName, err) return fmt.Errorf("failed to import '%s' data from %s decryption Secret '%s': %w", name, provider, secretName, err)
} }
d.azureToken = azkv.NewTokenCredential(azureToken) d.azureTokenCredential = azureToken
} }
case filepath.Ext(DecryptionGCPCredsFile): case filepath.Ext(DecryptionGCPCredsFile):
if name == DecryptionGCPCredsFile { if name == DecryptionGCPCredsFile {
d.gcpCredsJSON = bytes.Trim(value, "\n") creds, err := google.CredentialsFromJSON(ctx,
bytes.Trim(value, "\n"), gcpkmsapi.DefaultAuthScopes()...)
if err != nil {
return fmt.Errorf("failed to import '%s' data from %s decryption Secret '%s': %w", name, provider, secretName, err)
}
d.gcpTokenSource = creds.TokenSource
} }
} }
} }
@ -265,6 +282,63 @@ func (d *Decryptor) ImportKeys(ctx context.Context) error {
return nil return nil
} }
// SetAuthOptions sets the authentication options for secret-less authentication
// with cloud providers.
func (d *Decryptor) SetAuthOptions(ctx context.Context) {
if d.kustomization.Spec.Decryption == nil {
return
}
switch d.kustomization.Spec.Decryption.Provider {
case DecryptionProviderSOPS:
var opts []auth.Option
if d.kustomization.Spec.Decryption.ServiceAccountName != "" {
serviceAccount := types.NamespacedName{
Name: d.kustomization.Spec.Decryption.ServiceAccountName,
Namespace: d.kustomization.GetNamespace(),
}
opts = append(opts, auth.WithServiceAccount(serviceAccount, d.client))
}
involvedObject := cache.InvolvedObject{
Kind: kustomizev1.KustomizationKind,
Name: d.kustomization.GetName(),
Namespace: d.kustomization.GetNamespace(),
}
if d.awsCredentialsProvider == nil {
awsOpts := opts
if d.tokenCache != nil {
involvedObject.Operation = intcache.OperationDecryptWithAWS
awsOpts = append(awsOpts, auth.WithCache(*d.tokenCache, involvedObject))
}
d.awsCredentialsProvider = func(region string) awssdk.CredentialsProvider {
awsOpts := append(awsOpts, auth.WithSTSRegion(region))
return aws.NewCredentialsProvider(ctx, awsOpts...)
}
}
if d.azureTokenCredential == nil {
azureOpts := opts
if d.tokenCache != nil {
involvedObject.Operation = intcache.OperationDecryptWithAzure
azureOpts = append(azureOpts, auth.WithCache(*d.tokenCache, involvedObject))
}
d.azureTokenCredential = azure.NewTokenCredential(ctx, azureOpts...)
}
if d.gcpTokenSource == nil {
gcpOpts := opts
if d.tokenCache != nil {
involvedObject.Operation = intcache.OperationDecryptWithGCP
gcpOpts = append(gcpOpts, auth.WithCache(*d.tokenCache, involvedObject))
}
d.gcpTokenSource = gcp.NewTokenSource(ctx, gcpOpts...)
}
}
}
// SopsDecryptWithFormat attempts to load a SOPS encrypted file using the store // SopsDecryptWithFormat attempts to load a SOPS encrypted file using the store
// for the input format, gathers the data key for it from the key service, // for the input format, gathers the data key for it from the key service,
// and then decrypts the file data with the retrieved data key. // and then decrypts the file data with the retrieved data key.
@ -582,12 +656,10 @@ func (d *Decryptor) loadKeyServiceServer() {
intkeyservice.WithGnuPGHome(d.gnuPGHome), intkeyservice.WithGnuPGHome(d.gnuPGHome),
intkeyservice.WithVaultToken(d.vaultToken), intkeyservice.WithVaultToken(d.vaultToken),
intkeyservice.WithAgeIdentities(d.ageIdentities), intkeyservice.WithAgeIdentities(d.ageIdentities),
intkeyservice.WithGCPCredsJSON(d.gcpCredsJSON), intkeyservice.WithAWSCredentialsProvider{CredentialsProvider: d.awsCredentialsProvider},
intkeyservice.WithAzureTokenCredential{TokenCredential: d.azureTokenCredential},
intkeyservice.WithGCPTokenSource{TokenSource: d.gcpTokenSource},
} }
if d.azureToken != nil {
serverOpts = append(serverOpts, intkeyservice.WithAzureToken{Token: d.azureToken})
}
serverOpts = append(serverOpts, intkeyservice.WithAWSKeys{CredsProvider: d.awsCredsProvider})
server := intkeyservice.NewServer(serverOpts...) server := intkeyservice.NewServer(serverOpts...)
d.keyServices = append(make([]keyservice.KeyServiceClient, 0), keyservice.NewCustomLocalClient(server)) d.keyServices = append(make([]keyservice.KeyServiceClient, 0), keyservice.NewCustomLocalClient(server))
} }

View File

@ -210,7 +210,7 @@ aws_session_token: test-token`),
}, },
}, },
inspectFunc: func(g *GomegaWithT, decryptor *Decryptor) { inspectFunc: func(g *GomegaWithT, decryptor *Decryptor) {
g.Expect(decryptor.awsCredsProvider).ToNot(BeNil()) g.Expect(decryptor.awsCredentialsProvider).ToNot(BeNil())
}, },
}, },
{ {
@ -233,7 +233,7 @@ aws_session_token: test-token`),
}, },
}, },
inspectFunc: func(g *GomegaWithT, decryptor *Decryptor) { inspectFunc: func(g *GomegaWithT, decryptor *Decryptor) {
g.Expect(decryptor.gcpCredsJSON).ToNot(BeNil()) g.Expect(decryptor.gcpTokenSource).ToNot(BeNil())
}, },
}, },
{ {
@ -256,7 +256,7 @@ clientSecret: some-client-secret`),
}, },
}, },
inspectFunc: func(g *GomegaWithT, decryptor *Decryptor) { inspectFunc: func(g *GomegaWithT, decryptor *Decryptor) {
g.Expect(decryptor.azureToken).ToNot(BeNil()) g.Expect(decryptor.azureTokenCredential).ToNot(BeNil())
}, },
}, },
{ {
@ -278,7 +278,7 @@ clientSecret: some-client-secret`),
}, },
wantErr: true, wantErr: true,
inspectFunc: func(g *GomegaWithT, decryptor *Decryptor) { inspectFunc: func(g *GomegaWithT, decryptor *Decryptor) {
g.Expect(decryptor.azureToken).To(BeNil()) g.Expect(decryptor.azureTokenCredential).To(BeNil())
}, },
}, },
{ {
@ -300,7 +300,7 @@ clientSecret: some-client-secret`),
}, },
wantErr: true, wantErr: true,
inspectFunc: func(g *GomegaWithT, decryptor *Decryptor) { inspectFunc: func(g *GomegaWithT, decryptor *Decryptor) {
g.Expect(decryptor.azureToken).To(BeNil()) g.Expect(decryptor.azureTokenCredential).To(BeNil())
}, },
}, },
{ {
@ -376,7 +376,7 @@ clientSecret: some-client-secret`),
}, },
} }
d, cleanup, err := NewTempDecryptor("", cb.Build(), &kustomization) d, cleanup, err := NewTempDecryptor("", cb.Build(), &kustomization, nil)
g.Expect(err).ToNot(HaveOccurred()) g.Expect(err).ToNot(HaveOccurred())
t.Cleanup(cleanup) t.Cleanup(cleanup)
@ -393,6 +393,60 @@ clientSecret: some-client-secret`),
} }
} }
func TestDecryptor_SetAuthOptions(t *testing.T) {
t.Run("nil decryption settings", func(t *testing.T) {
g := NewWithT(t)
d := &Decryptor{
kustomization: &kustomizev1.Kustomization{},
}
d.SetAuthOptions(context.Background())
g.Expect(d.awsCredentialsProvider).To(BeNil())
g.Expect(d.azureTokenCredential).To(BeNil())
g.Expect(d.gcpTokenSource).To(BeNil())
})
t.Run("non-sops provider", func(t *testing.T) {
g := NewWithT(t)
d := &Decryptor{
kustomization: &kustomizev1.Kustomization{
Spec: kustomizev1.KustomizationSpec{
Decryption: &kustomizev1.Decryption{},
},
},
}
d.SetAuthOptions(context.Background())
g.Expect(d.awsCredentialsProvider).To(BeNil())
g.Expect(d.azureTokenCredential).To(BeNil())
g.Expect(d.gcpTokenSource).To(BeNil())
})
t.Run("sops provider", func(t *testing.T) {
g := NewWithT(t)
d := &Decryptor{
kustomization: &kustomizev1.Kustomization{
Spec: kustomizev1.KustomizationSpec{
Decryption: &kustomizev1.Decryption{
Provider: DecryptionProviderSOPS,
},
},
},
}
d.SetAuthOptions(context.Background())
g.Expect(d.awsCredentialsProvider).NotTo(BeNil())
g.Expect(d.azureTokenCredential).NotTo(BeNil())
g.Expect(d.gcpTokenSource).NotTo(BeNil())
})
}
func TestDecryptor_SopsDecryptWithFormat(t *testing.T) { func TestDecryptor_SopsDecryptWithFormat(t *testing.T) {
t.Run("decrypt INI to INI", func(t *testing.T) { t.Run("decrypt INI to INI", func(t *testing.T) {
g := NewWithT(t) g := NewWithT(t)
@ -551,7 +605,7 @@ func TestDecryptor_DecryptResource(t *testing.T) {
Provider: DecryptionProviderSOPS, Provider: DecryptionProviderSOPS,
} }
d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kus) d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kus, nil)
g.Expect(err).ToNot(HaveOccurred()) g.Expect(err).ToNot(HaveOccurred())
t.Cleanup(cleanup) t.Cleanup(cleanup)
@ -592,7 +646,7 @@ func TestDecryptor_DecryptResource(t *testing.T) {
Provider: DecryptionProviderSOPS, Provider: DecryptionProviderSOPS,
} }
d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kus) d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kus, nil)
g.Expect(err).ToNot(HaveOccurred()) g.Expect(err).ToNot(HaveOccurred())
t.Cleanup(cleanup) t.Cleanup(cleanup)
@ -627,7 +681,7 @@ func TestDecryptor_DecryptResource(t *testing.T) {
Provider: DecryptionProviderSOPS, Provider: DecryptionProviderSOPS,
} }
d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kus) d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kus, nil)
g.Expect(err).ToNot(HaveOccurred()) g.Expect(err).ToNot(HaveOccurred())
t.Cleanup(cleanup) t.Cleanup(cleanup)
@ -662,7 +716,7 @@ func TestDecryptor_DecryptResource(t *testing.T) {
Provider: DecryptionProviderSOPS, Provider: DecryptionProviderSOPS,
} }
d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kus) d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kus, nil)
g.Expect(err).ToNot(HaveOccurred()) g.Expect(err).ToNot(HaveOccurred())
t.Cleanup(cleanup) t.Cleanup(cleanup)
@ -711,7 +765,7 @@ func TestDecryptor_DecryptResource(t *testing.T) {
t.Run("nil resource", func(t *testing.T) { t.Run("nil resource", func(t *testing.T) {
g := NewWithT(t) g := NewWithT(t)
d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kustomization.DeepCopy()) d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kustomization.DeepCopy(), nil)
g.Expect(err).ToNot(HaveOccurred()) g.Expect(err).ToNot(HaveOccurred())
t.Cleanup(cleanup) t.Cleanup(cleanup)
@ -723,7 +777,7 @@ func TestDecryptor_DecryptResource(t *testing.T) {
t.Run("no decryption spec", func(t *testing.T) { t.Run("no decryption spec", func(t *testing.T) {
g := NewWithT(t) g := NewWithT(t)
d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kustomization.DeepCopy()) d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kustomization.DeepCopy(), nil)
g.Expect(err).ToNot(HaveOccurred()) g.Expect(err).ToNot(HaveOccurred())
t.Cleanup(cleanup) t.Cleanup(cleanup)
@ -739,7 +793,7 @@ func TestDecryptor_DecryptResource(t *testing.T) {
kus.Spec.Decryption = &kustomizev1.Decryption{ kus.Spec.Decryption = &kustomizev1.Decryption{
Provider: "not-supported", Provider: "not-supported",
} }
d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kus) d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kus, nil)
g.Expect(err).ToNot(HaveOccurred()) g.Expect(err).ToNot(HaveOccurred())
t.Cleanup(cleanup) t.Cleanup(cleanup)

View File

@ -0,0 +1,27 @@
/*
Copyright 2025 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package awskms
import (
"strings"
)
// GetRegionFromKMSARN extracts the region from a KMS ARN.
func GetRegionFromKMSARN(arn string) string {
arn = strings.TrimPrefix(arn, "arn:aws:kms:")
return strings.SplitN(arn, ":", 2)[0]
}

View File

@ -0,0 +1,34 @@
/*
Copyright 2025 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package awskms_test
import (
"testing"
. "github.com/onsi/gomega"
"github.com/fluxcd/kustomize-controller/internal/sops/awskms"
)
func TestGetRegionFromKMSARN(t *testing.T) {
g := NewWithT(t)
arn := "arn:aws:kms:us-east-1:211125720409:key/mrk-3179bb7e88bc42ffb1a27d5038ceea25"
region := awskms.GetRegionFromKMSARN(arn)
g.Expect(region).To(Equal("us-east-1"))
}

View File

@ -1,103 +0,0 @@
/*
Copyright 2023 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package azkv
import (
"errors"
"fmt"
"os"
"strings"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
)
// DefaultTokenCredential is a modification of azidentity.NewDefaultAzureCredential,
// specifically adapted to not shell out to the Azure CLI.
//
// It attempts to return an azcore.TokenCredential based on the following order:
//
// - azidentity.NewEnvironmentCredential if environment variables AZURE_CLIENT_ID,
// AZURE_CLIENT_ID is set with either one of the following: (AZURE_CLIENT_SECRET)
// or (AZURE_CLIENT_CERTIFICATE_PATH and AZURE_CLIENT_CERTIFICATE_PATH) or
// (AZURE_USERNAME, AZURE_PASSWORD)
// - azidentity.WorkloadIdentityCredential if environment variable configuration
// (AZURE_AUTHORITY_HOST, AZURE_CLIENT_ID, AZURE_FEDERATED_TOKEN_FILE, AZURE_TENANT_ID)
// is set by the Azure workload identity webhook.
// - azidentity.ManagedIdentityCredential if only AZURE_CLIENT_ID env variable is set.
func DefaultTokenCredential() (azcore.TokenCredential, error) {
var (
azureClientID = "AZURE_CLIENT_ID"
azureFederatedTokenFile = "AZURE_FEDERATED_TOKEN_FILE"
azureAuthorityHost = "AZURE_AUTHORITY_HOST"
azureTenantID = "AZURE_TENANT_ID"
)
var errorMessages []string
options := &azidentity.DefaultAzureCredentialOptions{}
envCred, err := azidentity.NewEnvironmentCredential(&azidentity.EnvironmentCredentialOptions{
ClientOptions: options.ClientOptions, DisableInstanceDiscovery: options.DisableInstanceDiscovery},
)
if err == nil {
return envCred, nil
} else {
errorMessages = append(errorMessages, "EnvironmentCredential: "+err.Error())
}
// workload identity requires values for AZURE_AUTHORITY_HOST, AZURE_CLIENT_ID, AZURE_FEDERATED_TOKEN_FILE, AZURE_TENANT_ID
haveWorkloadConfig := false
clientID, haveClientID := os.LookupEnv(azureClientID)
if haveClientID {
if file, ok := os.LookupEnv(azureFederatedTokenFile); ok {
if _, ok := os.LookupEnv(azureAuthorityHost); ok {
if tenantID, ok := os.LookupEnv(azureTenantID); ok {
haveWorkloadConfig = true
workloadCred, err := azidentity.NewWorkloadIdentityCredential(&azidentity.WorkloadIdentityCredentialOptions{
ClientID: clientID,
TenantID: tenantID,
TokenFilePath: file,
ClientOptions: options.ClientOptions,
DisableInstanceDiscovery: options.DisableInstanceDiscovery,
})
if err == nil {
return workloadCred, nil
} else {
errorMessages = append(errorMessages, "Workload Identity"+": "+err.Error())
}
}
}
}
}
if !haveWorkloadConfig {
err := errors.New("missing environment variables for workload identity. Check webhook and pod configuration")
errorMessages = append(errorMessages, fmt.Sprintf("Workload Identity: %s", err))
}
o := &azidentity.ManagedIdentityCredentialOptions{ClientOptions: options.ClientOptions}
if haveClientID {
o.ID = azidentity.ClientID(clientID)
}
miCred, err := azidentity.NewManagedIdentityCredential(o)
if err == nil {
return miCred, nil
} else {
errorMessages = append(errorMessages, "ManagedIdentity"+": "+err.Error())
}
return nil, errors.New(strings.Join(errorMessages, "\n"))
}

View File

@ -18,6 +18,8 @@ package keyservice
import ( import (
extage "filippo.io/age" extage "filippo.io/age"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
awssdk "github.com/aws/aws-sdk-go-v2/aws"
"github.com/getsops/sops/v3/age" "github.com/getsops/sops/v3/age"
"github.com/getsops/sops/v3/azkv" "github.com/getsops/sops/v3/azkv"
"github.com/getsops/sops/v3/gcpkms" "github.com/getsops/sops/v3/gcpkms"
@ -25,6 +27,9 @@ import (
"github.com/getsops/sops/v3/keyservice" "github.com/getsops/sops/v3/keyservice"
awskms "github.com/getsops/sops/v3/kms" awskms "github.com/getsops/sops/v3/kms"
"github.com/getsops/sops/v3/pgp" "github.com/getsops/sops/v3/pgp"
"golang.org/x/oauth2"
intawskms "github.com/fluxcd/kustomize-controller/internal/sops/awskms"
) )
// ServerOption is some configuration that modifies the Server. // ServerOption is some configuration that modifies the Server.
@ -57,33 +62,38 @@ func (o WithAgeIdentities) ApplyToServer(s *Server) {
s.ageIdentities = age.ParsedIdentities(o) s.ageIdentities = age.ParsedIdentities(o)
} }
// WithAWSKeys configures the AWS credentials on the Server // WithAWSCredentialsProvider configures the AWS credentials on the Server
type WithAWSKeys struct { type WithAWSCredentialsProvider struct {
CredsProvider *awskms.CredentialsProvider CredentialsProvider func(region string) awssdk.CredentialsProvider
} }
// ApplyToServer applies this configuration to the given Server. // ApplyToServer applies this configuration to the given Server.
func (o WithAWSKeys) ApplyToServer(s *Server) { func (o WithAWSCredentialsProvider) ApplyToServer(s *Server) {
s.awsCredsProvider = o.CredsProvider s.awsCredentialsProvider = func(arn string) *awskms.CredentialsProvider {
region := intawskms.GetRegionFromKMSARN(arn)
cp := o.CredentialsProvider(region)
return awskms.NewCredentialsProvider(cp)
}
} }
// WithGCPCredsJSON configures the GCP service account credentials JSON on the // WithGCPTokenSource configures the GCP token source on the Server.
// Server. type WithGCPTokenSource struct {
type WithGCPCredsJSON []byte TokenSource oauth2.TokenSource
// ApplyToServer applies this configuration to the given Server.
func (o WithGCPCredsJSON) ApplyToServer(s *Server) {
s.gcpCredsJSON = gcpkms.CredentialJSON(o)
}
// WithAzureToken configures the Azure credential token on the Server.
type WithAzureToken struct {
Token *azkv.TokenCredential
} }
// ApplyToServer applies this configuration to the given Server. // ApplyToServer applies this configuration to the given Server.
func (o WithAzureToken) ApplyToServer(s *Server) { func (o WithGCPTokenSource) ApplyToServer(s *Server) {
s.azureToken = o.Token s.gcpTokenSource = gcpkms.NewTokenSource(o.TokenSource)
}
// WithAzureTokenCredential configures the Azure credential token on the Server.
type WithAzureTokenCredential struct {
TokenCredential azcore.TokenCredential
}
// ApplyToServer applies this configuration to the given Server.
func (o WithAzureTokenCredential) ApplyToServer(s *Server) {
s.azureTokenCredential = azkv.NewTokenCredential(o.TokenCredential)
} }
// WithDefaultServer configures the fallback default server on the Server. // WithDefaultServer configures the fallback default server on the Server.

View File

@ -28,8 +28,6 @@ import (
"github.com/getsops/sops/v3/logging" "github.com/getsops/sops/v3/logging"
"github.com/getsops/sops/v3/pgp" "github.com/getsops/sops/v3/pgp"
"golang.org/x/net/context" "golang.org/x/net/context"
intazkv "github.com/fluxcd/kustomize-controller/internal/sops/azkv"
) )
// Server is a key service server that uses SOPS MasterKeys to fulfill // Server is a key service server that uses SOPS MasterKeys to fulfill
@ -54,20 +52,19 @@ type Server struct {
// When empty, the request will be handled by defaultServer. // When empty, the request will be handled by defaultServer.
vaultToken hcvault.Token vaultToken hcvault.Token
// azureToken is the credential token used for Encrypt and Decrypt // azureTokenCredential is the credential token used for Encrypt and Decrypt
// operations of Azure Key Vault requests. // operations of Azure Key Vault requests.
// When nil, the request will be handled by defaultServer. // When nil, the request will be handled by defaultServer.
azureToken *azkv.TokenCredential azureTokenCredential *azkv.TokenCredential
// awsCredsProvider is the Credentials object used for Encrypt and Decrypt // awsCredentialsProvider is the Credentials object used for Encrypt and Decrypt
// operations of AWS KMS requests. // operations of AWS KMS requests.
// When nil, the request will be handled by defaultServer. // When nil, the request will be handled by defaultServer.
awsCredsProvider *awskms.CredentialsProvider awsCredentialsProvider func(arn string) *awskms.CredentialsProvider
// gcpCredsJSON is the JSON credentials used for Decrypt and Encrypt // gcpTokenSource is the token source used for Encrypt and Decrypt
// operations of GCP KMS requests. When nil, a default client with // operations of GCP KMS requests.
// environmental runtime settings will be used. gcpTokenSource gcpkms.TokenSource
gcpCredsJSON gcpkms.CredentialJSON
// defaultServer is the fallback server, used to handle any request that // defaultServer is the fallback server, used to handle any request that
// is not eligible to be handled by this Server. // is not eligible to be handled by this Server.
@ -296,9 +293,7 @@ func (ks *Server) decryptWithHCVault(key *keyservice.VaultKey, ciphertext []byte
func (ks *Server) encryptWithAWSKMS(key *keyservice.KmsKey, plaintext []byte) ([]byte, error) { func (ks *Server) encryptWithAWSKMS(key *keyservice.KmsKey, plaintext []byte) ([]byte, error) {
awsKey := kmsKeyToMasterKey(key) awsKey := kmsKeyToMasterKey(key)
if ks.awsCredsProvider != nil { ks.awsCredentialsProvider(key.Arn).ApplyToMasterKey(&awsKey)
ks.awsCredsProvider.ApplyToMasterKey(&awsKey)
}
if err := awsKey.Encrypt(plaintext); err != nil { if err := awsKey.Encrypt(plaintext); err != nil {
return nil, err return nil, err
} }
@ -308,9 +303,7 @@ func (ks *Server) encryptWithAWSKMS(key *keyservice.KmsKey, plaintext []byte) ([
func (ks *Server) decryptWithAWSKMS(key *keyservice.KmsKey, cipherText []byte) ([]byte, error) { func (ks *Server) decryptWithAWSKMS(key *keyservice.KmsKey, cipherText []byte) ([]byte, error) {
awsKey := kmsKeyToMasterKey(key) awsKey := kmsKeyToMasterKey(key)
awsKey.EncryptedKey = string(cipherText) awsKey.EncryptedKey = string(cipherText)
if ks.awsCredsProvider != nil { ks.awsCredentialsProvider(key.Arn).ApplyToMasterKey(&awsKey)
ks.awsCredsProvider.ApplyToMasterKey(&awsKey)
}
return awsKey.Decrypt() return awsKey.Decrypt()
} }
@ -320,17 +313,7 @@ func (ks *Server) encryptWithAzureKeyVault(key *keyservice.AzureKeyVaultKey, pla
Name: key.Name, Name: key.Name,
Version: key.Version, Version: key.Version,
} }
if ks.azureToken == nil { ks.azureTokenCredential.ApplyToMasterKey(&azureKey)
// Ensure we use the default token credential if none is provided
// _without_ shelling out to `az`.
defaultToken, err := intazkv.DefaultTokenCredential()
if err != nil {
return nil, fmt.Errorf("failed to get Azure token credential to encrypt data: %w", err)
}
azkv.NewTokenCredential(defaultToken).ApplyToMasterKey(&azureKey)
} else {
ks.azureToken.ApplyToMasterKey(&azureKey)
}
if err := azureKey.Encrypt(plaintext); err != nil { if err := azureKey.Encrypt(plaintext); err != nil {
return nil, err return nil, err
} }
@ -343,17 +326,7 @@ func (ks *Server) decryptWithAzureKeyVault(key *keyservice.AzureKeyVaultKey, cip
Name: key.Name, Name: key.Name,
Version: key.Version, Version: key.Version,
} }
if ks.azureToken == nil { ks.azureTokenCredential.ApplyToMasterKey(&azureKey)
// Ensure we use the default token credential if none is provided
// _without_ shelling out to `az`.
defaultToken, err := intazkv.DefaultTokenCredential()
if err != nil {
return nil, fmt.Errorf("failed to get Azure token credential to decrypt data: %w", err)
}
azkv.NewTokenCredential(defaultToken).ApplyToMasterKey(&azureKey)
} else {
ks.azureToken.ApplyToMasterKey(&azureKey)
}
azureKey.EncryptedKey = string(ciphertext) azureKey.EncryptedKey = string(ciphertext)
plaintext, err := azureKey.Decrypt() plaintext, err := azureKey.Decrypt()
return plaintext, err return plaintext, err
@ -363,7 +336,7 @@ func (ks *Server) encryptWithGCPKMS(key *keyservice.GcpKmsKey, plaintext []byte)
gcpKey := gcpkms.MasterKey{ gcpKey := gcpkms.MasterKey{
ResourceID: key.ResourceId, ResourceID: key.ResourceId,
} }
ks.gcpCredsJSON.ApplyToMasterKey(&gcpKey) ks.gcpTokenSource.ApplyToMasterKey(&gcpKey)
if err := gcpKey.Encrypt(plaintext); err != nil { if err := gcpKey.Encrypt(plaintext); err != nil {
return nil, err return nil, err
} }
@ -374,7 +347,7 @@ func (ks *Server) decryptWithGCPKMS(key *keyservice.GcpKmsKey, ciphertext []byte
gcpKey := gcpkms.MasterKey{ gcpKey := gcpkms.MasterKey{
ResourceID: key.ResourceId, ResourceID: key.ResourceId,
} }
ks.gcpCredsJSON.ApplyToMasterKey(&gcpKey) ks.gcpTokenSource.ApplyToMasterKey(&gcpKey)
gcpKey.EncryptedKey = string(ciphertext) gcpKey.EncryptedKey = string(ciphertext)
plaintext, err := gcpKey.Decrypt() plaintext, err := gcpKey.Decrypt()
return plaintext, err return plaintext, err

View File

@ -21,7 +21,9 @@ import (
"os" "os"
"testing" "testing"
gcpkmsapi "cloud.google.com/go/kms/apiv1"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity" "github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/credentials"
"github.com/getsops/sops/v3/age" "github.com/getsops/sops/v3/age"
"github.com/getsops/sops/v3/azkv" "github.com/getsops/sops/v3/azkv"
@ -32,6 +34,7 @@ import (
"github.com/getsops/sops/v3/pgp" "github.com/getsops/sops/v3/pgp"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"golang.org/x/net/context" "golang.org/x/net/context"
"golang.org/x/oauth2/google"
) )
func TestServer_EncryptDecrypt_PGP(t *testing.T) { func TestServer_EncryptDecrypt_PGP(t *testing.T) {
@ -151,8 +154,8 @@ func TestServer_EncryptDecrypt_HCVault_Fallback(t *testing.T) {
func TestServer_EncryptDecrypt_awskms(t *testing.T) { func TestServer_EncryptDecrypt_awskms(t *testing.T) {
g := NewWithT(t) g := NewWithT(t)
s := NewServer(WithAWSKeys{ s := NewServer(WithAWSCredentialsProvider{
CredsProvider: awskms.NewCredentialsProvider(credentials.StaticCredentialsProvider{}), CredentialsProvider: func(region string) aws.CredentialsProvider { return credentials.StaticCredentialsProvider{} },
}) })
key := KeyFromMasterKey(awskms.NewMasterKeyFromArn("arn:aws:kms:us-west-2:107501996527:key/612d5f0p-p1l3-45e6-aca6-a5b005693a48", nil, "")) key := KeyFromMasterKey(awskms.NewMasterKeyFromArn("arn:aws:kms:us-west-2:107501996527:key/612d5f0p-p1l3-45e6-aca6-a5b005693a48", nil, ""))
@ -174,7 +177,7 @@ func TestServer_EncryptDecrypt_azkv(t *testing.T) {
identity, err := azidentity.NewDefaultAzureCredential(nil) identity, err := azidentity.NewDefaultAzureCredential(nil)
g.Expect(err).ToNot(HaveOccurred()) g.Expect(err).ToNot(HaveOccurred())
s := NewServer(WithAzureToken{Token: azkv.NewTokenCredential(identity)}) s := NewServer(WithAzureTokenCredential{TokenCredential: identity})
key := KeyFromMasterKey(azkv.NewMasterKey("", "", "")) key := KeyFromMasterKey(azkv.NewMasterKey("", "", ""))
_, err = s.Encrypt(context.TODO(), &keyservice.EncryptRequest{ _, err = s.Encrypt(context.TODO(), &keyservice.EncryptRequest{
@ -194,24 +197,24 @@ func TestServer_EncryptDecrypt_azkv(t *testing.T) {
func TestServer_EncryptDecrypt_gcpkms(t *testing.T) { func TestServer_EncryptDecrypt_gcpkms(t *testing.T) {
g := NewWithT(t) g := NewWithT(t)
creds := `{ "client_id": "<client-id>.apps.googleusercontent.com", creds, err := google.CredentialsFromJSON(context.Background(),
"client_secret": "<secret>", []byte(`{"type":"service_account"}`), gcpkmsapi.DefaultAuthScopes()...)
"type": "authorized_user"}` g.Expect(err).ToNot(HaveOccurred())
s := NewServer(WithGCPCredsJSON([]byte(creds))) s := NewServer(WithGCPTokenSource{TokenSource: creds.TokenSource})
resourceID := "projects/test-flux/locations/global/keyRings/test-flux/cryptoKeys/sops" resourceID := "projects/test-flux/locations/global/keyRings/test-flux/cryptoKeys/sops"
key := KeyFromMasterKey(gcpkms.NewMasterKeyFromResourceID(resourceID)) key := KeyFromMasterKey(gcpkms.NewMasterKeyFromResourceID(resourceID))
_, err := s.Encrypt(context.TODO(), &keyservice.EncryptRequest{ _, err = s.Encrypt(context.TODO(), &keyservice.EncryptRequest{
Key: &key, Key: &key,
}) })
g.Expect(err).To(HaveOccurred()) g.Expect(err).To(HaveOccurred())
g.Expect(err.Error()).To(ContainSubstring("cannot create GCP KMS service")) g.Expect(err.Error()).To(ContainSubstring("failed to encrypt sops data key with GCP KMS key"))
_, err = s.Decrypt(context.TODO(), &keyservice.DecryptRequest{ _, err = s.Decrypt(context.TODO(), &keyservice.DecryptRequest{
Key: &key, Key: &key,
}) })
g.Expect(err).To(HaveOccurred()) g.Expect(err).To(HaveOccurred())
g.Expect(err.Error()).To(ContainSubstring("cannot create GCP KMS service")) g.Expect(err.Error()).To(ContainSubstring("failed to decrypt sops data key with GCP KMS key"))
} }

22
main.go
View File

@ -32,10 +32,12 @@ import (
ctrlcache "sigs.k8s.io/controller-runtime/pkg/cache" ctrlcache "sigs.k8s.io/controller-runtime/pkg/cache"
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
ctrlcfg "sigs.k8s.io/controller-runtime/pkg/config" ctrlcfg "sigs.k8s.io/controller-runtime/pkg/config"
ctrlmetrics "sigs.k8s.io/controller-runtime/pkg/metrics"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/clusterreader" "github.com/fluxcd/cli-utils/pkg/kstatus/polling/clusterreader"
"github.com/fluxcd/cli-utils/pkg/kstatus/polling/engine" "github.com/fluxcd/cli-utils/pkg/kstatus/polling/engine"
pkgcache "github.com/fluxcd/pkg/cache"
"github.com/fluxcd/pkg/runtime/acl" "github.com/fluxcd/pkg/runtime/acl"
runtimeClient "github.com/fluxcd/pkg/runtime/client" runtimeClient "github.com/fluxcd/pkg/runtime/client"
runtimeCtrl "github.com/fluxcd/pkg/runtime/controller" runtimeCtrl "github.com/fluxcd/pkg/runtime/controller"
@ -73,6 +75,10 @@ func init() {
} }
func main() { func main() {
const (
tokenCacheDefaultMaxSize = 100
)
var ( var (
metricsAddr string metricsAddr string
eventsAddr string eventsAddr string
@ -93,6 +99,7 @@ func main() {
defaultServiceAccount string defaultServiceAccount string
featureGates feathelper.FeatureGates featureGates feathelper.FeatureGates
disallowedFieldManagers []string disallowedFieldManagers []string
tokenCacheOptions pkgcache.TokenFlags
) )
flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.") flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.")
@ -116,6 +123,7 @@ func main() {
featureGates.BindFlags(flag.CommandLine) featureGates.BindFlags(flag.CommandLine)
watchOptions.BindFlags(flag.CommandLine) watchOptions.BindFlags(flag.CommandLine)
intervalJitterOptions.BindFlags(flag.CommandLine) intervalJitterOptions.BindFlags(flag.CommandLine)
tokenCacheOptions.BindFlags(flag.CommandLine, tokenCacheDefaultMaxSize)
flag.Parse() flag.Parse()
@ -240,6 +248,19 @@ func main() {
os.Exit(1) os.Exit(1)
} }
var tokenCache *pkgcache.TokenCache
if tokenCacheOptions.MaxSize > 0 {
var err error
tokenCache, err = pkgcache.NewTokenCache(tokenCacheOptions.MaxSize,
pkgcache.WithMaxDuration(tokenCacheOptions.MaxDuration),
pkgcache.WithMetricsRegisterer(ctrlmetrics.Registry),
pkgcache.WithMetricsPrefix("gotk_token_"))
if err != nil {
setupLog.Error(err, "unable to create token cache")
os.Exit(1)
}
}
if err = (&controller.KustomizationReconciler{ if err = (&controller.KustomizationReconciler{
ControllerName: controllerName, ControllerName: controllerName,
DefaultServiceAccount: defaultServiceAccount, DefaultServiceAccount: defaultServiceAccount,
@ -257,6 +278,7 @@ func main() {
DisallowedFieldManagers: disallowedFieldManagers, DisallowedFieldManagers: disallowedFieldManagers,
StrictSubstitutions: strictSubstitutions, StrictSubstitutions: strictSubstitutions,
GroupChangeLog: groupChangeLog, GroupChangeLog: groupChangeLog,
TokenCache: tokenCache,
}).SetupWithManager(ctx, mgr, controller.KustomizationReconcilerOptions{ }).SetupWithManager(ctx, mgr, controller.KustomizationReconcilerOptions{
DependencyRequeueInterval: requeueDependency, DependencyRequeueInterval: requeueDependency,
HTTPRetry: httpRetry, HTTPRetry: httpRetry,