Skip to content

Commit 296f534

Browse files
committed
Introduce global decryption for SOPS age keys
Signed-off-by: Matheus Pimenta <[email protected]>
1 parent a342d00 commit 296f534

File tree

7 files changed

+184
-157
lines changed

7 files changed

+184
-157
lines changed

docs/spec/v1/kustomizations.md

Lines changed: 39 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -1430,24 +1430,29 @@ Kustomization object itself, it will fall back to these defaults.
14301430

14311431
See also the [workload identity](/flux/installation/configuration/workload-identity/) docs.
14321432

1433-
#### AWS KMS
1433+
#### Cloud Provider KMS Services
14341434

1435-
While making use of the [IAM OIDC provider](https://eksctl.io/usage/iamserviceaccounts/)
1436-
on your EKS cluster, you can create an IAM Role and Service Account with access
1437-
to AWS KMS (using at least `kms:Decrypt` and `kms:DescribeKey`). Once these are
1438-
created, you can annotate the kustomize-controller Service Account with the
1439-
Role ARN, granting the controller permission to decrypt the Secrets. Please refer
1440-
to the [SOPS guide](https://fluxcd.io/flux/guides/mozilla-sops/#aws) for detailed steps.
1435+
For cloud provider KMS services, please refer to the specific sections in the integration guides:
14411436

1442-
```sh
1443-
kubectl -n flux-system annotate serviceaccount kustomize-controller \
1444-
--field-manager=flux-client-side-apply \
1445-
eks.amazonaws.com/role-arn='arn:aws:iam::<ACCOUNT_ID>:role/<KMS-ROLE-NAME>'
1446-
```
1437+
Service-specific configuration:
1438+
1439+
- [AWS KMS](https://fluxcd.io/flux/integrations/aws/#for-amazon-key-management-service)
1440+
- [Azure Key Vault](https://fluxcd.io/flux/integrations/azure/#for-azure-key-vault)
1441+
- [GCP KMS](https://fluxcd.io/flux/integrations/gcp/#for-google-cloud-key-management-service)
1442+
1443+
Controller-level configuration:
1444+
1445+
- [AWS](https://fluxcd.io/flux/integrations/aws/#at-the-controller-level)
1446+
- [Azure](https://fluxcd.io/flux/integrations/azure/#at-the-controller-level)
1447+
- [GCP](https://fluxcd.io/flux/integrations/gcp/#at-the-controller-level)
14471448

1448-
Furthermore, you can also use the usual [environment variables used for specifying AWS
1449-
credentials](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html#envvars-list),
1450-
by patching the kustomize-controller Deployment:
1449+
These guides provide detailed instructions for setting up authentication,
1450+
permissions, and controller configuration for each cloud provider.
1451+
1452+
#### Hashicorp Vault
1453+
1454+
To configure a global default for Hashicorp Vault, patch the controller's
1455+
Deployment with a `VAULT_TOKEN` environment variable.
14511456

14521457
```yaml
14531458
---
@@ -1462,128 +1467,32 @@ spec:
14621467
containers:
14631468
- name: manager
14641469
env:
1465-
- name: AWS_ACCESS_KEY_ID
1466-
valueFrom:
1467-
secretKeyRef:
1468-
name: aws-creds
1469-
key: awsAccessKeyID
1470-
- name: AWS_SECRET_ACCESS_KEY
1471-
valueFrom:
1472-
secretKeyRef:
1473-
name: aws-creds
1474-
key: awsSecretAccessKey
1475-
- name: AWS_SESSION_TOKEN
1476-
valueFrom:
1477-
secretKeyRef:
1478-
name: aws-creds
1479-
key: awsSessionToken
1480-
```
1481-
1482-
In addition to this, the
1483-
[general SOPS documentation around KMS AWS applies](https://github.com/mozilla/sops#27kms-aws-profiles),
1484-
allowing you to specify e.g. a `SOPS_KMS_ARN` environment variable.
1485-
1486-
**Note:**: If you are mounting a secret containing the AWS credentials as a
1487-
file in the `kustomize-controller` Pod, you need to specify an environment
1488-
variable `$HOME`, since the AWS credentials file is expected to be present at
1489-
`~/.aws`. For example:
1490-
1491-
```yaml
1492-
env:
1493-
- name: HOME
1494-
value: /home/{$USER}
1470+
- name: VAULT_TOKEN
1471+
value: <token>
14951472
```
14961473

1497-
#### Azure Key Vault
1498-
1499-
##### Workload Identity
1474+
#### SOPS Age Keys
15001475

1501-
If you have Workload Identity set up on your AKS cluster, you can establish
1502-
a federated identity between the kustomize-controller ServiceAccount and an
1503-
identity that has "Decrypt" role on the Azure Key Vault. Once, this is done
1504-
you can label and annotate the kustomize-controller ServiceAccount and Pod
1505-
with the patch shown below:
1476+
To configure global decryption for SOPS Age keys, use the `--sops-age-secret`
1477+
controller flag to specify a Kubernetes Secret containing the Age private keys.
15061478

1507-
```yaml
1508-
apiVersion: kustomize.config.k8s.io/v1beta1
1509-
kind: Kustomization
1510-
resources:
1511-
- gotk-components.yaml
1512-
- gotk-sync.yaml
1513-
patches:
1514-
- patch: |-
1515-
apiVersion: v1
1516-
kind: ServiceAccount
1517-
metadata:
1518-
name: kustomize-controller
1519-
namespace: flux-system
1520-
annotations:
1521-
azure.workload.identity/client-id: <AZURE_CLIENT_ID>
1522-
labels:
1523-
azure.workload.identity/use: "true"
1524-
- patch: |-
1525-
apiVersion: apps/v1
1526-
kind: Deployment
1527-
metadata:
1528-
name: kustomize-controller
1529-
namespace: flux-system
1530-
labels:
1531-
azure.workload.identity/use: "true"
1532-
spec:
1533-
template:
1534-
metadata:
1535-
labels:
1536-
azure.workload.identity/use: "true"
1537-
```
1538-
1539-
##### Kubelet Identity
1540-
1541-
If the kubelet managed identity has `Decrypt` permissions on Azure Key Vault,
1542-
no additional configuration is required for the kustomize-controller to decrypt
1543-
data.
1544-
1545-
#### GCP KMS
1546-
1547-
While making use of Google Cloud Platform, the [`GOOGLE_APPLICATION_CREDENTIALS`
1548-
environment variable](https://cloud.google.com/docs/authentication/production)
1549-
is automatically taken into account.
1550-
[Granting permissions](https://cloud.google.com/kms/docs/reference/permissions-and-roles)
1551-
to the Service Account attached to this will therefore be sufficient to decrypt
1552-
data. When running outside GCP, it is possible to manually patch the
1553-
kustomize-controller Deployment with a valid set of (mounted) credentials.
1479+
First, create a Secret containing the Age private keys with the `.agekey` suffix:
15541480

15551481
```yaml
15561482
---
1557-
apiVersion: apps/v1
1558-
kind: Deployment
1483+
apiVersion: v1
1484+
kind: Secret
15591485
metadata:
1560-
name: kustomize-controller
1486+
name: sops-age-keys
15611487
namespace: flux-system
1562-
spec:
1563-
template:
1564-
spec:
1565-
containers:
1566-
- name: manager
1567-
env:
1568-
- name: GOOGLE_APPLICATION_CREDENTIALS
1569-
value: /var/gcp/credentials.json
1570-
volumeMounts:
1571-
- name: gcp-credentials
1572-
mountPath: /var/gcp/
1573-
readOnly: true
1574-
volumes:
1575-
- name: gcp-credentials
1576-
secret:
1577-
secretName: mysecret
1578-
items:
1579-
- key: credentials
1580-
path: credentials.json
1488+
stringData:
1489+
identity1.agekey: <identity1 key>
1490+
identity2.agekey: <identity1 key>
15811491
```
15821492

1583-
#### Hashicorp Vault
1493+
The Secret must be in the same namespace as the kustomize-controller Deployment.
15841494

1585-
To configure a global default for Hashicorp Vault, patch the controller's
1586-
Deployment with a `VAULT_TOKEN` environment variable.
1495+
Then, patch the kustomize-controller Deployment to add the `--sops-age-secret` flag:
15871496

15881497
```yaml
15891498
---
@@ -1597,11 +1506,13 @@ spec:
15971506
spec:
15981507
containers:
15991508
- name: manager
1600-
env:
1601-
- name: VAULT_TOKEN
1602-
value: <token>
1509+
args:
1510+
- --sops-age-secret=sops-age-keys
16031511
```
16041512

1513+
The field `.spec.decryption.secretRef` in the Kustomization will take precedence
1514+
in case both the controller flag and the Kustomization field are set.
1515+
16051516
### Kustomize secretGenerator
16061517

16071518
SOPS encrypted data can be stored as a base64 encoded Secret, which enables the

internal/controller/kustomization_controller.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ import (
7575
intcache "github.com/fluxcd/kustomize-controller/internal/cache"
7676
"github.com/fluxcd/kustomize-controller/internal/decryptor"
7777
"github.com/fluxcd/kustomize-controller/internal/inventory"
78+
intruntime "github.com/fluxcd/kustomize-controller/internal/runtime"
7879
)
7980

8081
// +kubebuilder:rbac:groups=kustomize.toolkit.fluxcd.io,resources=kustomizations,verbs=get;list;watch;create;update;patch;delete
@@ -104,6 +105,7 @@ type KustomizationReconciler struct {
104105
NoRemoteBases bool
105106
FailFast bool
106107
DefaultServiceAccount string
108+
SOPSAgeSecret string
107109
KubeConfigOpts runtimeClient.KubeConfigOptions
108110
ConcurrentSSA int
109111
DisallowedFieldManagers []string
@@ -642,7 +644,18 @@ func (r *KustomizationReconciler) generate(obj unstructured.Unstructured,
642644
func (r *KustomizationReconciler) build(ctx context.Context,
643645
obj *kustomizev1.Kustomization, u unstructured.Unstructured,
644646
workDir, dirPath string) ([]byte, error) {
645-
dec, cleanup, err := decryptor.NewTempDecryptor(workDir, r.Client, obj, r.TokenCache)
647+
648+
// Build decryptor.
649+
decryptorOpts := []decryptor.Option{
650+
decryptor.WithRoot(workDir),
651+
}
652+
if r.TokenCache != nil {
653+
decryptorOpts = append(decryptorOpts, decryptor.WithTokenCache(*r.TokenCache))
654+
}
655+
if name, ns := r.SOPSAgeSecret, intruntime.Namespace(); name != "" && ns != "" {
656+
decryptorOpts = append(decryptorOpts, decryptor.WithSOPSAgeSecret(name, ns))
657+
}
658+
dec, cleanup, err := decryptor.New(r.Client, obj, decryptorOpts...)
646659
if err != nil {
647660
return nil, err
648661
}

internal/decryptor/decryptor.go

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -162,32 +162,30 @@ type Decryptor struct {
162162
// decryptor.
163163
keyServices []keyservice.KeyServiceClient
164164
localServiceOnce sync.Once
165-
}
166165

167-
// NewDecryptor creates a new Decryptor for the given kustomization.
168-
// gnuPGHome can be empty, in which case the systems' keyring is used.
169-
func NewDecryptor(root string, client client.Client, kustomization *kustomizev1.Kustomization,
170-
maxFileSize int64, gnuPGHome string, tokenCache *cache.TokenCache) *Decryptor {
171-
return &Decryptor{
172-
root: root,
173-
client: client,
174-
kustomization: kustomization,
175-
maxFileSize: maxFileSize,
176-
gnuPGHome: pgp.GnuPGHome(gnuPGHome),
177-
tokenCache: tokenCache,
178-
}
166+
// sopsAgeSecret is the NamespacedName of the Secret containing
167+
// a fallback SOPS age decryption key.
168+
sopsAgeSecret *types.NamespacedName
179169
}
180170

181-
// NewTempDecryptor creates a new Decryptor, with a temporary GnuPG
171+
// New creates a new Decryptor, with a temporary GnuPG
182172
// home directory to Decryptor.ImportKeys() into.
183-
func NewTempDecryptor(root string, client client.Client, kustomization *kustomizev1.Kustomization,
184-
tokenCache *cache.TokenCache) (*Decryptor, func(), error) {
173+
func New(client client.Client, kustomization *kustomizev1.Kustomization, opts ...Option) (*Decryptor, func(), error) {
185174
gnuPGHome, err := pgp.NewGnuPGHome()
186175
if err != nil {
187176
return nil, nil, fmt.Errorf("cannot create decryptor: %w", err)
188177
}
189178
cleanup := func() { _ = os.RemoveAll(gnuPGHome.String()) }
190-
return NewDecryptor(root, client, kustomization, maxEncryptedFileSize, gnuPGHome.String(), tokenCache), cleanup, nil
179+
d := &Decryptor{
180+
client: client,
181+
kustomization: kustomization,
182+
maxFileSize: maxEncryptedFileSize,
183+
gnuPGHome: gnuPGHome,
184+
}
185+
for _, opt := range opts {
186+
opt(d)
187+
}
188+
return d, cleanup, nil
191189
}
192190

193191
// IsEncryptedSecret checks if the given object is a Kubernetes Secret encrypted
@@ -210,16 +208,43 @@ func IsEncryptedSecret(object *unstructured.Unstructured) bool {
210208
// For the import of PGP keys, the Decryptor must be configured with
211209
// an absolute GnuPG home directory path.
212210
func (d *Decryptor) ImportKeys(ctx context.Context) error {
213-
if d.kustomization.Spec.Decryption == nil || d.kustomization.Spec.Decryption.SecretRef == nil {
211+
if d.kustomization.Spec.Decryption == nil ||
212+
(d.kustomization.Spec.Decryption.SecretRef == nil && d.sopsAgeSecret == nil) {
214213
return nil
215214
}
216215

217216
provider := d.kustomization.Spec.Decryption.Provider
218217
switch provider {
219218
case DecryptionProviderSOPS:
219+
secretRef := d.kustomization.Spec.Decryption.SecretRef
220+
221+
// We handle the SOPS age global decryption separately, as most of the other
222+
// decryption providers already support global decryption in other ways, and
223+
// we don't want to introduce duplicate methods of achieving the same.
224+
// Furthermore, allowing e.g. cloud provider credentials to be fetched
225+
// from this global secret would prevent workload identity from working.
226+
if secretRef == nil && d.sopsAgeSecret != nil {
227+
var secret corev1.Secret
228+
if err := d.client.Get(ctx, *d.sopsAgeSecret, &secret); err != nil {
229+
if apierrors.IsNotFound(err) {
230+
return err
231+
}
232+
return fmt.Errorf("cannot get %s SOPS age decryption Secret '%s': %w", provider, *d.sopsAgeSecret, err)
233+
}
234+
for name, value := range secret.Data {
235+
if filepath.Ext(name) == DecryptionAgeExt {
236+
if err := d.ageIdentities.Import(string(value)); err != nil {
237+
return fmt.Errorf("failed to import '%s' data from %s SOPS age decryption Secret '%s': %w",
238+
name, provider, *d.sopsAgeSecret, err)
239+
}
240+
}
241+
}
242+
return nil
243+
}
244+
220245
secretName := types.NamespacedName{
221246
Namespace: d.kustomization.GetNamespace(),
222-
Name: d.kustomization.Spec.Decryption.SecretRef.Name,
247+
Name: secretRef.Name,
223248
}
224249

225250
var secret corev1.Secret

0 commit comments

Comments
 (0)