Skip to content

Commit a967d29

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

11 files changed

Lines changed: 531 additions & 12 deletions

File tree

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 Hashicorp Vault/OpenBao 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 Hashicorp Vault/OpenBao 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 Hashicorp Vault/OpenBao instances.
30+
Instances []VaultInstance `json:"instances"`
31+
}
32+
33+
// VaultInstance describes a single Hashicorp Vault/OpenBao instance and how the
34+
// controller should authenticate to it.
35+
type VaultInstance struct {
36+
// Address is the address of the Hashicorp Vault/OpenBao 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
@@ -1429,6 +1429,92 @@ as if the resource was present but empty, without any variables defined.</p>
14291429
</table>
14301430
</div>
14311431
</div>
1432+
<h3 id="kustomize.toolkit.fluxcd.io/v1.VaultConfig">VaultConfig
1433+
</h3>
1434+
<p>VaultConfig is the controller-level configuration that enables and scopes
1435+
authentication to Hashicorp Vault/OpenBao instances for SOPS decryption. The
1436+
controller presents a Kubernetes ServiceAccount token to a JWT-backed auth
1437+
method (e.g. the Kubernetes or JWT auth method). The operator provides this
1438+
config through a ConfigMap, listing the instances the controller may
1439+
authenticate to along with each instance&rsquo;s login path. It only governs this
1440+
ServiceAccount-token authentication: the existing static token decryption
1441+
paths (the sops.vault-token Secret entry and the VAULT_TOKEN environment
1442+
variable) are unaffected and continue to work for any address.</p>
1443+
<div class="md-typeset__scrollwrap">
1444+
<div class="md-typeset__table">
1445+
<table>
1446+
<thead>
1447+
<tr>
1448+
<th>Field</th>
1449+
<th>Description</th>
1450+
</tr>
1451+
</thead>
1452+
<tbody>
1453+
<tr>
1454+
<td>
1455+
<code>instances</code><br>
1456+
<em>
1457+
<a href="#kustomize.toolkit.fluxcd.io/v1.VaultInstance">
1458+
[]VaultInstance
1459+
</a>
1460+
</em>
1461+
</td>
1462+
<td>
1463+
<p>Instances is the list of known Hashicorp Vault/OpenBao instances.</p>
1464+
</td>
1465+
</tr>
1466+
</tbody>
1467+
</table>
1468+
</div>
1469+
</div>
1470+
<h3 id="kustomize.toolkit.fluxcd.io/v1.VaultInstance">VaultInstance
1471+
</h3>
1472+
<p>
1473+
(<em>Appears on:</em>
1474+
<a href="#kustomize.toolkit.fluxcd.io/v1.VaultConfig">VaultConfig</a>)
1475+
</p>
1476+
<p>VaultInstance describes a single Hashicorp Vault/OpenBao instance and how the
1477+
controller should authenticate to it.</p>
1478+
<div class="md-typeset__scrollwrap">
1479+
<div class="md-typeset__table">
1480+
<table>
1481+
<thead>
1482+
<tr>
1483+
<th>Field</th>
1484+
<th>Description</th>
1485+
</tr>
1486+
</thead>
1487+
<tbody>
1488+
<tr>
1489+
<td>
1490+
<code>address</code><br>
1491+
<em>
1492+
string
1493+
</em>
1494+
</td>
1495+
<td>
1496+
<p>Address is the address of the Hashicorp Vault/OpenBao instance, matching
1497+
the address stored in the SOPS metadata of the encrypted data key.</p>
1498+
</td>
1499+
</tr>
1500+
<tr>
1501+
<td>
1502+
<code>loginPath</code><br>
1503+
<em>
1504+
string
1505+
</em>
1506+
</td>
1507+
<td>
1508+
<p>LoginPath is the API path of the login endpoint to authenticate to this
1509+
instance with, e.g. &ldquo;auth/kubernetes/login&rdquo;. It is used verbatim, so it
1510+
supports any JWT-backed auth method (e.g. the Kubernetes or JWT auth
1511+
method) and namespace-prefixed paths (e.g. &ldquo;ns1/ns2/auth/kubernetes/login&rdquo;).</p>
1512+
</td>
1513+
</tr>
1514+
</tbody>
1515+
</table>
1516+
</div>
1517+
</div>
14321518
<div class="admonition note">
14331519
<p class="last">This page was automatically generated with <code>gen-crd-api-reference-docs</code></p>
14341520
</div>

docs/spec/v1/kustomizations.md

Lines changed: 98 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1061,7 +1061,8 @@ 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 Hashicorp Vault/OpenBao via the [Kubernetes auth method](#kubernetes-auth).
10651066

10661067
To make a Kustomization react immediately to changes in the referenced Secret
10671068
see [this](#reacting-immediately-to-configuration-dependencies) section.
@@ -1310,6 +1311,10 @@ data:
13101311
sops.vault-token: <BASE64>
13111312
```
13121313

1314+
Alternatively, for secret-less authentication to Hashicorp Vault/OpenBao using a
1315+
Kubernetes ServiceAccount instead of a static token, see
1316+
[Kubernetes auth](#kubernetes-auth).
1317+
13131318
#### Controlling the decryption behavior of resources
13141319

13151320
To change the decryption behaviour for specific Kubernetes resources, you can annotate them with:
@@ -1673,8 +1678,13 @@ permissions, and controller configuration for each cloud provider.
16731678

16741679
#### Hashicorp Vault
16751680

1676-
To configure a global default for Hashicorp Vault, patch the controller's
1677-
Deployment with a `VAULT_TOKEN` environment variable.
1681+
There are two ways to configure controller-level authentication to Hashicorp
1682+
Vault/OpenBao for SOPS decryption: a static token, or the Kubernetes auth method.
1683+
1684+
##### Static token
1685+
1686+
To configure a global default token, patch the controller's Deployment with a
1687+
`VAULT_TOKEN` environment variable.
16781688

16791689
```yaml
16801690
---
@@ -1693,6 +1703,91 @@ spec:
16931703
value: <token>
16941704
```
16951705

1706+
##### Kubernetes auth
1707+
1708+
Instead of a static token, the controller can authenticate to Hashicorp
1709+
Vault/OpenBao by exchanging a Kubernetes ServiceAccount token for a short-lived
1710+
Vault token via a JWT-backed auth method (e.g. the
1711+
[Kubernetes auth method](https://openbao.org/docs/auth/kubernetes/) or the
1712+
[JWT auth method](https://openbao.org/docs/auth/jwt/)). This is enabled with the
1713+
`--sops-vault-configmap` flag, which names a ConfigMap in the controller's
1714+
namespace that maps each Vault address to the login path to authenticate at. The
1715+
ConfigMap also acts as an **allowlist**: only addresses listed in it can be
1716+
authenticated to this way. This does not affect the static token paths below —
1717+
the `sops.vault-token` Secret entry and the `VAULT_TOKEN` environment variable
1718+
continue to work for any address. When the flag is empty, this authentication is
1719+
disabled and decryption falls back to those static token paths.
1720+
1721+
The mapping is stored under the `config.yaml` key as a list of instances, each
1722+
with an `address` and the `loginPath` to authenticate at. The login path is used
1723+
verbatim, so it supports any JWT-backed auth method and namespace-prefixed paths
1724+
(e.g. `ns1/ns2/auth/kubernetes/login`). The ConfigMap must be in the same
1725+
namespace as the kustomize-controller Deployment.
1726+
1727+
```yaml
1728+
---
1729+
apiVersion: v1
1730+
kind: ConfigMap
1731+
metadata:
1732+
name: sops-vault-config
1733+
namespace: flux-system
1734+
data:
1735+
config.yaml: |
1736+
instances:
1737+
- address: https://vault-a.example.com:8200
1738+
loginPath: auth/kubernetes/login
1739+
- address: https://vault-b.example.com:8200
1740+
loginPath: ns1/ns2/auth/kubernetes/login
1741+
```
1742+
1743+
Then, patch the kustomize-controller Deployment to add the flag:
1744+
1745+
```yaml
1746+
---
1747+
apiVersion: apps/v1
1748+
kind: Deployment
1749+
metadata:
1750+
name: kustomize-controller
1751+
namespace: flux-system
1752+
spec:
1753+
template:
1754+
spec:
1755+
containers:
1756+
- name: manager
1757+
args:
1758+
- --sops-vault-configmap=sops-vault-config
1759+
```
1760+
1761+
When enabled, a Kustomization whose decryption Secret does **not** contain a
1762+
`sops.vault-token` authenticates to the Vault address found in the SOPS metadata
1763+
of each encrypted data key as follows:
1764+
1765+
- The Kubernetes ServiceAccount token is issued for
1766+
[`.spec.decryption.serviceAccountName`](#decryption) when set (object-level
1767+
workload identity), otherwise for the controller's own ServiceAccount
1768+
(controller-level workload identity). Object-level workload identity requires
1769+
the `ObjectLevelWorkloadIdentity` feature gate to be enabled.
1770+
- The token audience is set to the Vault address.
1771+
- The Vault role is derived from the ServiceAccount as `{namespace}_{name}`
1772+
(e.g. a ServiceAccount `sops` in namespace `apps` maps to the role `apps_sops`).
1773+
1774+
On the Vault server, the operator must enable and configure the auth method
1775+
behind the configured login path (for the Kubernetes auth method, with the
1776+
cluster's API server address, CA certificate and a token reviewer JWT), and
1777+
create a role `{namespace}_{name}` bound to the ServiceAccount and namespace,
1778+
with its audience set to the Vault address and a policy granting `update` on the
1779+
relevant `transit/decrypt/<key>` paths. See the
1780+
[OpenBao Kubernetes auth docs](https://openbao.org/docs/auth/kubernetes/) for the
1781+
auth method and role configuration. When several clusters share one Vault, enable
1782+
a separate auth mount per cluster (each configured with that cluster's API
1783+
server, CA and reviewer JWT) and set each cluster's login path in its own
1784+
ConfigMap.
1785+
1786+
The authentication methods are tried in order of precedence: a static
1787+
`sops.vault-token` in the Secret referenced by
1788+
[`.spec.decryption.secretRef`](#decryption) takes priority over Kubernetes auth,
1789+
which in turn takes priority over the global `VAULT_TOKEN` environment variable.
1790+
16961791
#### SOPS Age Keys
16971792

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

internal/controller/kustomization_controller.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ type KustomizationReconciler struct {
106106
NoCrossNamespaceRefs bool
107107
NoRemoteBases bool
108108
SOPSAgeSecret string
109+
SOPSVaultConfigMap string
109110
TokenCache *cache.TokenCache
110111

111112
// Retry and requeue options
@@ -758,6 +759,9 @@ func (r *KustomizationReconciler) build(ctx context.Context,
758759
if name, ns := r.SOPSAgeSecret, os.Getenv(runtimeCtrl.EnvRuntimeNamespace); name != "" && ns != "" {
759760
decryptorOpts = append(decryptorOpts, decryptor.WithSOPSAgeSecret(name, ns))
760761
}
762+
if name, ns := r.SOPSVaultConfigMap, os.Getenv(runtimeCtrl.EnvRuntimeNamespace); name != "" && ns != "" {
763+
decryptorOpts = append(decryptorOpts, decryptor.WithVaultConfigMap(name, ns))
764+
}
761765
dec, cleanup, err := decryptor.New(r.Client, obj, decryptorOpts...)
762766
if err != nil {
763767
return nil, err

0 commit comments

Comments
 (0)