Skip to content

Commit 792c6bf

Browse files
committed
Introduce generic Kubernetes workload identity for SOPS Vault/OpenBao
Signed-off-by: Matheus Pimenta <matheuscscp@gmail.com>
1 parent cdf2ed1 commit 792c6bf

12 files changed

Lines changed: 558 additions & 23 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ kustomize-controller is a core component of the [Flux GitOps Toolkit](https://fl
4444

4545
1. Fetches a source artifact produced by source-controller from a `GitRepository`, `Bucket`, `OCIRepository`, or (feature-gated) generic `ExternalArtifact`.
4646
2. Runs a Kustomize build against the referenced path, with optional `postBuild` variable substitution (envsubst).
47-
3. Decrypts SOPS-encrypted Kubernetes secrets inline using keys from PGP, age, Hashicorp Vault, AWS KMS, GCP KMS, or Azure Key Vault.
47+
3. Decrypts SOPS-encrypted Kubernetes secrets inline using keys from PGP, age, OpenBao/Vault, AWS KMS, GCP KMS, or Azure Key Vault.
4848
4. Applies the resulting manifests with server-side apply using a stable field manager, detects drift, prunes objects removed from source via a resource inventory, and waits on health checks using `cli-utils` kstatus.
4949
5. Optionally impersonates a `ServiceAccount` and/or targets a remote cluster via a `kubeconfig` secret.
5050

api/v1/metrics.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ const (
3232
// MetricDecryptWithGCP is the metric name for counting
3333
// decryption attempts using GCP KMS.
3434
MetricDecryptWithGCP = "decrypt_with_gcp"
35+
36+
// MetricDecryptWithVault is the metric name for counting
37+
// decryption attempts using OpenBao/Vault transit
38+
// authenticated with the Kubernetes auth method.
39+
MetricDecryptWithVault = "decrypt_with_vault"
3540
)
3641

3742
// AllMetrics is the list of all supported cache metrics.
@@ -40,4 +45,5 @@ var AllMetrics = []string{
4045
MetricDecryptWithAWS,
4146
MetricDecryptWithAzure,
4247
MetricDecryptWithGCP,
48+
MetricDecryptWithVault,
4349
}

api/v1/vault_types.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
Copyright 2026 The Flux authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package v1
18+
19+
// VaultConfig is the controller-level configuration that enables and scopes
20+
// authentication to OpenBao/Vault instances for SOPS decryption. The
21+
// controller presents a Kubernetes ServiceAccount token to a JWT-backed auth
22+
// method (e.g. the Kubernetes or JWT auth method). The operator provides this
23+
// config through a ConfigMap, listing the instances the controller may
24+
// authenticate to along with each instance's login path. It only governs this
25+
// ServiceAccount-token authentication: the existing static token decryption
26+
// paths (the sops.vault-token Secret entry and the VAULT_TOKEN environment
27+
// variable) are unaffected and continue to work for any address.
28+
type VaultConfig struct {
29+
// Instances is the list of known OpenBao/Vault instances.
30+
Instances []VaultInstance `json:"instances"`
31+
}
32+
33+
// VaultInstance describes a single OpenBao/Vault instance and how the
34+
// controller should authenticate to it.
35+
type VaultInstance struct {
36+
// Address is the address of the OpenBao/Vault instance, matching
37+
// the address stored in the SOPS metadata of the encrypted data key.
38+
Address string `json:"address"`
39+
40+
// LoginPath is the API path of the login endpoint to authenticate to this
41+
// instance with, e.g. "auth/kubernetes/login". It is used verbatim, so it
42+
// supports any JWT-backed auth method (e.g. the Kubernetes or JWT auth
43+
// method) and namespace-prefixed paths (e.g. "ns1/ns2/auth/kubernetes/login").
44+
LoginPath string `json:"loginPath"`
45+
}

api/v1/zz_generated.deepcopy.go

Lines changed: 35 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/api/v1/kustomize.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1368,6 +1368,92 @@ as if the resource was present but empty, without any variables defined.</p>
13681368
</table>
13691369
</div>
13701370
</div>
1371+
<h3 id="kustomize.toolkit.fluxcd.io/v1.VaultConfig">VaultConfig
1372+
</h3>
1373+
<p>VaultConfig is the controller-level configuration that enables and scopes
1374+
authentication to OpenBao/Vault instances for SOPS decryption. The
1375+
controller presents a Kubernetes ServiceAccount token to a JWT-backed auth
1376+
method (e.g. the Kubernetes or JWT auth method). The operator provides this
1377+
config through a ConfigMap, listing the instances the controller may
1378+
authenticate to along with each instance&rsquo;s login path. It only governs this
1379+
ServiceAccount-token authentication: the existing static token decryption
1380+
paths (the sops.vault-token Secret entry and the VAULT_TOKEN environment
1381+
variable) are unaffected and continue to work for any address.</p>
1382+
<div class="md-typeset__scrollwrap">
1383+
<div class="md-typeset__table">
1384+
<table>
1385+
<thead>
1386+
<tr>
1387+
<th>Field</th>
1388+
<th>Description</th>
1389+
</tr>
1390+
</thead>
1391+
<tbody>
1392+
<tr>
1393+
<td>
1394+
<code>instances</code><br>
1395+
<em>
1396+
<a href="#kustomize.toolkit.fluxcd.io/v1.VaultInstance">
1397+
[]VaultInstance
1398+
</a>
1399+
</em>
1400+
</td>
1401+
<td>
1402+
<p>Instances is the list of known OpenBao/Vault instances.</p>
1403+
</td>
1404+
</tr>
1405+
</tbody>
1406+
</table>
1407+
</div>
1408+
</div>
1409+
<h3 id="kustomize.toolkit.fluxcd.io/v1.VaultInstance">VaultInstance
1410+
</h3>
1411+
<p>
1412+
(<em>Appears on:</em>
1413+
<a href="#kustomize.toolkit.fluxcd.io/v1.VaultConfig">VaultConfig</a>)
1414+
</p>
1415+
<p>VaultInstance describes a single OpenBao/Vault instance and how the
1416+
controller should authenticate to it.</p>
1417+
<div class="md-typeset__scrollwrap">
1418+
<div class="md-typeset__table">
1419+
<table>
1420+
<thead>
1421+
<tr>
1422+
<th>Field</th>
1423+
<th>Description</th>
1424+
</tr>
1425+
</thead>
1426+
<tbody>
1427+
<tr>
1428+
<td>
1429+
<code>address</code><br>
1430+
<em>
1431+
string
1432+
</em>
1433+
</td>
1434+
<td>
1435+
<p>Address is the address of the OpenBao/Vault instance, matching
1436+
the address stored in the SOPS metadata of the encrypted data key.</p>
1437+
</td>
1438+
</tr>
1439+
<tr>
1440+
<td>
1441+
<code>loginPath</code><br>
1442+
<em>
1443+
string
1444+
</em>
1445+
</td>
1446+
<td>
1447+
<p>LoginPath is the API path of the login endpoint to authenticate to this
1448+
instance with, e.g. &ldquo;auth/kubernetes/login&rdquo;. It is used verbatim, so it
1449+
supports any JWT-backed auth method (e.g. the Kubernetes or JWT auth
1450+
method) and namespace-prefixed paths (e.g. &ldquo;ns1/ns2/auth/kubernetes/login&rdquo;).</p>
1451+
</td>
1452+
</tr>
1453+
</tbody>
1454+
</table>
1455+
</div>
1456+
</div>
13711457
<div class="admonition note">
13721458
<p class="last">This page was automatically generated with <code>gen-crd-api-reference-docs</code></p>
13731459
</div>

docs/spec/v1/kustomizations.md

Lines changed: 120 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1048,7 +1048,7 @@ configure decryption for Secrets that are a part of the Kustomization.
10481048
The only supported encryption provider is [SOPS](https://getsops.io/).
10491049
With SOPS you can encrypt your secrets with [age](https://github.com/FiloSottile/age)
10501050
or [OpenPGP](https://www.openpgp.org) keys, or with keys from Key Management Services
1051-
(KMS), like AWS KMS, Azure Key Vault, GCP KMS or Hashicorp Vault.
1051+
(KMS), like AWS KMS, Azure Key Vault, GCP KMS or OpenBao/Vault.
10521052

10531053
**Note:** You must leave `metadata`, `kind` or `apiVersion` in plain text.
10541054
An easy way to do this is limiting the encrypted keys with the flag
@@ -1061,7 +1061,12 @@ The `.spec.decryption` field has the following subfields:
10611061
- `.secretRef.name`: The name of the secret that contains the keys or cloud provider
10621062
static credentials for KMS services to be used for decryption.
10631063
- `.serviceAccountName`: The name of the service account used for
1064-
secret-less authentication with KMS services from cloud providers.
1064+
secret-less authentication with KMS services from cloud providers, and
1065+
with OpenBao/Vault via the
1066+
[Kubernetes auth method](#openbaovault-kubernetes-auth) (object-level workload
1067+
identity). This requires the `ObjectLevelWorkloadIdentity` feature gate to be
1068+
enabled. When unset, the controller falls back to its own ServiceAccount
1069+
([controller-level workload identity](#controller-global-decryption)).
10651070

10661071
To make a Kustomization react immediately to changes in the referenced Secret
10671072
see [this](#reacting-immediately-to-configuration-dependencies) section.
@@ -1115,7 +1120,7 @@ metadata:
11151120
data:
11161121
# Exemplary age private key
11171122
identity.agekey: <BASE64>
1118-
# Exemplary Hashicorp Vault token
1123+
# Exemplary OpenBao/Vault token
11191124
sops.vault-token: <BASE64>
11201125
```
11211126

@@ -1293,9 +1298,9 @@ stringData:
12931298
}
12941299
```
12951300

1296-
#### Hashicorp Vault Secret entry
1301+
#### OpenBao/Vault Secret entry
12971302

1298-
To specify credentials for Hashicorp Vault in a Kubernetes Secret, append a
1303+
To specify credentials for OpenBao/Vault in a Kubernetes Secret, append a
12991304
`.data` entry with a fixed `sops.vault-token` key and the token as value.
13001305

13011306
```yaml
@@ -1306,10 +1311,95 @@ metadata:
13061311
name: sops-keys
13071312
namespace: default
13081313
data:
1309-
# Exemplary Hashicorp Vault Secret token
1314+
# Exemplary OpenBao/Vault Secret token
13101315
sops.vault-token: <BASE64>
13111316
```
13121317

1318+
#### OpenBao/Vault Kubernetes auth
1319+
1320+
Instead of a static `sops.vault-token`, the controller can authenticate to
1321+
OpenBao/Vault by exchanging a Kubernetes ServiceAccount token for a short-lived
1322+
Vault token via a JWT-backed auth method (e.g. the
1323+
[Kubernetes auth method](https://openbao.org/docs/auth/kubernetes/) or the
1324+
[JWT auth method](https://openbao.org/docs/auth/jwt/)). This is enabled with the
1325+
`--sops-vault-configmap` flag, which names a ConfigMap in the controller's
1326+
namespace that maps each Vault address to the login path to authenticate at. The
1327+
ConfigMap also acts as an **allowlist**: only addresses listed in it can be
1328+
authenticated to this way. This does not affect the static token paths — the
1329+
`sops.vault-token` Secret entry and the `VAULT_TOKEN` environment variable
1330+
continue to work for any address. When the flag is empty, this authentication is
1331+
disabled and decryption falls back to those static token paths.
1332+
1333+
The mapping is stored under the `config.yaml` key as a list of instances, each
1334+
with an `address` and the `loginPath` to authenticate at. The login path is used
1335+
verbatim, so it supports any JWT-backed auth method and namespace-prefixed paths
1336+
(e.g. `ns1/ns2/auth/kubernetes/login`). The ConfigMap must be in the same
1337+
namespace as the kustomize-controller Deployment.
1338+
1339+
```yaml
1340+
---
1341+
apiVersion: v1
1342+
kind: ConfigMap
1343+
metadata:
1344+
name: sops-vault-config
1345+
namespace: flux-system
1346+
data:
1347+
config.yaml: |
1348+
instances:
1349+
- address: https://vault-a.example.com:8200
1350+
loginPath: auth/kubernetes/login
1351+
- address: https://vault-b.example.com:8200
1352+
loginPath: ns1/ns2/auth/kubernetes/login
1353+
```
1354+
1355+
Then, patch the kustomize-controller Deployment to add the flag:
1356+
1357+
```yaml
1358+
---
1359+
apiVersion: apps/v1
1360+
kind: Deployment
1361+
metadata:
1362+
name: kustomize-controller
1363+
namespace: flux-system
1364+
spec:
1365+
template:
1366+
spec:
1367+
containers:
1368+
- name: manager
1369+
args:
1370+
- --sops-vault-configmap=sops-vault-config
1371+
```
1372+
1373+
When enabled, a Kustomization whose decryption Secret does **not** contain a
1374+
`sops.vault-token` authenticates to the Vault address found in the SOPS metadata
1375+
of each encrypted data key as follows:
1376+
1377+
- The Kubernetes ServiceAccount token is issued for
1378+
[`.spec.decryption.serviceAccountName`](#decryption) when set (object-level
1379+
workload identity, which requires the `ObjectLevelWorkloadIdentity` feature
1380+
gate to be enabled), otherwise for the controller's own ServiceAccount
1381+
([controller-level workload identity](#controller-global-decryption)).
1382+
- The token audience is set to the Vault address.
1383+
- The Vault role is derived from the ServiceAccount as `{namespace}_{name}`
1384+
(e.g. a ServiceAccount `sops` in namespace `apps` maps to the role `apps_sops`).
1385+
1386+
On the Vault server, the operator must enable and configure the auth method
1387+
behind the configured login path (for the Kubernetes auth method, with the
1388+
cluster's API server address, CA certificate and a token reviewer JWT), and
1389+
create a role `{namespace}_{name}` bound to the ServiceAccount and namespace,
1390+
with its audience set to the Vault address and a policy granting `update` on the
1391+
relevant `transit/decrypt/<key>` paths. See the
1392+
[OpenBao Kubernetes auth docs](https://openbao.org/docs/auth/kubernetes/) for the
1393+
auth method and role configuration. When several clusters share one Vault, enable
1394+
a separate auth mount per cluster (each configured with that cluster's API
1395+
server, CA and reviewer JWT) and set each cluster's login path in its own
1396+
ConfigMap.
1397+
1398+
The authentication methods are tried in order of precedence: a static
1399+
`sops.vault-token` in the Secret referenced by
1400+
[`.spec.decryption.secretRef`](#decryption) takes priority over Kubernetes auth,
1401+
which in turn takes priority over the global `VAULT_TOKEN` environment variable.
1402+
13131403
#### Controlling the decryption behavior of resources
13141404

13151405
To change the decryption behaviour for specific Kubernetes resources, you can annotate them with:
@@ -1671,10 +1761,15 @@ Controller-level configuration:
16711761
These guides provide detailed instructions for setting up authentication,
16721762
permissions, and controller configuration for each cloud provider.
16731763

1674-
#### Hashicorp Vault
1764+
#### OpenBao/Vault
1765+
1766+
There are two ways to configure controller-level authentication to OpenBao/Vault
1767+
for SOPS decryption: a static token, or the Kubernetes auth method.
1768+
1769+
##### Static token
16751770

1676-
To configure a global default for Hashicorp Vault, patch the controller's
1677-
Deployment with a `VAULT_TOKEN` environment variable.
1771+
To configure a global default token, patch the controller's Deployment with a
1772+
`VAULT_TOKEN` environment variable.
16781773

16791774
```yaml
16801775
---
@@ -1693,6 +1788,22 @@ spec:
16931788
value: <token>
16941789
```
16951790

1791+
##### Kubernetes auth
1792+
1793+
When SOPS decryption uses OpenBao/Vault with the
1794+
[Kubernetes auth method](#openbaovault-kubernetes-auth), the controller-global
1795+
identity is simply kustomize-controller's own ServiceAccount: a Kustomization
1796+
that sets no [`.spec.decryption.serviceAccountName`](#decryption) authenticates
1797+
to Vault as the controller's ServiceAccount (controller-level workload identity).
1798+
1799+
Setting `.spec.decryption.serviceAccountName` makes the authentication
1800+
object-level rather than global, and requires the `ObjectLevelWorkloadIdentity`
1801+
feature gate to be enabled.
1802+
1803+
See [OpenBao/Vault Kubernetes auth](#openbaovault-kubernetes-auth) for enabling
1804+
the method (the `--sops-vault-configmap` flag and ConfigMap) and the Vault server
1805+
configuration.
1806+
16961807
#### SOPS Age Keys
16971808

16981809
To configure global decryption for SOPS Age keys, use the `--sops-age-secret`

0 commit comments

Comments
 (0)