Skip to content

Zero Trust Workload Identity Manager OIDC federation

Zero Trust Workload Identity Manager integrates with OpenID Connect (OIDC) by allowing a SPIRE server to act as an OIDC provider. This enables workloads to request and receive verifiable JSON Web Tokens - SPIFFE Verifiable Identity Documents (JWT-SVIDs) from the local SPIRE agent. External systems, such as cloud providers, can then use the OIDC discovery endpoint exposed by the SPIRE server to retrieve public keys.

The following providers are verified to work with SPIRE OIDC federation:

  • Azure Entra ID

  • Vault

About the Entra ID OpenID Connect

Entra ID is a cloud-based identity and access management service that centralizes user management and access control. Entra ID serves as the identify provider, verifying user identities and issuing and ID token to the application. This token has essential user information, allowing the application to confirm who the user is without managing their credentials.

Integrating Entra ID OpenID Connect (OIDC) with SPIRE provides workloads with automatic, short-lived cryptographic identities. The SPIRE-issued identities are sent to Entra ID to securely authenticate the service without any static secrets.

Configuring the external certificate for the managed OIDC discovery provider route

The managed route uses the External Route Certificate feature to set the tls.externalCertificate field to an externally managed Transfer Layer Security (TLS) secret’s name.

Prerequisites
  • You have installed Zero Trust Workload Identity Manager 0.2.0 or later.

  • You have deployed the SPIRE Server, SPIRE Agent, SPIFFEE CSI Driver, and the SPIRE OIDC Discovery Provider operands in the cluster.

  • You have installed the cert-manager Operator for Red Hat OpenShift. For more information, Installing the cert-manager Operator for Red Hat OpenShift.

  • You have created a ClusterIssuer or Issuer configured with a publicly trusted CA service. For example, an Automated Certificate Management Environment (ACME) type Issuer with the "Let’s Encrypt ACME" service. For more information, see Configuring an ACME issuer

Procedure
  1. Create a Role to provide the router service account permissions to read the referenced secret by running the following command:

    $ oc create role secret-reader \
      --verb=get,list,watch \
      --resource=secrets \
      --resource-name=$TLS_SECRET_NAME \
      -n zero-trust-workload-identity-manager
  2. Create a RoleBinding resource to bind the router service account with the newly created Role resource by running the following command:

    $ oc create rolebinding secret-reader-binding \
      --role=secret-reader \
      --serviceaccount=openshift-ingress:router \
      -n zero-trust-workload-identity-manager
  3. Configure the SpireOIDCDIscoveryProvider Custom Resource (CR) object to reference the Secret generated in the earlier step by running the following command:

    $ oc patch SpireOIDCDiscoveryProvider cluster --type=merge -p='
    spec:
      externalSecretRef: ${TLS_SECRET_NAME}
    '
Verification
  1. In the SpireOIDCDiscoveryProvider CR, check if the ManageRouteReady condition is set to True by running the following command:

    $ oc wait --for=jsonpath='{.status.conditions[?(@.type=="ManagedRouteReady")].status}'=True SpireOIDCDiscoveryProvider/cluster --timeout=120s
  2. Verify that the OIDC endpoint can be accessed securely through HTTPS by running the following command:

    $ curl https://$JWT_ISSUER_ENDPOINT/.well-known/openid-configuration
    
    {
      "issuer": "https://$JWT_ISSUER_ENDPOINT",
      "jwks_uri": "https://$JWT_ISSUER_ENDPOINT/keys",
      "authorization_endpoint": "",
      "response_types_supported": [
        "id_token"
      ],
      "subject_types_supported": [],
      "id_token_signing_alg_values_supported": [
        "RS256",
        "ES256",
        "ES384"
      ]
    }%

Disabling a managed route

If you want to fully control the behavior of exposing the OIDC Discovery Provider service, you can disable the managed Route based on your requirements.

Procedure
  • To manually configure the OIDC Discovery Provider, set managedRoute to false by running the following command:

    $ oc patch SpireOIDCDiscoveryProvider cluster --type=merge -p='
    spec:
      managedRoute: "false"

