Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions roles/vault_utils/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,112 @@ unseal_namespace: "imperative"
vault_jwt_config: false
```

## Vault JWT authentication

JWT auth lets workloads authenticate to Vault with OIDC discovery (for example SPIFFE JWT
SVIDs validated against an in-cluster SPIRE OIDC provider, or an external IdP). The tasks
in `vault_jwt.yaml` run when `vault_jwt_config` is `true` (Ansible tag: `vault_jwt`).

### Extra vars from the pattern

Several JWT settings are not role defaults. Validated Patterns passes them from pattern
Helm values (for example `overrides/values-vault-jwt.yaml`) as **extra vars** when this role
runs, the same way as `oidc_discovery_url`:

| Extra var | Required | Role |
| --- | --- | --- |
| `oidc_discovery_url` | Yes (when JWT config is enabled) | OIDC discovery document URL for Vault JWT auth |
| `default_role` | No | Default JWT role name in Vault (defaults to `default` in tasks) |
| `oidc_discovery_ca_pem` | No | PEM trust bundle for TLS to `oidc_discovery_url`; see below |
| `vault_jwt_policies` | No | List of `{name, policy}` entries written to Vault |
| `vault_jwt_roles` | No | JWT roles applied via `vault_jwt_roles.yaml` |

Enable JWT handling in pattern values with `vault_jwt_config: true` (role default is `false`).

### Workflow

1. **Enable JWT auth** on Vault if it is not already enabled.
2. **Check reachability** of `oidc_discovery_url` from the Vault pod (`/.well-known/openid-configuration`).
3. **Read current JWT config** from Vault and decide whether an update is needed. Configuration is
rewritten when JWT auth was just enabled, discovery config is missing, or
`oidc_discovery_url` / `default_role` differ from what is stored in Vault.
4. **Build a CA bundle** for TLS verification of the discovery endpoint:
- If the `oidc_discovery_ca_pem` extra var is set, use that material.
- Otherwise read the OpenShift service CA from ConfigMap `openshift-service-ca.crt` in
`vault_ns` (key `service-ca.crt`). OpenShift injects this ConfigMap into every namespace;
it is the correct trust anchor for in-cluster `*.svc` endpoints.
5. **Write the bundle** into the Vault pod and run `vault write auth/jwt/config` with
`oidc_discovery_url`, `default_role`, and `oidc_discovery_ca_pem` (Vault API field).
6. **Apply policies and roles** from `vault_jwt_policies` and `vault_jwt_roles`.

### OIDC discovery CA (`oidc_discovery_ca_pem`)

Vault must trust the TLS certificate presented by the host in `oidc_discovery_url` when it
fetches OIDC metadata. Pass `oidc_discovery_ca_pem` as an extra var (same mechanism as
`oidc_discovery_url`) when the OpenShift service CA is not the right trust anchor—external IdP,
corporate PKI, or TLS terminated with a private CA in front of the discovery URL.

If you omit `oidc_discovery_ca_pem` or pass an empty string, the role uses the OpenShift
service CA from `openshift-service-ca.crt` in `vault_ns`.

| `oidc_discovery_ca_pem` extra var | Trust used for discovery TLS |
| --- | --- |
| Omitted or empty | OpenShift service CA from `openshift-service-ca.crt` in `vault_ns` |
| Single PEM string | That certificate or bundle (multiline YAML with `\|`) |
| List of PEM strings | Certificates concatenated in order (multiple CAs or chain members) |

**In-cluster OIDC** — pattern override with discovery URL only (SPIRE example):

```yaml
vault_jwt_config: true
vault_jwt_policies:
- name: apps-qtodo-jwt-secret
policy: |
path "secret/data/apps/qtodo/*" {
capabilities = ["read"]
}
vault_jwt_roles:
- name: qtodo
audience: https://keycloak.apps.example.com/realms/ztvp
subject: spiffe://apps.example.com/ns/qtodo/sa/qtodo
policies:
- apps-qtodo-jwt-secret
oidc_discovery_url: https://spire-spiffe-oidc-discovery-provider.zero-trust-workload-identity-manager.svc.cluster.local
```

**External OIDC** — add `oidc_discovery_ca_pem` next to `oidc_discovery_url` in the same values file:

```yaml
vault_jwt_config: true
oidc_discovery_url: https://idp.example.com
default_role: default
oidc_discovery_ca_pem: |
-----BEGIN CERTIFICATE-----
MIIDXTCCAkWgAwIBAgIJAKZ...
-----END CERTIFICATE-----
vault_jwt_policies: []
vault_jwt_roles: []
```

**Multiple CAs** — list form in pattern values:

```yaml
oidc_discovery_url: https://idp.example.com
oidc_discovery_ca_pem:
- |
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
- |
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
```

Changing only `oidc_discovery_ca_pem` does not trigger a JWT config update if `oidc_discovery_url`
and `default_role` are unchanged. After rotating a custom CA, change one of those temporarily,
or update `auth/jwt/config` on Vault manually, so the new bundle is applied.

## Dependencies

This relies on [kubernetes.core](https://docs.ansible.com/ansible/latest/collections/kubernetes/core/k8s_module.html)
Expand Down
64 changes: 49 additions & 15 deletions roles/vault_utils/tasks/vault_jwt.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -71,20 +71,57 @@
jwt_config_default_role: "{{ jwt_discovery_config.data.default_role }}"
when: jwt_discovery

- name: Get OIDC discovery certificate
- name: Set JWT configuration update required fact
ansible.builtin.set_fact:
vault_jwt_config_needs_update: >-
{{
(not vault_auth_jwt) or
(not jwt_discovery) or
(jwt_config_oidc_discovery_url | default('') != oidc_discovery_url) or
(jwt_config_default_role | default('') != (default_role | default('default')))
}}

- name: Set whether custom OIDC discovery CA is configured
ansible.builtin.set_fact:
oidc_discovery_ca_custom: >-
{{
(oidc_discovery_ca_pem | default('') is string and (oidc_discovery_ca_pem | default('') | trim | length) > 0) or
(oidc_discovery_ca_pem is defined and oidc_discovery_ca_pem is not string and (oidc_discovery_ca_pem | length) > 0)
}}

- name: Set OIDC discovery CA bundle from user-provided certificate(s)
ansible.builtin.set_fact:
oidc_discovery_ca_bundle: "{{ oidc_discovery_ca_pem if oidc_discovery_ca_pem is string else (oidc_discovery_ca_pem | join('\n')) }}"
when:
- vault_jwt_config_needs_update | bool
- oidc_discovery_ca_custom | bool

- name: Get OpenShift service CA for OIDC discovery
kubernetes.core.k8s_info:
api_version: v1
kind: ConfigMap
name: openshift-service-ca.crt
namespace: "{{ vault_ns }}"
register: openshift_service_ca_cm
when:
- vault_jwt_config_needs_update | bool
- not oidc_discovery_ca_custom | bool

- name: Set OIDC discovery CA bundle from OpenShift service CA
ansible.builtin.set_fact:
oidc_discovery_ca_bundle: "{{ openshift_service_ca_cm.resources[0].data['service-ca.crt'] }}"
when:
- vault_jwt_config_needs_update | bool
- not oidc_discovery_ca_custom | bool

- name: Write OIDC discovery CA certificate to vault pod
kubernetes.core.k8s_exec:
namespace: "{{ vault_ns }}"
pod: "{{ vault_pod }}"
command: >
bash -e -c
"echo -n | openssl s_client -connect {{ oidc_discovery_host }}:{{ oidc_discovery_port }} -servername {{ oidc_discovery_host }}
| openssl x509 -outform PEM > /tmp/oidc-discovery-certificate.pem"
retries: 10
delay: 30
when: not vault_auth_jwt or
not jwt_discovery or
not jwt_config_oidc_discovery_url == oidc_discovery_url or
not jwt_config_default_role == default_role | default('default')
bash -c
"echo {{ oidc_discovery_ca_bundle | b64encode }} | base64 -d > /tmp/oidc-discovery-certificate.pem"
when: vault_jwt_config_needs_update | bool

- name: Write JWT configuration
kubernetes.core.k8s_exec:
Expand All @@ -97,10 +134,7 @@
oidc_discovery_ca_pem=@/tmp/oidc-discovery-certificate.pem
retries: 10
delay: 30
when: not vault_auth_jwt or
not jwt_discovery or
not jwt_config_oidc_discovery_url == oidc_discovery_url or
not jwt_config_default_role == default_role | default('default')
when: vault_jwt_config_needs_update | bool

# Create JWT auth policies from vault_jwt_policies variable
# These are manually defined policies for SPIFFE workloads that need direct Vault access
Expand All @@ -123,7 +157,7 @@
loop_control:
loop_var: jwt_role

- name: Delete router CA certificate
- name: Delete OIDC discovery CA certificate from vault pod
kubernetes.core.k8s_exec:
namespace: "{{ vault_ns }}"
pod: "{{ vault_pod }}"
Expand Down
Loading