OIDC certificate exchange server and client for Talos Linux. Enables OIDC-based access control for talosctl by issuing ephemeral short-lived client certificates signed by the Talos CA.
Talos Linux uses mTLS (mutual TLS) with client certificates for API authentication. There is no native OIDC support in the Talos API. This tool bridges the gap through a certificate exchange server model:
- A server (
talosctl-oidc serve) holds the Talos CA private key and runs alongside the cluster (as a Talos extension or standalone) - A user runs
talosctl-oidc login, which opens a browser for OIDC authentication (Authorization Code + PKCE) - The client sends the resulting ID token to the server
- The server validates the token (signature, issuer, audience, expiry) and signs an ephemeral short-lived client certificate (default: 5 minutes)
- The client writes the certificate to
~/.talos/config(using atomic updates to prevent corruption) - Before expiry, the client can proactively renew the certificate using the OIDC refresh token, either on demand or in the background via the
--watchflag
sequenceDiagram
participant User
participant Browser
participant Client as talosctl-oidc (client)
participant OIDC as OIDC Provider
participant Server as talosctl-oidc (server)
participant Talos as Talos Node
User->>Client: talosctl-oidc login
Client->>Browser: Open authorization URL
Browser->>OIDC: Authorization Code + PKCE
OIDC->>Browser: Redirect with auth code
Browser->>Client: Callback with auth code
Client->>OIDC: Exchange code for tokens
OIDC->>Client: ID token + access token
Client->>Server: POST /exchange {id_token}
Server->>OIDC: Fetch JWKS & validate token
OIDC->>Server: Public keys
Server->>Server: Sign ephemeral cert with Talos CA
Server->>Client: {ca, cert, key, endpoints}
Client->>Client: Write to ~/.talos/config
User->>Talos: talosctl version (mTLS with ephemeral cert)
- Go 1.25+ (to build from source)
- A running Talos cluster with API access
- The Talos API CA certificate and private key (from
controlplane.yaml, the firstca:block undermachine.ca) - An OIDC provider with a configured client application (any OIDC-compliant provider works)
The quickest way to install on Linux or macOS:
curl -fsSL https://raw.githubusercontent.com/qjoly/talosctl-oidc/main/install.sh | shThe script:
- Detects your OS and architecture automatically
- Fetches the latest release from GitHub
- Verifies the SHA-256 checksum before installing
- Installs to
/usr/local/bin(falls back to~/.local/binif not writable)
Install a specific version:
curl -fsSL https://raw.githubusercontent.com/qjoly/talosctl-oidc/main/install.sh | VERSION=v0.0.2 shbrew install qjoly/tap/talosctl-oidcThe formula is updated automatically on every release.
If you have Go installed:
go install github.com/qjoly/talosctl-oidc@latestDownload the binary for your platform directly from the releases page:
| OS | Architecture | Binary |
|---|---|---|
| Linux | x86-64 | talosctl-oidc-linux-amd64 |
| Linux | ARM64 | talosctl-oidc-linux-arm64 |
| macOS | Intel | talosctl-oidc-darwin-amd64 |
| macOS | Apple Silicon (M1/M2/M3) | talosctl-oidc-darwin-arm64 |
| Windows | x86-64 | talosctl-oidc-windows-amd64.exe |
# Example: Linux amd64
curl -fsSL https://github.com/qjoly/talosctl-oidc/releases/latest/download/talosctl-oidc-linux-amd64 \
-o /usr/local/bin/talosctl-oidc
chmod +x /usr/local/bin/talosctl-oidcgit clone https://github.com/qjoly/talosctl-oidc.git
cd talosctl-oidc
go build -o talosctl-oidc .
sudo mv talosctl-oidc /usr/local/bin/Build the extension image, then use the Talos imager to create a custom installer that includes it:
# Build and push the extension OCI image
docker build -t ghcr.io/qjoly/talosctl-oidc-talos-ext:v0.1.0 --target extension .
docker push ghcr.io/qjoly/talosctl-oidc-talos-ext:v0.1.0
# Build a custom Talos installer with the extension baked in
TALOS_VERSION=v1.12.4
EXTENSION_REF=$(crane digest ghcr.io/qjoly/talosctl-oidc-talos-ext:v0.1.0)
docker run --rm -t -v $PWD/_out:/out \
ghcr.io/siderolabs/imager:${TALOS_VERSION} installer \
--system-extension-image ghcr.io/qjoly/talosctl-oidc-talos-ext:v0.1.0@${EXTENSION_REF}
# Push the custom installer to your registry
crane push _out/installer-amd64.tar ghcr.io/qjoly/talosctl-oidc-installer:${TALOS_VERSION}
# Install or upgrade with it
talosctl upgrade --image ghcr.io/qjoly/talosctl-oidc-installer:${TALOS_VERSION}See Deploying as a Talos Extension for detailed configuration.
Create a client application in your OIDC provider with the following settings:
| Setting | Value |
|---|---|
| Client type | Public |
| Grant type | Authorization Code |
| Redirect URI | http://127.0.0.1:8900/callback |
| Scopes | openid, profile, email, offline_access |
| PKCE | Enabled (S256) |
- Go to Admin > Applications > Providers > Create
- Select OAuth2/OpenID Provider
- Set Client type to Public
- Set Redirect URIs to
http://127.0.0.1:8900/callback - Under Advanced, ensure Subject mode is set appropriately
- Add scopes
openid,profile,offline_accessandemail - Create an Application linked to this provider
- Go to your Keycloak admin console > Clients > Create client
- Set Client authentication to Off (public client)
- Enable Standard flow (Authorization Code)
- Set Valid redirect URIs to
http://127.0.0.1:8900/callback - Under Advanced > Proof Key for Code Exchange, set to
S256
staticClients:
- id: talosctl
name: "Talosctl OIDC"
redirectURIs:
- "http://127.0.0.1:8900/callback"
public: trueThe server needs the Talos API CA certificate and private key to sign client certificates. These are found in your cluster's controlplane.yaml (the first ca: block under machine:).
# From controlplane.yaml, extract the first ca block
yq '.machine.ca.crt' controlplane.yaml | base64 -d > talos-ca.crt
yq '.machine.ca.key' controlplane.yaml | base64 -d > talos-ca.keyImportant: This is the machine/API CA, not the OS-level CA from
secrets.yaml. The API CA is the one that signs client certificates used bytalosctl.
The serve command can be configured via environment variables or a YAML configuration file. Environment variables take precedence over file values when both are set.
Create a config.yaml file and specify it with the --config flag:
talosctl-oidc serve --config config.yamlExample config.yaml:
issuer_url: https://idp.example.com/application/o/talos-oidc/
client_id: your-client-id
endpoints:
- 10.0.0.1
- 10.0.0.2
ca_cert: talos-ca.crt
ca_key: talos-ca.key
listen: ":8443"
cert_ttl: "5m"
# Static roles — used when RBAC is disabled or no rule matches.
# Set to [] to deny access when no RBAC rule matches.
roles:
- os:admin
# Optional: Rate limiting (disabled by default)
rate_limit_requests: 10
rate_limit_window: "1m"
# Optional: IP allowlist (empty = allow all)
ip_allowlist:
- 192.168.1.0/24
- 10.0.0.50
# Optional: Dynamic RBAC — map OIDC claims to Talos roles
# rbac:
# rules:
# - claim: groups
# value: platform-admins
# roles:
# - os:admin
# - claim: groups
# value: developers
# roles:
# - os:readerYou can also set the config file path via the TALOSCTL_OIDC_CONFIG environment variable:
export TALOSCTL_OIDC_CONFIG=/etc/talosctl-oidc/config.yaml
talosctl-oidc serveNote: See the Configuration wiki page for complete documentation on config file options and precedence rules.
Configure entirely via environment variables:
export TALOSCTL_OIDC_CA_CERT=talos-ca.crt
export TALOSCTL_OIDC_CA_KEY=talos-ca.key
export TALOSCTL_OIDC_ISSUER_URL=https://idp.example.com/application/o/talos-oidc/
export TALOSCTL_OIDC_CLIENT_ID=<your-client-id>
export TALOSCTL_OIDC_ENDPOINTS=10.0.0.1,10.0.0.2
export TALOSCTL_OIDC_LISTEN=:8443
export TALOSCTL_OIDC_CERT_TTL=5m
export TALOSCTL_OIDC_ROLES=os:admin
talosctl-oidc serveBy default, the server generates a self-signed TLS certificate at startup and logs the CA PEM. Save the CA PEM to a file and pass it to login --server-ca so the client trusts the server.
To use your own TLS certificates:
export TALOSCTL_OIDC_TLS_CERT=/path/to/server.crt
export TALOSCTL_OIDC_TLS_KEY=/path/to/server.key
talosctl-oidc serveTo run without TLS (not recommended for production):
export TALOSCTL_OIDC_INSECURE=true
talosctl-oidc serve# With self-signed TLS (default server mode), save the CA PEM from server logs:
talosctl-oidc login \
--provider https://idp.example.com/application/o/talos-oidc/ \
--client-id <your-client-id> \
--server https://localhost:8443 \
--server-ca server-ca.pem \
--context-name oidc \
--callback-port 8900
# With insecure server:
talosctl-oidc login \
--provider https://idp.example.com/application/o/talos-oidc/ \
--client-id <your-client-id> \
--server http://localhost:8443 \
--insecure \
--context-name oidc \
--callback-port 8900This will:
- Open your browser to the OIDC provider login page
- Wait for you to authenticate
- Exchange the ID token with the cert server for an ephemeral certificate (over TLS)
- Write the certificate to
~/.talos/configunder theoidccontext - (Optional) If
--watchis provided, stay in the foreground and refresh certificates as they approach expiry
After login, talosctl works normally:
talosctl --context oidc version
talosctl --context oidc get members
talosctl --context oidc dashboardStart the certificate exchange server.
talosctl-oidc serveThe server can be configured via YAML configuration file or environment variables:
- Use
--config <path>flag orTALOSCTL_OIDC_CONFIGenv var to specify a config file - Environment variables override config file values
- See the Configuration wiki page for details
| Variable | Config File Field | Required | Default | Description |
|---|---|---|---|---|
TALOSCTL_OIDC_CA_CERT |
ca_cert |
Yes* | Path to Talos CA certificate | |
TALOSCTL_OIDC_CA_KEY |
ca_key |
Yes* | Path to Talos CA private key | |
TALOSCTL_OIDC_CA_CERT_DATA |
ca_cert_data |
Yes* | Inline PEM-encoded CA certificate | |
TALOSCTL_OIDC_CA_KEY_DATA |
ca_key_data |
Yes* | Inline PEM-encoded CA private key | |
TALOSCTL_OIDC_TALOS_CONFIG |
talos_config |
Yes* | Path to talosconfig YAML file | |
TALOSCTL_OIDC_ISSUER_URL |
issuer_url |
Yes | OIDC issuer URL for token validation | |
TALOSCTL_OIDC_CLIENT_ID |
client_id |
Yes | Expected OIDC client ID / audience | |
TALOSCTL_OIDC_ENDPOINTS |
endpoints |
Yes | Talos node endpoints (comma-separated) | |
TALOSCTL_OIDC_CLIENT_SECRET |
client_secret |
No | OIDC client secret (for HS256-signed tokens) | |
TALOSCTL_OIDC_LISTEN |
listen |
No | :8443 |
Address to listen on |
TALOSCTL_OIDC_CERT_TTL |
cert_ttl |
No | 5m |
Lifetime of issued client certificates |
TALOSCTL_OIDC_ROLES |
roles |
No | os:admin |
Talos roles (comma-separated) |
TALOSCTL_OIDC_TLS_CERT |
tls_cert |
No | Path to TLS certificate (HTTPS with provided cert) | |
TALOSCTL_OIDC_TLS_KEY |
tls_key |
No | Path to TLS private key (must be set with TLS_CERT) | |
TALOSCTL_OIDC_INSECURE |
insecure |
No | false |
Set to true to serve plain HTTP |
TALOSCTL_OIDC_DATA_DIR |
data_dir |
No | Directory to persist self-signed TLS certs across restarts | |
TALOSCTL_OIDC_AUDIT_LOG |
audit_log |
No | stdout | Path to audit log file (- for stdout) |
TALOSCTL_OIDC_ADMIN_TOKEN |
admin_token |
No | Bearer token to protect /admin/* endpoints (required to enable admin API) |
|
TALOSCTL_OIDC_RATE_LIMIT_REQUESTS |
rate_limit_requests |
No | 0 |
Max requests per IP per window (0 = disabled) |
TALOSCTL_OIDC_RATE_LIMIT_WINDOW |
rate_limit_window |
No | 1m |
Rate limit time window (e.g., 1m, 5m, 1h) |
TALOSCTL_OIDC_IP_ALLOWLIST |
ip_allowlist |
No | Comma-separated list of allowed IPs/CIDRs (empty = allow all) | |
DEBUG |
— | No | Set to any value to enable detailed debug logging (includes RBAC rule evaluation) |
* Either CA files, inline CA data, or talos_config is required
| Mode | Configuration | Description |
|---|---|---|
| Self-signed (default) | No TLS env vars | Generates a self-signed cert at startup, logs the CA PEM |
| Self-signed + persisted | DATA_DIR=/data |
Same as above, but cert is saved to disk and reused on restart |
| Provided cert | TLS_CERT + TLS_KEY |
HTTPS with your own certificate |
| Insecure | INSECURE=true |
Plain HTTP (not recommended for production) |
When TALOSCTL_OIDC_DATA_DIR is set, the self-signed CA and server certificate are
saved to <DATA_DIR>/ca.crt, ca.key, server.crt, and server.key. On subsequent
restarts, the same certificates are reloaded so the CA PEM stays stable and clients
don't need to update their --server-ca file.
| Endpoint | Method | Description |
|---|---|---|
/exchange |
POST | Exchange an OIDC ID token for an ephemeral certificate |
/healthz |
GET | Health check (returns 200 OK) |
/ca |
GET | Returns the self-signed CA PEM (only in self-signed mode) |
/admin/ |
GET | Web dashboard with session-based auth |
/admin/login |
POST | Login form submission |
/admin/logout |
POST | Logout and clear session |
/admin/stats |
GET | Server statistics (requires Authorization: Bearer <admin-token>) |
/admin/certs |
GET | List active (non-expired) issued certs (requires Authorization: Bearer <admin-token>) |
Exchange request:
{"id_token": "eyJ..."}Exchange response:
{
"ca": "-----BEGIN CERTIFICATE-----\n...",
"cert": "-----BEGIN CERTIFICATE-----\n...",
"key": "-----BEGIN ED25519 PRIVATE KEY-----\n...",
"endpoints": ["10.0.0.1"],
"ttl_seconds": 3600
}Authenticate via OIDC and obtain ephemeral Talos credentials.
talosctl-oidc login [flags]| Flag | Required | Default | Description |
|---|---|---|---|
--provider |
Yes | OIDC issuer URL | |
--client-id |
Yes | OIDC client ID | |
--server |
Yes | Cert exchange server URL (e.g. https://localhost:8443) |
|
--client-secret |
No | OIDC client secret (for confidential clients) | |
--scopes |
No | openid,profile,email |
OIDC scopes |
--callback-port |
No | 8900 |
Local callback server port |
--context-name |
No | oidc |
Name for the talosconfig context |
--talosconfig |
No | ~/.talos/config |
Path to talosconfig file |
--server-ca |
No | Path to PEM CA certificate to trust for the server (for self-signed TLS) | |
--insecure |
No | false |
Allow plain HTTP connection to the server |
--watch |
No | false |
Run in the background and keep the Talos certificate fresh |
Remove OIDC credentials and clear cached tokens.
talosctl-oidc logout [flags]| Flag | Required | Default | Description |
|---|---|---|---|
--context-name |
No | oidc |
Name of the talosconfig context to remove |
--talosconfig |
No | ~/.talos/config |
Path to talosconfig file |
This removes:
- The OIDC token from the system keychain
- The context (including embedded certificates) from the talosconfig file
Display current authentication status.
talosctl-oidc status [flags]| Flag | Required | Default | Description |
|---|---|---|---|
--context-name |
No | oidc |
Name of the talosconfig context to check |
--talosconfig |
No | ~/.talos/config |
Path to talosconfig file |
Choose the deployment method that best fits your infrastructure:
| Method | Best for | Complexity | Notes |
|---|---|---|---|
| Talos extension | Single-cluster, no existing k8s infra | Low | Baked into the node, restarts with the node |
| Kubernetes Deployment | Multi-cluster, existing k8s platform | Medium | Needs external access from developer workstations |
| Standalone systemd | Air-gapped, dedicated infra, simplicity | Low | Manual updates, needs a Linux host |
Talos system extensions must be baked into the installer image — you cannot install them at runtime. This requires building a custom installer image using the Talos imager.
Use the Talos imager container to produce an installer image that includes the extension. You need crane to push the result.
# Determine the digest of your extension image
EXTENSION_REF=$(crane digest ghcr.io/qjoly/talosctl-oidc-talos-ext:v0.1.0)
# Build the installer (adjust the Talos version to match your cluster)
TALOS_VERSION=v1.12.4
docker run --rm -t -v $PWD/_out:/out \
ghcr.io/siderolabs/imager:${TALOS_VERSION} installer \
--system-extension-image ghcr.io/qjoly/talosctl-oidc-talos-ext:v0.1.0@${EXTENSION_REF}This produces _out/metal-amd64-installer.tar.
Push it to your container registry:
crane push _out/metal-amd64-installer.tar ghcr.io/qjoly/talosctl-oidc-installer:${TALOS_VERSION}For a new installation, reference the custom installer in your machine config:
machine:
install:
image: ghcr.io/qjoly/talosctl-oidc-installer:v0.1.0For an existing cluster, upgrade nodes to the new installer:
talosctl upgrade --image ghcr.io/qjoly/talosctl-oidc-installer:v0.1.0You can also build an ISO for bare-metal boot by replacing
installerwithisoin the imager command.
The extension reads its configuration from an ExtensionServiceConfig document in the Talos machine config. The CA certificate and key are provided as config files, and all runtime settings are passed via environment variables.
Add this to your machine config (or apply it via talosctl apply-config):
apiVersion: v1alpha1
kind: ExtensionServiceConfig
name: talosctl-oidc
configFiles:
- content: |
-----BEGIN CERTIFICATE-----
<your Talos API CA certificate>
-----END CERTIFICATE-----
mountPath: /config/ca.crt
- content: |
-----BEGIN ED25519 PRIVATE KEY-----
<your Talos API CA private key>
-----END ED25519 PRIVATE KEY-----
mountPath: /config/ca.key
environment:
- TALOSCTL_OIDC_CA_CERT=/config/ca.crt
- TALOSCTL_OIDC_CA_KEY=/config/ca.key
- TALOSCTL_OIDC_ISSUER_URL=https://idp.example.com/application/o/talos-oidc/
- TALOSCTL_OIDC_CLIENT_ID=your-client-id
- TALOSCTL_OIDC_ENDPOINTS=10.0.0.1,10.0.0.2
- TALOSCTL_OIDC_CERT_TTL=5m
- TALOSCTL_OIDC_ROLES=os:admin
- TALOSCTL_OIDC_DATA_DIR=/var/lib/talosctl-oidcThe extension service mounts /var/lib/talosctl-oidc from the host (Talos EPHEMERAL partition) into the container. This directory persists the self-signed TLS certificate across restarts so the CA PEM stays stable and clients don't need to update their --server-ca file.
If the data directory is not writable (e.g. the mount is missing), the server falls back to in-memory certificate generation and logs a warning.
To use your own TLS certificates instead, mount them as config files and set TALOSCTL_OIDC_TLS_CERT and TALOSCTL_OIDC_TLS_KEY.
See the Environment Variables table for all available settings.
When TALOSCTL_OIDC_DATA_DIR is set (recommended), the CA PEM is written to <DATA_DIR>/ca.crt and is stable across restarts. You can retrieve it directly from the server:
# Fetch the CA PEM from the /ca endpoint (only available in self-signed mode)
curl -k https://<talos-node-ip>:8443/ca > server-ca.pemOr read it from the extension logs on first start:
talosctl logs ext-talosctl-oidc | grep -A 20 "BEGIN CERTIFICATE"The Talos node must be able to reach the OIDC provider over HTTPS (e.g. https://idp.example.com). The server fetches the provider's JWKS keys to validate tokens. If the node is on an isolated network, ensure the provider's hostname is resolvable and reachable from the node.
After the node boots (or upgrades) with the custom installer, the extension runs as ext-talosctl-oidc:
# Check service status
talosctl service ext-talosctl-oidc
# View logs
talosctl logs ext-talosctl-oidc
# Restart the service
talosctl service ext-talosctl-oidc restartThis method runs the server as a Kubernetes Deployment managed by a Helm chart. It is a good fit if you already have a Kubernetes cluster and want to share the server across multiple Talos clusters or teams.
Requirements: the server endpoint must be reachable from developer workstations, not just from inside the cluster. See Exposing the server below.
The chart is published to GHCR as an OCI chart. Install the latest release directly:
helm install talosctl-oidc \
oci://ghcr.io/qjoly/charts/talosctl-oidc \
--version <version> \
--namespace talos-system --create-namespaceAlternatively, clone the repository and install from the local path:
git clone https://github.com/qjoly/talosctl-oidc.git
cd talosctl-oidc
helm install talosctl-oidc charts/talosctl-oidc/ --namespace talos-system --create-namespaceThe Helm chart deploys the server image (
ghcr.io/qjoly/talosctl-oidc-server), which includes the system CA bundle at the standard/etc/ssl/certs/ca-certificates.crtpath and the binary at/talosctl-oidc. This is distinct from the extension image (ghcr.io/qjoly/talosctl-oidc-talos-ext) used by the Talos imager, which has a different directory layout required by the Talos extension runtime.
The server needs the Talos API CA certificate and private key to sign client certificates. There are two ways to supply them.
Extract the CA from your cluster with talosctl:
talosctl get osrootsecrets -o yaml
# Copy spec.issuingCA.crt and spec.issuingCA.key (base64-encoded PEM)
# Decode them:
echo "<base64-crt>" | base64 -d > talos-ca.crt
echo "<base64-key>" | base64 -d > talos-ca.keyPass the decoded PEM files at install time via --set-file. The chart stores them in a Kubernetes Secret and injects them via environment variables — no file mount needed:
helm install talosctl-oidc charts/talosctl-oidc/ \
--namespace talos-system --create-namespace \
--set-file talos.caCertData=talos-ca.crt \
--set-file talos.caKeyData=talos-ca.key \
--set config.issuerUrl=https://idp.example.com/application/o/talos-oidc/ \
--set config.clientId=your-client-id \
--set "config.endpoints={10.0.0.1,10.0.0.2}"
--set-filereads the file contents verbatim (preserving newlines). Do not use--setor--set-stringfor PEM data — those pass the value as a shell string and will strip the newlines that PEM requires, causing a "failed to decode CA certificate PEM" error at startup.
Keep
talos-ca.keysafe. It is the private key that signs all client certificates. Do not commit it to source control.
Create a Kubernetes Secret manually (e.g. via an external secrets operator), then point the chart at it:
kubectl create secret generic talosctl-oidc-ca \
--namespace talos-system \
--from-file=talos-ca.crt=talos-ca.crt \
--from-file=talos-ca.key=talos-ca.keyhelm install talosctl-oidc charts/talosctl-oidc/ \
--namespace talos-system --create-namespace \
--set talos.caSecretName=talosctl-oidc-ca \
--set "talos.caSecretKeys.cert=talos-ca.crt" \
--set "talos.caSecretKeys.key=talos-ca.key" \
--set config.issuerUrl=https://idp.example.com/application/o/talos-oidc/ \
--set config.clientId=your-client-id \
--set "config.endpoints={10.0.0.1,10.0.0.2}"| Value | Default | Description |
|---|---|---|
config.issuerUrl |
"" |
OIDC issuer URL (required) |
config.clientId |
"" |
OIDC client ID (required) |
config.clientSecret |
"" |
OIDC client secret (optional) |
config.endpoints |
[] |
Talos node endpoints (required) |
config.roles |
[os:admin] |
Talos roles for issued certs |
config.certTTL |
5m |
Lifetime of issued client certificates |
config.adminToken |
"" |
Bearer token to enable the admin API |
config.auditLog |
- |
Audit log destination (- for stdout) |
talos.caCertData |
"" |
Inline Talos CA certificate PEM |
talos.caKeyData |
"" |
Inline Talos CA private key PEM |
talos.caSecretName |
"" |
Name of an existing Secret with the CA |
talos.caSecretKeys.cert |
talos-ca.crt |
Key name for the cert in the Secret |
talos.caSecretKeys.key |
talos-ca.key |
Key name for the key in the Secret |
service.type |
ClusterIP |
Kubernetes Service type |
ingress.enabled |
false |
Enable Ingress |
tolerations |
[] |
Pod tolerations |
extraVolumes |
[] |
Additional volumes to attach to the pod |
extraVolumeMounts |
[] |
Additional volume mounts for the container |
To persist the self-signed TLS certificate across pod restarts (so the CA PEM stays stable), mount a PersistentVolumeClaim and set TALOSCTL_OIDC_DATA_DIR via extraVolumes / extraVolumeMounts:
helm upgrade talosctl-oidc charts/talosctl-oidc/ \
--namespace talos-system \
--set "extraVolumes[0].name=tls-data" \
--set "extraVolumes[0].persistentVolumeClaim.claimName=talosctl-oidc-tls" \
--set "extraVolumeMounts[0].name=tls-data" \
--set "extraVolumeMounts[0].mountPath=/data" \
--set "extraEnv[0].name=TALOSCTL_OIDC_DATA_DIR" \
--set "extraEnv[0].value=/data"The server handles its own TLS — the client verifies the server CA directly. Standard TLS-terminating ingresses will break the connection. Choose one of the options below.
helm upgrade talosctl-oidc charts/talosctl-oidc/ \
--namespace talos-system \
--set service.type=LoadBalancerkubectl get svc talosctl-oidc -n talos-system
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# talosctl-oidc LoadBalancer 10.96.12.34 203.0.113.10 8443:32443/TCP 1mUsers connect to https://203.0.113.10:8443.
helm upgrade talosctl-oidc charts/talosctl-oidc/ \
--namespace talos-system \
--set service.type=NodePortUsers connect to https://<any-node-ip>:<node-port>.
If you want to expose the server on a custom domain (e.g. https://oidc.example.com), you can use an ingress. Be careful that you should enable insecure mode and let the ingress handle TLS termination (which means the connection between the ingress and the server is unencrypted, but the client-server connection is still secure with TLS).
helm upgrade talosctl-oidc charts/talosctl-oidc/ \
--namespace talos-system \
--set ingress.enabled=true \
--set ingress.className=traefik \
--set "ingress.hosts[0].host=oidc.example.com" \
--set "ingress.hosts[0].paths[0].path=/" \
--set "ingress.hosts[0].paths[0].pathType=Prefix"This could not be the most secure option since the traffic between the ingress and the server is unencrypted.
Another option could be to generate a TLS Certificate through cert-manager and map it to the server through a Kubernetes Secret (to configure SSL passthrough). This way, the connection between the ingress and the server is also encrypted, but it requires more setup (see issue)
If you used the self-signed TLS mode, the CA PEM is stable across restarts if you set TALOSCTL_OIDC_DATA_DIR. You can retrieve it from the server's /ca endpoint:
# Fetch the self-signed CA from the /ca endpoint
curl -k https://<external-ip>:8443/ca > server-ca.pem
# Log in
talosctl-oidc login \
--provider https://idp.example.com/application/o/talos-oidc/ \
--client-id your-client-id \
--server https://<external-ip>:8443 \
--server-ca server-ca.pem \
--context-name oidcThis is not needed if you used your own TLS certificates.
This method runs the server directly on a Linux host (a jump host, a VM, or any machine that developer workstations can reach). No Kubernetes or Talos node is required.
From GitHub releases (replace the version as needed):
curl -L https://github.com/qjoly/talosctl-oidc/releases/latest/download/talosctl-oidc-linux-amd64 \
-o /usr/local/bin/talosctl-oidc
chmod +x /usr/local/bin/talosctl-oidcFrom source:
git clone https://github.com/qjoly/talosctl-oidc.git
cd talosctl-oidc
go build -o /usr/local/bin/talosctl-oidc .useradd --system --no-create-home --shell /sbin/nologin talosctl-oidc
mkdir -p /etc/talosctl-oidc /var/lib/talosctl-oidc /var/log/talosctl-oidc
chown talosctl-oidc:talosctl-oidc /var/lib/talosctl-oidc /var/log/talosctl-oidc
chmod 750 /var/lib/talosctl-oidc# Extract first (see Setup section above)
cp talos-ca.crt /etc/talosctl-oidc/ca.crt
cp talos-ca.key /etc/talosctl-oidc/ca.key
chown talosctl-oidc:talosctl-oidc /etc/talosctl-oidc/ca.crt /etc/talosctl-oidc/ca.key
chmod 400 /etc/talosctl-oidc/ca.key
chmod 444 /etc/talosctl-oidc/ca.crtcat > /etc/talosctl-oidc/env << 'EOF'
TALOSCTL_OIDC_CA_CERT=/etc/talosctl-oidc/ca.crt
TALOSCTL_OIDC_CA_KEY=/etc/talosctl-oidc/ca.key
TALOSCTL_OIDC_ISSUER_URL=https://idp.example.com/application/o/talos-oidc/
TALOSCTL_OIDC_CLIENT_ID=your-client-id
TALOSCTL_OIDC_ENDPOINTS=10.0.0.1,10.0.0.2
TALOSCTL_OIDC_CERT_TTL=5m
TALOSCTL_OIDC_ROLES=os:admin
TALOSCTL_OIDC_DATA_DIR=/var/lib/talosctl-oidc
TALOSCTL_OIDC_AUDIT_LOG=/var/log/talosctl-oidc/audit.log
EOF
chmod 600 /etc/talosctl-oidc/env
chown talosctl-oidc:talosctl-oidc /etc/talosctl-oidc/envThe env file contains the CA key path and OIDC client settings. Restrict access with
chmod 600.
cat > /etc/systemd/system/talosctl-oidc.service << 'EOF'
[Unit]
Description=talosctl-oidc certificate exchange server
Documentation=https://github.com/qjoly/talosctl-oidc
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=talosctl-oidc
Group=talosctl-oidc
EnvironmentFile=/etc/talosctl-oidc/env
ExecStart=/usr/local/bin/talosctl-oidc serve
Restart=on-failure
RestartSec=5s
# Hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/talosctl-oidc /var/log/talosctl-oidc
[Install]
WantedBy=multi-user.target
EOFsystemctl daemon-reload
systemctl enable --now talosctl-oidc
# Check status
systemctl status talosctl-oidc
# View logs
journalctl -u talosctl-oidc -fAllow TCP port 8443 from developer workstations only. Example with ufw:
ufw allow from <developer-subnet> to any port 8443 proto tcpOr with firewalld:
firewall-cmd --add-rich-rule='rule family="ipv4" source address="<developer-subnet>" port port="8443" protocol="tcp" accept' --permanent
firewall-cmd --reloadThe CA PEM is stable across restarts when TALOSCTL_OIDC_DATA_DIR is set:
curl -k https://<host-ip>:8443/ca > server-ca.pem
talosctl-oidc login \
--provider https://idp.example.com/application/o/talos-oidc/ \
--client-id your-client-id \
--server https://<host-ip>:8443 \
--server-ca server-ca.pem \
--context-name oidcOIDC tokens are cached in the system keychain (macOS Keychain, GNOME Keyring, KDE Wallet, or Windows Credential Manager).
| Scenario | Behavior |
|---|---|
| No cached token | Opens browser for full OIDC login |
| Valid cached token | Skips browser, exchanges cached token for new cert |
| Expired token with refresh token | Silently refreshes, no browser needed |
| Expired token without refresh token | Opens browser for full OIDC login |
| Refresh fails | Falls back to full OIDC login |
| Certificate about to expire | Proactively renews using refresh token if --watch or rerun login |
The login flow has a 5-minute timeout. If the user does not complete authentication in the browser within that window, the command exits with an error.
Use --context-name to manage credentials for different Talos clusters:
# Login to production cluster
talosctl-oidc login \
--provider https://idp.example.com/realms/talos \
--client-id talosctl \
--server https://prod-oidc-server:8443 \
--server-ca prod-ca.pem \
--context-name prod
# Login to staging cluster
talosctl-oidc login \
--provider https://idp.example.com/realms/talos \
--client-id talosctl \
--server https://staging-oidc-server:8443 \
--server-ca staging-ca.pem \
--context-name staging
# Switch between clusters
talosctl --context prod version
talosctl --context staging version
# Check status of each
talosctl-oidc status --context-name prod
talosctl-oidc status --context-name stagingThe server emits structured JSON audit events for every authentication attempt and certificate issuance. Each event is a single JSON line written to the configured output.
By default, audit events are written to stdout (mixed with regular log output). To write to a dedicated file:
export TALOSCTL_OIDC_AUDIT_LOG=/var/log/talosctl-oidc/audit.log| Event | Description |
|---|---|
auth_success |
OIDC token validated successfully |
auth_failure |
Token validation failed (invalid signature, expired, wrong audience, etc.) |
cert_issued |
Ephemeral client certificate issued to authenticated user |
cert_error |
Certificate generation failed after successful authentication |
{"timestamp":"2026-02-17T14:30:00Z","type":"cert_issued","subject":"abc123","email":"user@example.com","issuer":"https://idp.example.com/","client_ip":"192.168.1.10:52431","roles":["os:admin"],"cert_ttl":"5m0s","cert_expiry":"2026-02-17T14:35:00Z"}
{"timestamp":"2026-02-17T14:31:00Z","type":"auth_failure","client_ip":"10.0.0.5:48291","error":"token expired"}| Field | Description |
|---|---|
timestamp |
UTC timestamp of the event |
type |
Event type (auth_success, auth_failure, cert_issued, cert_error) |
subject |
OIDC subject identifier (sub claim) |
email |
User's email from the OIDC token |
issuer |
OIDC issuer URL |
client_ip |
Remote address of the client |
roles |
Talos roles assigned to the issued certificate |
cert_ttl |
Lifetime of the issued certificate |
cert_expiry |
When the issued certificate expires |
error |
Error message (for failure events) |
The server provides optional admin endpoints for monitoring server activity and inspecting active certificates. These endpoints are protected by a bearer token.
Set the TALOSCTL_OIDC_ADMIN_TOKEN environment variable to a secret value:
export TALOSCTL_OIDC_ADMIN_TOKEN=$(openssl rand -hex 32)If this variable is not set, the admin endpoints return 403 Forbidden.
Returns aggregate server statistics.
curl -s -H "Authorization: Bearer $TALOSCTL_OIDC_ADMIN_TOKEN" \
https://localhost:8443/admin/stats | jq .{
"started_at": "2026-02-17T14:00:00Z",
"uptime": "2h30m0s",
"total_certs_issued": 42,
"active_certs": 5,
"total_auth_successes": 45,
"total_auth_failures": 3,
"total_cert_errors": 0
}Returns the list of currently active (non-expired) issued certificates.
curl -s -H "Authorization: Bearer $TALOSCTL_OIDC_ADMIN_TOKEN" \
https://localhost:8443/admin/certs | jq .[
{
"subject": "abc123",
"email": "user@example.com",
"issued_at": "2026-02-17T15:30:00Z",
"expires_at": "2026-02-17T16:30:00Z",
"client_ip": "192.168.1.10:52431",
"roles": ["os:admin"],
"ttl": "1h0m0s"
}
]Expired certificates are automatically pruned from the list on each request.
The server provides a web-based admin dashboard for visual monitoring. Navigate to /admin/ in your browser:
https://localhost:8443/admin/
The dashboard provides:
- Login page with token-based authentication
- Real-time statistics showing uptime, active sessions, auth successes/failures
- Active sessions table with user details, roles, and certificate expiry
- Auto-refresh capability and logout functionality
Sessions are maintained via HTTP-only cookies with a 24-hour timeout.
Example:
- Open
https://localhost:8443/admin/in your browser - Enter your admin token when prompted
- View the dashboard with all statistics and active sessions
The API endpoints (/admin/stats and /admin/certs) continue to accept Bearer tokens for programmatic access.
The server supports optional rate limiting and IP allowlisting on the /exchange endpoint to protect against abuse and brute-force attacks.
Rate limiting is disabled by default. When enabled, it applies a per-IP sliding window limit to the /exchange endpoint.
| Setting | Description | Default |
|---|---|---|
rate_limit_requests |
Maximum requests allowed per IP per window | 0 (disabled) |
rate_limit_window |
Time window for counting requests | 1m |
Configuration via environment variables:
export TALOSCTL_OIDC_RATE_LIMIT_REQUESTS=10
export TALOSCTL_OIDC_RATE_LIMIT_WINDOW=1mConfiguration via config file:
rate_limit_requests: 10
rate_limit_window: "1m"Example: Allow 10 requests per minute per IP. Additional requests receive 429 Too Many Requests with a Retry-After header.
IP allowlisting restricts access to the /exchange endpoint to specific IP addresses or CIDR ranges.
| Setting | Description | Default |
|---|---|---|
ip_allowlist |
List of allowed IPs or CIDR ranges | (empty = allow all) |
Configuration via environment variables:
export TALOSCTL_OIDC_IP_ALLOWLIST="192.168.1.0/24,10.0.0.50,172.16.0.0/16"Configuration via config file:
ip_allowlist:
- 192.168.1.0/24
- 10.0.0.50
- 172.16.0.0/16Note: The server respects the X-Forwarded-For header for determining client IP when running behind a reverse proxy. Requests from IPs not in the allowlist receive 403 Forbidden.
config:
rateLimitRequests: 10
rateLimitWindow: "1m"
ipAllowlist:
- 192.168.1.0/24
- 10.0.0.50By default, every authenticated user receives the same static set of roles configured in roles. RBAC lets you map OIDC token claims (such as group membership) to different Talos roles dynamically, so users get only the permissions they need.
When RBAC rules are configured, the server evaluates each rule against the claims in the validated ID token. Rules are checked in order; all matching rules' roles are combined (union). If no rule matches, the user receives the static roles list as a fallback.
If roles is empty and RBAC is enabled but no rules match, the exchange request is rejected with 403 Forbidden — enforcing least-privilege by default.
Add an rbac block to your config.yaml:
# Fallback roles when no RBAC rule matches.
# Set to [] to deny access when no rule matches.
roles: []
rbac:
rules:
# Users in the 'platform-admins' OIDC group get full admin access.
- claim: groups
value: platform-admins
roles:
- os:admin
# Users in the 'developers' group get read-only access.
- claim: groups
value: developers
roles:
- os:reader
# A separate claim can also be used (e.g., a custom 'department' claim).
- claim: department
value: sre
roles:
- os:adminThe RBAC engine handles multiple ways OIDC providers encode claim values:
| Claim format | Example | Behaviour |
|---|---|---|
| String | "platform-admins" |
Exact match |
| Space-separated string | "group1 group2" |
Splits on whitespace and checks each token |
| JSON array of strings | ["platform-admins","developers"] |
Checks each element |
Each rule under rbac.rules has three fields:
| Field | Required | Description |
|---|---|---|
claim |
Yes | The OIDC claim name to inspect (e.g. groups, roles, email) |
value |
Yes | The expected value that must appear in the claim |
roles |
Yes | Talos roles to grant when this rule matches |
| Role | Description |
|---|---|
os:admin |
Full administrative access to all Talos API operations |
os:reader |
Read-only access (inspect nodes, services, config) |
os:etcd:backup |
Permission to trigger etcd backups |
| RBAC configured | Rule matched | Result |
|---|---|---|
| No | — | Static roles applied to all users |
| Yes | Yes | Matched rules' roles applied (static roles ignored) |
| Yes | No | Static roles applied as fallback |
| Yes | No | 403 Forbidden if roles: [] is also empty |
Set DEBUG=1 on the server to see per-rule evaluation in the logs:
[DEBUG] [RBAC] Evaluating 3 RBAC rules against claims
[DEBUG] [RBAC] Rule 1: checking claim 'groups' = '[platform-admins developers]' against expected value 'platform-admins'
[DEBUG] [RBAC] -> checking []interface{} with 2 items
[DEBUG] [RBAC] item 0: platform-admins
[DEBUG] [RBAC] -> MATCH at index 0
[DEBUG] [RBAC] Rule 1: MATCH! Assigning roles: [os:admin]
[DEBUG] [RBAC] RBAC evaluation complete. Assigned roles: [os:admin]
Authentik sends group membership as a JSON array of strings in the groups claim. The group name is the full display name configured in Authentik (e.g. "authentik Admins" — note the space is part of the name, not a separator):
rbac:
rules:
- claim: groups
value: "authentik Admins"
roles:
- os:adminKeycloak can send roles as an array in roles or as realm/client roles nested under realm_access.roles. Use the top-level roles claim if you configure it as a mapper, or use a custom claim name:
rbac:
rules:
- claim: roles
value: talos-admin
roles:
- os:adminDex passes group membership from upstream connectors in the groups claim as a string array. Configuration is the same as the standard example above.
Note: See the RBAC wiki page for more detailed examples and troubleshooting.
- TLS by default: The server generates a self-signed TLS certificate at startup when no TLS configuration is provided. Plain HTTP requires explicitly setting
TALOSCTL_OIDC_INSECURE=true - Ephemeral certificates: Client certificates are short-lived (default 5 minutes). Users cannot extend or forge certificates without re-authenticating
- CA key isolation: The Talos CA private key is held only by the server, never exposed to clients
- PKCE is mandatory: The OIDC flow uses S256 challenge method, protecting against authorization code interception
- OIDC tokens are stored in the system keychain, encrypted at rest by the operating system
- Token validation: The server validates ID tokens against the OIDC provider's JWKS (RS256, ES256, EdDSA) or HMAC secret (HS256)
- The callback server binds to
127.0.0.1only, preventing access from other machines - State parameter is used for CSRF protection during the OIDC flow
- Admin API is opt-in: The
/admin/*endpoints are disabled by default and require settingTALOSCTL_OIDC_ADMIN_TOKEN. The token is compared using constant-time comparison to prevent timing attacks - Audit logging provides a tamper-evident record of all authentication events for compliance and security monitoring
- Rate limiting can be configured to prevent brute-force attacks on the
/exchangeendpoint (disabled by default) - IP allowlisting can restrict access to specific networks or IP addresses (disabled by default)
Talos does not support CRL (Certificate Revocation List) or OCSP checks on client certificates. This means a compromised certificate cannot be invalidated before it expires. This is addressed through the following layered compensating controls:
| Control | Mechanism | Effect |
|---|---|---|
| Short certificate TTL | Set cert_ttl to 5–10 minutes (default: 5m) |
A compromised certificate becomes invalid within minutes without any action |
Auto-renewal via --watch |
Client renews automatically before expiry using the cached refresh token | Short TTLs are transparent to the user |
| IdP refresh token revocation | Revoke the user's refresh token in your OIDC provider | Prevents the client from obtaining a new certificate after the current one expires |
Recommended response to a compromised credential:
- Immediately revoke the user's refresh token (and/or session) in your OIDC provider. This stops the
--watchloop and any subsequentloginfrom successfully renewing the certificate. - The existing certificate remains usable for at most the configured
cert_ttl(default 5 minutes). - Optionally, lower
cert_ttlto1mor less in your server configuration for higher-sensitivity environments. Note that this increases the frequency of cert-exchange requests.
How to revoke in common IdPs:
- Authentik: Admin → Directory → Users → select user → Sessions → Revoke all, or revoke the specific token under Tokens
- Keycloak: Admin Console → Users → select user → Sessions → Log Out, or use the Token Introspection / Revocation API
- Dex: Revoke through the upstream connector (Dex itself has no revocation endpoint)
This strategy is documented as a compensating control for ISO 27001 A.9.2.6 (removal or adjustment of access rights). The combination of a short TTL and IdP revocation provides an effective access removal window bounded by the certificate TTL.
You can enable detailed internal tracing for both the client and the server by setting the DEBUG environment variable to any non-empty value.
# Debug client-side login flow
DEBUG=1 talosctl-oidc login --provider ...
# Debug server-side exchange flow
DEBUG=1 talosctl-oidc serveDebug logs include information about OIDC discovery, PKCE challenges, token response fields, keychain/file storage operations, and certificate expiry calculations.
The OIDC provider is rejecting the token request. Common causes:
- The provider is configured as a confidential client but no
--client-secretwas provided. Either switch to a public client or pass--client-secret - The Client ID is incorrect
Another process is using port 8900. Use --callback-port to pick a different port. Make sure the redirect URI in your OIDC provider matches (e.g. http://127.0.0.1:9000/callback).
The tool could not reach the OIDC provider's /.well-known/openid-configuration endpoint. Verify:
- The
--provider/--issuer-urlis correct and reachable - Your network/proxy allows access to the provider
The state parameter returned by the OIDC provider does not match what was sent. This could indicate a stale browser tab. Try the login again.
The CA certificate and key files could not be loaded. Verify:
- The files are valid PEM-encoded Ed25519 certificates/keys
- You are using the Talos API CA (from
machine.caincontrolplane.yaml), not the OS-level CA fromsecrets.yaml - The cert and key match (same public key)
On Linux, go-keyring requires a running secret service (GNOME Keyring or KDE Wallet). On headless servers:
sudo apt install gnome-keyring
eval $(gnome-keyring-daemon --start --components=secrets)
export GNOME_KEYRING_CONTROL