Using Entra ID with Microsoft Azure

After the Entra ID configuration is complete, you can set up Entra ID to work with Azure.

Prerequisites
  • You have configured the SPIRE OIDC Discovery Provider Route to serve the TLS certificates from a publicly trusted CA.

Procedure
  1. Log in to Azure by running the following command:

    $ az login
  2. Configure variables for your Azure subscription and tenant by running the following commands:

    $ export SUBSCRIPTION_ID=$(az account list --query "[?isDefault].id" -o tsv) 
    $ export TENANT_ID=$(az account list --query "[?isDefault].tenantId" -o tsv) 
    $ export LOCATION=centralus 
    1. Your unique subscription identifier.
    2. The ID for your Azure Active Directory instance.
    3. The Azure region where your resource is created.
  3. Define resource variable names by running the following commands:

    $ export NAME=ztwim 
    $ export RESOURCE_GROUP="${NAME}-rg" 
    $ export STORAGE_ACCOUNT="${NAME}storage" 
    $ export STORAGE_CONTAINER="${NAME}storagecontainer" 
    $ export USER_ASSIGNED_IDENTITY_NAME="${NAME}-identity" 
    1. A base name for all resources.
    2. The name of the resource group.
    3. The name for the storage account.
    4. The name for the storage container.
    5. The name for a managed identity.
  4. Create the resource group by running the following command:

    $ az group create \
      --name "${RESOURCE_GROUP}" \
      --location "${LOCATION}"

Configuring Azure blob storage

You need to create a new storage account to be used to store content.

Procedure
  1. Create a new storage account that is used to store content by running the following command:

    $ az storage account create \
      --name ${STORAGE_ACCOUNT} \
      --resource-group ${RESOURCE_GROUP} \
      --location ${LOCATION} \
      --encryption-services blob
  2. Obtain the storage ID for the newly created storage account by running the following command:

    $ export STORAGE_ACCOUNT_ID=$(az storage account show -n ${STORAGE_ACCOUNT} -g ${RESOURCE_GROUP} --query id --out tsv)
  3. Create a storage container inside the newly created storage account to provide a location to support the storage of blobs by running the following command:

    $ az storage container create \
      --account-name ${STORAGE_ACCOUNT} \
      --name ${STORAGE_CONTAINER} \
      --auth-mode login

Configuring an Azure user managed identity

You need to Create a new User Managed Identity and then obtain the Client ID of the related Service Principal associated with the User Managed Identity.

Procedure
  1. Create a new User Managed Identity and then obtain the Client ID of the related Service Principal associated with the User Managed Identity by running the following command:

    $ az identity create \
      --name ${USER_ASSIGNED_IDENTITY_NAME} \
      --resource-group ${RESOURCE_GROUP}
    $ export IDENTITY_CLIENT_ID=$(az identity show --resource-group "${RESOURCE_GROUP}" --name "${USER_ASSIGNED_IDENTITY_NAME}" --query 'clientId' -otsv)
  2. Retrieve the CLIENT_ID of an Azure user-assigned managed identity and save it as an environment variable by running the following command:

    $ export IDENTITY_CLIENT_ID=$(az identity show --resource-group "${RESOURCE_GROUP}" --name "${USER_ASSIGNED_IDENTITY_NAME}" --query 'clientId' -otsv)
  3. Associate a role with the Service Principal associated with the User Managed Identity by running the following command:

    $ az role assignment create \
      --role "Storage Blob Data Contributor" \
      --assignee "${IDENTITY_CLIENT_ID}" \
      --scope ${STORAGE_ACCOUNT_ID}

Creating the demonstration application

The demonstration application provides you a way to see if the entire system works.

Procedure

To create the demonstration application, complete the following steps:

  1. Set the application name and namespace by running the following commands:

    $ export APP_NAME=workload-app
    $ export APP_NAMESPACE=demo
  2. Create the namespace by running the following command:

    $ oc create namespace $APP_NAMESPACE
  3. Create the application Secret by running the following command:

    $ oc apply -f - << EOF
    apiVersion: v1
    kind: Secret
    metadata:
      name: $APP_NAME
      namespace: $APP_NAMESPACE
    stringData:
      AAD_AUTHORITY: https://login.microsoftonline.com/
      AZURE_AUDIENCE: "api://AzureADTokenExchange"
      AZURE_TENANT_ID: "${TENANT_ID}"
      AZURE_CLIENT_ID: "${IDENTITY_CLIENT_ID}"
      BLOB_STORE_ACCOUNT: "${STORAGE_ACCOUNT}"
      BLOB_STORE_CONTAINER: "${STORAGE_CONTAINER}"
    EOF

Deploying the workload application

Once the demonstration application has been created.

Prerequisites
  • The demonstration application has been created and deployed.

Procedure
  1. To deploy the application, copy the entire command block provided and paste it directly into your terminal. Press Enter.

    $ oc apply -f - << EOF
    apiVersion: v1
    kind: ServiceAccount
    metadata:
      name: $APP_NAME
      namespace: $APP_NAMESPACE
    ---
    kind: Deployment
    apiVersion: apps/v1
    metadata:
      name: $APP_NAME
      namespace: $APP_NAMESPACE
    spec:
      selector:
        matchLabels:
          app: $APP_NAME
      template:
        metadata:
          labels:
            app: $APP_NAME
            deployment: $APP_NAME
        spec:
          serviceAccountName: $APP_NAME
          containers:
            - name: $APP_NAME
              image: "registry.redhat.io/ubi9/python-311:latest"
              command:
                - /bin/bash
                - "-c"
                - |
                  #!/bin/bash
                  pip install spiffe azure-cli
    
                  cat << EOF > /opt/app-root/src/get-spiffe-token.py
                  #!/opt/app-root/bin/python
                  from spiffe import JwtSource
                  import argparse
                  parser = argparse.ArgumentParser(description='Retrieve SPIFFE Token.')
                  parser.add_argument("-a", "--audience", help="The audience to include in the token", required=True)
                  args = parser.parse_args()
                  with JwtSource() as source:
                    jwt_svid = source.fetch_svid(audience={args.audience})
                    print(jwt_svid.token)
                  EOF
    
                  chmod +x /opt/app-root/src/get-spiffe-token.py
                  while true; do sleep 10; done
              envFrom:
              - secretRef:
                  name: $APP_NAME
              env:
                - name: SPIFFE_ENDPOINT_SOCKET
                  value: unix:///run/spire/sockets/spire-agent.sock
              securityContext:
                allowPrivilegeEscalation: false
                capabilities:
                  drop:
                    - ALL
                readOnlyRootFilesystem: false
                runAsNonRoot: true
                seccompProfile:
                  type: RuntimeDefault
              ports:
                - containerPort: 8080
                  protocol: TCP
              volumeMounts:
                - name: spiffe-workload-api
                  mountPath: /run/spire/sockets
                  readOnly: true
          volumes:
            - name: spiffe-workload-api
              csi:
                driver: csi.spiffe.io
                readOnly: true
    EOF
Verification
  1. Ensure that the workload-app pod is running successfully by running the following command:

    $ oc get pods -n $APP_NAMESPACE
    Example output
    NAME                             READY     STATUS      RESTARTS      AGE
    workload-app-5f8b9d685b-abcde    1/1       Running     0             60s
  2. Retrieve the SPIFFE JWT Token (SVID-JWT):

    1. Get the pod name dynamically by running the following command:

      $ POD_NAME=$(oc get pods -n $APP_NAMESPACE -l app=$APP_NAME -o jsonpath='{.items[0].metadata.name}')
    2. Run the script inside the pod by running the following command:

      $ oc exec -it $POD_NAME -n $APP_NAMESPACE -- \
        /opt/app-root/src/get-spiffe-token.py -a "api://AzureADTokenExchange"

Configuring Azure with the SPIFFE identity federation

You can configure Azure with the SPIFFE identity federation to enable password-free and automated authentication to the demonstration application.

Procedure
  • Federate the identities between the User Managed Identity and the SPIFFE identity associated with the workload application by running the following command:

    $ az identity federated-credential create \
     --name ${NAME} \
     --identity-name ${USER_ASSIGNED_IDENTITY_NAME} \
     --resource-group ${RESOURCE_GROUP} \
     --issuer https://$JWT_ISSUER_ENDPOINT \
     --subject spiffe://$APP_DOMAIN/ns/$APP_NAMESPACE/sa/$APP_NAME \
     --audience api://AzureADTokenExchange

Verifying that the application workload can access the content in the Azure Blob Storage

You can check if the application workload can access the Azure Blob Storage.

Prerequisites
  • An Azure Blob Storage has been created.

Procedure
  1. Retrieve a JWT token from the SPIFFE Workload API by running the following command:

    $ oc rsh -n $APP_NAMESPACE deployment/$APP_NAME
  2. Create and export an environment variable named TOKEN by running the following command:

    $ export TOKEN=$(/opt/app-root/src/get-spiffe-token.py --audience=$AZURE_AUDIENCE)
  3. Log in to Azure CLI included within the pod by running the following command:

    $ az login --service-principal \
      -t ${AZURE_TENANT_ID} \
      -u ${AZURE_CLIENT_ID} \
      --federated-token ${TOKEN}
  4. Create a new file with the application workload pod and upload the file to the Blob Storage by running the following command:

    $ echo “Hello from OpenShift” > openshift-spire-federated-identities.txt
  5. Upload a file to the Azure Blog Storage by running the following command:

    $ az storage blob upload \
      --account-name ${BLOB_STORE_ACCOUNT} \
      --container-name ${BLOB_STORE_CONTAINER} \
      --name openshift-spire-federated-identities.txt \
      --file openshift-spire-federated-identities.txt \
      --auth-mode login
Verification
  • Confirm the file uploaded successfully by listing the files contained by running the following command:

    $ az storage blob list \
      --account-name ${BLOB_STORE_ACCOUNT} \
      --container-name ${BLOB_STORE_CONTAINER} \
      --auth-mode login \
      -o table

About Vault OpenID Connect

Vault OpenID Connect (OIDC) with SPIRE creates a secure authentication method where Vault uses SPIRE as a trusted OIDC provider. A workload requests a JWT-SVID from its local SPIRE Agent, which has a unique SPIFFE ID. The workload then presents this token to Vault, and Vault validates it against the public keys on the SPIRE Server. If all conditions are met, Vault issues a short-lived Vault token to the workload which the workload can now use to access secrets and perform actions within Vault.

Installing Vault

Before Vault is used as an OIDC, you need to install Vault.

Prerequisites
  • Configure a route. For more information, see Configuring routes

  • Helm is installed.

  • A command-line JSON processor for easily reading the output from the Vault API.

  • A HashiCorp Helm repository is added.

Procedure
  1. Create the vault-helm-value.yaml file.

    global:
      enabled: true
      openshift: true 
      tlsDisable: true 
    injector:
      enabled: false
    server:
      ui:
        enabled: true
      image:
        repository: docker.io/hashicorp/vault
        tag: "1.19.0"
      dataStorage:
        enabled: true 
        size: 1Gi
      standalone:
        enabled: true 
        config: |
          listener "tcp" {
            tls_disable = 1 
            address = "[::]:8200"
            cluster_address = "[::]:8201"
          }
          storage "file" {
            path = "/vault/data"
          }
      extraEnvironmentVars: {}
    1. Optimizes the deployment for OpenShift-specific security contexts.
    2. Disables TLS for Kubernetes objects created by the chart.
    3. Creates a 1Gi persistent volume to store Vault data.
    4. Deploys a single Vault pod.
    5. Tells the Vault server to not use TLS.
  2. Run the helm install command:

    $ helm install vault hashicorp/vault \
      --create-namespace -n vault \
      --values ./vault-helm-value.yaml
  3. Expose the Vault service by running the following command:

    $ oc expose service vault -n vault
  4. Set the VAULT_ADDR environment variable to retrieve the hostname from the new route and then export it by running the following command:

    $ export VAULT_ADDR="http://$(oc get route vault -n vault -o jsonpath='{.spec.host}')"

    Note

    http:// is prepended because TLS is disabled.

Verification
  • To ensure your Vault instance is running, run the following command:

    $ curl -s $VAULT_ADDR/v1/sys/health | jq
    Example output
    {
      "initialized": true,
      "sealed": true,
      "standby": true,
      "performance_standby": false,
      "replication_performance_mode": "disabled",
      "replication_dr_mode": "disabled",
      "server_time_utc": 1663786574,
      "version": "1.19.0",
      "cluster_name": "vault-cluster-a1b2c3d4",
      "cluster_id": "5e6f7a8b-9c0d-1e2f-3a4b-5c6d7e8f9a0b"
    }

Initializing and unsealing Vault

A newly installed Vault is sealed. This means that the primary encryption key, which protects all other encryption keys, is not loaded into the server memory upon startup. You need to initialize Vault to unseal it.

The steps to initialize a Vault server are:

  1. Initialize and unseal Vault

  2. Enable the key-value (KV) secrets engine and store a test secret

  3. Configure JSON Web Token (JWT) authentication with SPIRE

  4. Deploy a demonstration application

  5. Authenticate and retrieve the secret

Prerequisites
  • Ensure that Vault is running.

  • Ensure that Vault is not initialized. You can only initialize a Vault server once.

Procedure
  1. Open a remote shell into the vault pod by running the following command:

    $ oc rsh -n vault statefulset/vault
  2. Initialize Vault to get your unseal key and root token by running the following command:

    $ vault operator init -key-shares=1 -key-threshold=1 -format=json
  3. Export the unseal key and root token you received from the earlier command by running the following commands:

    $ export UNSEAL_KEY=<Your-Unseal-Key>
    $ export ROOT_TOKEN=<Your-Root-Token>
  4. Unseal Vault using your unseal key by running the following command:

    $ vault operator unseal -format=json $UNSEAL_KEY
  5. Exit the pod by entering exit.

Verification
  • To verify that the Vault pod is ready, run the following command:

    $ oc get pod -n vault
    Example output
    NAME        READY        STATUS      RESTARTS     AGE
    vault-0     1/1          Running     0            65d

Enabling the key-value secrets engine and store a test secret

You enable the key-value secrets engine to establish a secure, centralized location for managing credentials.

Prerequisites
  • Make sure that Vault is initialized and unsealed.

Procedure
  1. Open another shell session in the Vault pod by running the following command:

    $ oc rsh -n vault statefulset/vault
  2. Export your root token again within this new session and log in by running the following command:

    $ export ROOT_TOKEN=<Your-Root-Token>
    $ vault login "${ROOT_TOKEN}"
  3. Enable the KV secrets engine at the secret/ path and create a test secret by running the following commands:

    $ export NAME=ztwim
    $ vault secrets enable -path=secret kv
    $ vault kv put secret/$NAME version=v0.1.0
Verification
  • To verify that the secret is stored correctly, run the following command:

    $ vault kv get secret/$NAME

Configuring JSON Web Token authentication with SPIRE

You need to set up JSON Web Token (JWT) authentication so your applications can securely log in to Vault by using SPIFFE identities.

Prerequisites
  • Make sure that Vault is initialized and unsealed.

  • Ensure that a test secret is stored in the key-value secrets engine.

Procedure
  1. On your local machine, retrieve the SPIRE Certificate Authority (CA) bundle and save it to a file by running the following command:

    $ oc get cm -n zero-trust-workload-identity-manager spire-bundle -o jsonpath='{ .data.bundle\.crt }' > oidc_provider_ca.pem
  2. Back in the Vault pod shell, create a temporary file and paste the contents of oidc_provider_ca.pem into it by running the following command:

    $ cat << EOF > /tmp/oidc_provider_ca.pem
    -----BEGIN CERTIFICATE-----
    <Paste-Your-Certificate-Content-Here>
    -----END CERTIFICATE-----
    EOF
  3. Set up the necessary environment variables for the JWT configuration by running the following commands:

    $ export APP_DOMAIN=<Your-App-Domain>
    $ export JWT_ISSUER_ENDPOINT="oidc-discovery.$APP_DOMAIN"
    $ export OIDC_URL="https://$JWT_ISSUER_ENDPOINT"
    $ export OIDC_CA_PEM="$(cat /tmp/oidc_provider_ca.pem)"
  4. Crate a new environment variable by running the following command:

    $ export ROLE="${NAME}-role"
  5. Enable the JWT authentication method by running the following command:

    $ vault auth enable jwt
  6. Configure you ODIC authentication method by running the following command:

    $ vault write auth/jwt/config \
      oidc_discovery_url=$OIDC_URL \
      oidc_discovery_ca_pem="$OIDC_CA_PEM" \
      default_role=$ROLE
  7. Create a policy named ztwim-policy by running the following command:

    $ export POLICY="${NAME}-policy"
  8. Grant read access to the secret you created earlier by running the following command:

    $ vault policy write $POLICY -<<EOF
    path "secret/$NAME" {
        capabilities = ["read"]
    }
    EOF
  9. Create the following environment variables by running the following commands:

    $ export APP_NAME=client
    $ export APP_NAMESPACE=demo
    $ export AUDIENCE=$APP_NAME
  10. Create a JWT role that binds the policy to workload with a specific SPIFFE ID by running the following command:

    $ vault write auth/jwt/role/$ROLE -<<EOF
    {
      "role_type": "jwt",
      "user_claim": "sub",
      "bound_audiences": "$AUDIENCE",
      "bound_claims_type": "glob",
      "bound_claims": {
        "sub": "spiffe://$APP_DOMAIN/ns/$APP_NAMESPACE/sa/$APP_NAME"
      },
      "token_ttl": "24h",
      "token_policies": "$POLICY"
    }
    EOF

Deploying a demonstration application

When you deploy a demonstration application, you create a simple client application that uses its SPIFFE identity to authenticate with Vault.

Procedure
  1. On your local machine, set the environment variables for your application by running the following commands:

    $ export APP_NAME=client
    $ export APP_NAMESPACE=demo
    $ export AUDIENCE=$APP_NAME
  2. Apply the Kubernetes manifest to create the namespace, service account, and deployment for the demo app by running the following command. This deployment mounts the SPIFFE CSI driver socket.

    $ oc apply -f - <<EOF
    # ... (paste the full YAML from your provided code here) ...
    EOF
Verification
  • Verify that the client deployment is ready by running the following command:

    $ oc get deploy -n $APP_NAMESPACE
    Example output
    NAME             READY        UP-TO-DATE      AVAILABLE     AGE
    frontend-app     2/2          2               2             120d
    backend-api      3/3          3               3             120d

Authenticating and retrieving the secret

You use the demonstration application to fetch a JWT token from the SPIFFE Workload API and use it to log in to Vault and retrieve the secret.

Procedure
  1. Fetch a JWT-SVID by running the following command inside the running client pod:

    $ oc -n $APP_NAMESPACE exec -it $(oc get pod -o=jsonpath='{.items[*].metadata.name}' -l app=$APP_NAME -n $APP_NAMESPACE) \
      -- /opt/spire/bin/spire-agent api fetch jwt \
      -socketPath /run/spire/sockets/spire-agent.sock \
      -audience $AUDIENCE
  2. Copy the token from the output and export it as an environment variable on your local machine by running the following command:

    $ export IDENTITY_TOKEN=<Your-JWT-Token>
  3. Crate a new environment variable by running the following command:

    $ export ROLE="${NAME}-role"
  4. Use curl to send the JWT token to the Vault login endpoint to get a Vault client token by running the following command:

    $ VAULT_TOKEN=$(curl -s --request POST --data '{ "jwt": "'"${IDENTITY_TOKEN}"'", "role": "'"${ROLE}"'"}' "${VAULT_ADDR}"/v1/auth/jwt/login | jq -r '.auth.client_token')
Verification
  • Use the newly acquired Vault token to read the secret from the KV store by running the following command:

    $ curl -s -H "X-Vault-Token: $VAULT_TOKEN" $VAULT_ADDR/v1/secret/$NAME | jq

    You should see the contents of the secret ("version": "v0.1.0") in the output, confirming the entire workflow is successful