Authenticate an AI Agent to Secrets Manager

Use SPIRE JWT SVIDs to authenticate an AI agent to CyberArk Secrets Manager.

Introduction

30 Second Summary

AI agents need secrets like API keys and passwords to interact with the services they depend on. Hardcoding those credentials is one of the biggest security risks in modern infrastructure, because a single leaked key can give attackers the same access your agent has.

In this project, you will build an end-to-end identity pipeline that lets an AI agent prove who it is using a short-lived cryptographic token from SPIRE, then use that identity to retrieve secrets from CyberArk Secrets Manager. No static credentials involved.

What You'll Build

You will connect an open-source SPIFFE/SPIRE identity system to CyberArk Secrets Manager SaaS so that an AI agent workload can authenticate using a JWT SVID and retrieve a secret through the REST API.

By the end of this project, you'll have:

  • A running local SPIRE server and agent with a registered AI agent workload that receives short-lived JWT SVIDs.
  • A publicly accessible JWKS endpoint served by the SPIRE OIDC Discovery Provider and exposed through a Cloudflare Tunnel.
  • A JWT authenticator in CyberArk Secrets Manager that trusts SPIRE-issued JWTs, with a working end-to-end flow where your AI agent authenticates and retrieves a secret.
  • Secret Mission: automate the entire JWT fetch, authenticate, and secret retrieval flow in a single shell script that handles token refresh.

Are there any prerequisites?

This project requires a CyberArk Secrets Manager SaaS tenant with admin privileges (membership in the Conjur_Cloud_Admins or Authn_Admins group). You also need a Cloudflare account with a DNS zone configured.

Familiarity with SPIFFE/SPIRE concepts and CyberArk Secrets Manager basics is recommended before starting.

Before We Start

Before We Start...

Install Prerequisites and Build SPIRE

Before you can issue cryptographic identities or connect anything to CyberArk Secrets Manager, you need the right tools on your machine. This step gets your local development environment ready.

You will install three CLI tools via Homebrew. Then you will clone the SPIRE source code and build the binaries yourself, since pre-built macOS binaries are not available.

In this step, get ready to:
  • Install Go, cloudflared, and the CyberArk Secrets Manager CLI.
  • Clone the SPIRE repository and build the server and agent binaries from source.
  • Build the SPIRE OIDC Discovery Provider binary.
Install Go, cloudflared, and the Secrets Manager CLI

This project depends on three CLI tools. Go compiles the SPIRE source code. cloudflared creates a Cloudflare Tunnel to expose your local endpoints to the internet. The Secrets Manager CLI (conjur) manages policies and authenticator configuration.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal and press Enter to open it.
  • Check if Go is already installed by running:
go version

✔️ I see a version number

Go is already installed. Skip ahead to the cloudflared check below.

ⓧ Command not found

  • Install Go via Homebrew by running:
brew install go
  • Verify the installation by running:
go version

You should see a version number like go1.23.x printed to the terminal.

Still seeing command not found?

Make sure Homebrew is installed. Run brew --version to check. If Homebrew itself is missing, install it from brew.sh first.

Help me troubleshoot my Go installation

  • Check if cloudflared is already installed by running:
cloudflared --version

✔️ I see a version number

cloudflared is ready. Skip ahead to the Secrets Manager CLI install below.

ⓧ Command not found

  • Install cloudflared via Homebrew by running:
brew install cloudflared
  • Verify the installation by running:
cloudflared --version

You should see a version number and build date printed to the terminal.

Now install the CyberArk Secrets Manager CLI. This tool is distributed through CyberArk's own Homebrew tap, so the install command adds that tap first.

  • Install the Secrets Manager CLI by running:
brew tap cyberark/tools && brew update && brew install conjur-cli

What does this command do?

brew tap cyberark/tools registers CyberArk's formula repository with Homebrew. brew update refreshes the package list so Homebrew can find the new formula.

brew install conjur-cli then installs the CLI itself. This gives you the conjur command for managing Secrets Manager policies, variables, and authenticators.

  • Verify the CLI installed correctly by running:
conjur --version

You should see a version number (e.g. 9.x) printed to the terminal.

conjur command not found?

Make sure the brew tap step completed without errors. Look for a line like Tapped cyberark/tools in the output. If the tap failed, re-run the full command.

Help me troubleshoot the conjur CLI installation

Clone the SPIRE repository and build the binaries

SPIRE does not publish pre-built binaries for macOS. You need to clone the source code and compile it yourself using Go.

Why build from source?

SPIRE is the open-source reference implementation of the SPIFFE specification. The official SPIRE quickstart for macOS requires cloning and building from source because the project only distributes Linux binaries.

Building locally also ensures you get a binary compiled for your exact architecture (Intel or Apple Silicon).

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Clone the SPIRE repository at tag v1.14.5 by running:
git clone --single-branch --branch v1.14.5 https://github.com/spiffe/spire.git

What does --single-branch do?

The --single-branch flag tells Git to download only the tagged release you specified. This saves time and disk space by skipping the full history of every branch.

  • Confirm the clone succeeded by running:
ls

You should see a spire folder in the output.

  • Move into the cloned repository by running:
cd spire

Now you will compile the two core SPIRE binaries. The build process downloads Go dependencies and compiles the source code, so it may take a few minutes.

  • Create a bin/ directory and build the SPIRE server and agent by running these commands:
mkdir -p bin
go build -o bin/spire-server ./cmd/spire-server
go build -o bin/spire-agent ./cmd/spire-agent

What do these commands do?

mkdir -p bin creates a bin/ directory to hold your compiled binaries. The -p flag prevents an error if the directory already exists.

go build -o bin/spire-server ./cmd/spire-server compiles the SPIRE server source code and places the resulting binary at bin/spire-server. The second command does the same for the SPIRE agent.

  • Confirm both binaries were created by running:
ls bin/

You should see spire-agent and spire-server listed in the output.

Build failed with an error?

If you see a message about missing modules, make sure you are inside the spire directory (the folder that git clone created on your Desktop). Run pwd to confirm your current location.

If the error mentions a Go version mismatch, check that your Go version is 1.13 or higher by running go version.

Help me troubleshoot the SPIRE build

Build the OIDC Discovery Provider

The SPIRE repository includes a helper component called the OIDC Discovery Provider. It serves a JWKS endpoint that Secrets Manager will use later to validate JWT SVIDs issued by your SPIRE server.

  • Build the OIDC Discovery Provider by running:
go build -o bin/oidc-discovery-provider ./support/oidc-discovery-provider

What is the OIDC Discovery Provider?

CyberArk Secrets Manager needs to reach a public URL to download the signing keys that verify your SPIRE JWTs. The OIDC Discovery Provider is a lightweight web server that exposes these keys at a standard /keys endpoint and a discovery document at /.well-known/openid-configuration.

You will configure and start this provider in a later step. For now, you just need the compiled binary.

  • Confirm all three binaries exist by running:
ls bin/

You should see oidc-discovery-provider, spire-agent, and spire-server listed in the output.

Only seeing two binaries?

If oidc-discovery-provider is missing, check that you ran the build command from the spire directory (the repository root). The path ./support/oidc-discovery-provider is relative to that root.

Help me figure out why my OIDC Discovery Provider build failed

✔️ I see all three binaries

All three binaries are compiled and ready. Make sure you have saved nothing else to close out this step.

ⓧ I'm missing one or more binaries

Double check that you ran each build command from inside the spire directory on your Desktop. Run pwd to confirm your current path shows ~/Desktop/spire.

Re-run the missing build command. Here are all three for reference:

mkdir -p bin
go build -o bin/spire-server ./cmd/spire-server
go build -o bin/spire-agent ./cmd/spire-agent
go build -o bin/oidc-discovery-provider ./support/oidc-discovery-provider

Your toolchain is ready and all three SPIRE binaries are compiled. Next up, you will configure and start the SPIRE server and agent to begin issuing identities.

Start the SPIRE Server and Agent

In the previous step, you installed all the prerequisites and built the SPIRE binaries from source. Now it is time to bring those binaries to life by running the SPIRE server and agent.

The SPIRE server is the central authority that manages identities for your trust domain. The SPIRE agent runs on the same machine as your workloads and fetches identities on their behalf.

In this step, get ready to:
  • Configure and start the SPIRE server with a local trust domain.
  • Start the SPIRE agent and connect it to the server using a join token.
  • Register an AI agent workload with a unique SPIFFE ID.
Configure and start the SPIRE server

Before the SPIRE server can start, it needs a configuration file and a dummy upstream CA certificate. The upstream CA is what SPIRE uses to sign intermediate certificates for your trust domain.

  • Create the directory structure for your SPIRE configuration and data by running this command:
mkdir -p conf/server conf/agent .data/server .data/agent
  • Confirm the directories exist by running:
ls conf/ .data/

You should see server and agent listed under both conf/ and .data/.

  • Generate a self-signed dummy upstream CA certificate and key by running this command:
openssl req -x509 -newkey rsa:2048 \
  -keyout conf/server/dummy_upstream_ca.key \
  -out conf/server/dummy_upstream_ca.crt \
  -days 365 -nodes -subj "/CN=SPIRE Dummy Upstream CA"

What does this command do?

This generates a self-signed X.509 certificate and private key using openssl. SPIRE's disk-based UpstreamAuthority plugin uses these files to sign the intermediate certificates it issues.

The -nodes flag means the key is not encrypted with a passphrase, which keeps things simple for local development. In production, you would use a real PKI or a hardware-backed key manager.

  • Verify the CA files were created by running:
ls conf/server/

You should see dummy_upstream_ca.crt and dummy_upstream_ca.key in the output.

openssl command not found?

macOS ships with LibreSSL, which provides the openssl command. If you see a "command not found" error, install OpenSSL via Homebrew by running brew install openssl.

Help me install openssl on my machine

  • Create the SPIRE server configuration file by pasting the following into a new file at conf/server/server.conf:
# Main server configuration block
server {
    bind_address = "127.0.0.1"
    bind_port = "8081"
    socket_path = "/tmp/spire-server/private/api.sock"
    trust_domain = "example.org"
    data_dir = "./.data/server"
    log_level = "INFO"
    default_jwt_svid_ttl = "5m"
}

plugins {
    # Persistent storage using SQLite
    DataStore "sql" {
        plugin_data {
            database_type = "sqlite3"
            connection_string = "./.data/server/datastore.sqlite3"
        }
    }

    # Agent attestation via one-time join tokens
    NodeAttestor "join_token" {
        plugin_data {}
    }

    # Signing keys stored on disk
    KeyManager "disk" {
        plugin_data {
            keys_path = "./.data/server/keys.json"
        }
    }

    # Self-signed upstream CA for issuing intermediate certs
    UpstreamAuthority "disk" {
        plugin_data {
            key_file_path = "./conf/server/dummy_upstream_ca.key"
            cert_file_path = "./conf/server/dummy_upstream_ca.crt"
        }
    }
}

What does this configuration do?

  • trust_domain sets the SPIFFE trust domain to example.org. All identities issued by this server will be under spiffe://example.org/.
  • bind_address and bind_port tell the server to listen on localhost port 8081.
  • default_jwt_svid_ttl sets the lifetime of JWT SVIDs to 5 minutes. Short-lived tokens are a core zero-trust principle.
  • The NodeAttestor "join_token" plugin lets agents prove their identity using a one-time token generated by the server.
  • The UpstreamAuthority "disk" plugin points to the dummy CA files you just generated.
  • Save conf/server/server.conf.
  • Start the SPIRE server as a background process by running:
bin/spire-server run -config conf/server/server.conf &

What does the & do?

The & at the end runs the server in the background so you can keep using this terminal. You may see some log output appear. Press Enter to get your command prompt back.

  • Wait a few seconds for the server to initialize, then verify it is healthy by running:
bin/spire-server healthcheck

You should see Server is healthy. in the output.

Server is not healthy?

If you see an error like connection refused, the server may not have finished starting yet. Wait 5 seconds and try the healthcheck again.

If you see a socket-related error, check that socket_path in your server.conf matches /tmp/spire-server/private/api.sock.

Help me troubleshoot my SPIRE server startup

✔️ Awesome, I've got everything!

Great. Double check you have saved conf/server/server.conf and that the server healthcheck returned healthy.

ⓧ I'd like to double check the full code

Here is the complete conf/server/server.conf file:

server {
    bind_address = "127.0.0.1"
    bind_port = "8081"
    socket_path = "/tmp/spire-server/private/api.sock"
    trust_domain = "example.org"
    data_dir = "./.data/server"
    log_level = "INFO"
    default_jwt_svid_ttl = "5m"
}

plugins {
    DataStore "sql" {
        plugin_data {
            database_type = "sqlite3"
            connection_string = "./.data/server/datastore.sqlite3"
        }
    }

    NodeAttestor "join_token" {
        plugin_data {}
    }

    KeyManager "disk" {
        plugin_data {
            keys_path = "./.data/server/keys.json"
        }
    }

    UpstreamAuthority "disk" {
        plugin_data {
            key_file_path = "./conf/server/dummy_upstream_ca.key"
            cert_file_path = "./conf/server/dummy_upstream_ca.crt"
        }
    }
}
Generate a join token and start the SPIRE agent

The SPIRE agent needs to prove its identity to the server before it can start issuing workload identities. You will generate a one-time join token on the server and pass it to the agent at startup.

  • Generate a join token for the agent by running:
bin/spire-server token generate -spiffeID spiffe://example.org/myagent

What does this command do?

This creates a one-time token that the agent will use to attest itself to the server. The -spiffeID flag gives the agent a human-readable SPIFFE ID of spiffe://example.org/myagent.

The command outputs a token string. Copy this token and save it somewhere accessible. You will need it in a moment.

  • Export the server's trust bundle so the agent can verify the server's identity by running:
bin/spire-server bundle show > conf/agent/bootstrap.crt

Why do we need a bootstrap certificate?

The agent needs to trust the server before connecting to it. The bundle show command exports the server's trust bundle in PEM format. The agent uses this as its bootstrap certificate to verify the server's identity during the initial handshake.

  • Create the SPIRE agent configuration file by pasting the following into a new file at conf/agent/agent.conf:
# Main agent configuration block
agent {
    data_dir = "./.data/agent"
    log_level = "INFO"
    server_address = "127.0.0.1"
    server_port = "8081"
    socket_path = "/tmp/spire-agent/public/api.sock"
    trust_bundle_path = "./conf/agent/bootstrap.crt"
    trust_domain = "example.org"
}

plugins {
    # Match the server's join_token NodeAttestor
    NodeAttestor "join_token" {
        plugin_data {}
    }

    # Persist the agent's private key to disk
    KeyManager "disk" {
        plugin_data {
            directory = "./.data/agent"
        }
    }

    # Identify workloads by Unix process attributes
    WorkloadAttestor "unix" {
        plugin_data {}
    }
}

What does this configuration do?

  • server_address and server_port point the agent to the SPIRE server running on 127.0.0.1:8081.
  • trust_bundle_path tells the agent where to find the bootstrap certificate you just exported.
  • The WorkloadAttestor "unix" plugin lets the agent identify workloads by Unix process attributes like UID. This is how the agent will match your AI agent process to its registered SPIFFE ID.
  • Save conf/agent/agent.conf.
  • Start the SPIRE agent with the join token you copied earlier by running:
bin/spire-agent run -config conf/agent/agent.conf -joinToken [[JOIN_TOKEN="your join token from the previous command"]] &
  • Wait a few seconds for the agent to connect to the server, then verify it is healthy by running:
bin/spire-agent healthcheck

You should see Agent is healthy. in the output.

Agent is not healthy?

If you see a connection error, make sure the SPIRE server is still running. Run bin/spire-server healthcheck to confirm.

If you see a "join token has already been used" error, generate a new token with bin/spire-server token generate -spiffeID spiffe://example.org/myagent and restart the agent.

Help me troubleshoot my SPIRE agent connection

✔️ Awesome, I've got everything!

Great. Double check you have saved conf/agent/agent.conf and that the agent healthcheck returned healthy.

ⓧ I'd like to double check the full code

Here is the complete conf/agent/agent.conf file:

agent {
    data_dir = "./.data/agent"
    log_level = "INFO"
    server_address = "127.0.0.1"
    server_port = "8081"
    socket_path = "/tmp/spire-agent/public/api.sock"
    trust_bundle_path = "./conf/agent/bootstrap.crt"
    trust_domain = "example.org"
}

plugins {
    NodeAttestor "join_token" {
        plugin_data {}
    }

    KeyManager "disk" {
        plugin_data {
            directory = "./.data/agent"
        }
    }

    WorkloadAttestor "unix" {
        plugin_data {}
    }
}
Register the AI agent workload

The server and agent are running, but no workloads are registered yet. You need to tell SPIRE which processes are allowed to request identities and what SPIFFE ID each one should receive.

  • Register the AI agent workload with the SPIRE server by running:
bin/spire-server entry create \
  -parentID spiffe://example.org/myagent \
  -spiffeID spiffe://example.org/ai-agent \
  -selector unix:uid:$(id -u)

What does this command do?

  • -parentID links this workload to the agent you registered earlier (spiffe://example.org/myagent).
  • -spiffeID assigns the workload the identity spiffe://example.org/ai-agent. This is the identity CyberArk Secrets Manager will use to authenticate the workload.
  • -selector unix:uid:$(id -u) matches any process running as your current user's UID. The $(id -u) part dynamically inserts your user ID.

You should see output confirming the entry was created, including an Entry ID.

  • Verify the registration entry by running:
bin/spire-server entry show

You should see your registered entry with SPIFFE ID spiffe://example.org/ai-agent and the unix:uid selector matching your UID.

Entry not showing up?

Make sure the SPIRE server is still running by checking bin/spire-server healthcheck. If the entry create command returned an error, double check that the -parentID matches spiffe://example.org/myagent exactly.

Help me troubleshoot my SPIRE workload registration

Your SPIRE infrastructure is running and your AI agent workload is registered. Next up, you will fetch your first JWT SVID and inspect what is inside it.

Fetch a JWT SVID and Inspect It

In the previous step, you registered your AI agent workload with SPIRE and confirmed it shows up in the entry list. The identity is defined. Now it is time to see it in action.

The Workload API on the SPIRE agent can issue short-lived JWT SVIDs to any workload that proves its identity through local selectors. In this step, you will fetch one of these tokens and decode it to see the claims that CyberArk Secrets Manager will use to authenticate your AI agent.

In this step, get ready to:
  • Fetch a JWT SVID for your AI agent workload from the SPIRE agent.
  • Decode the token and inspect its claims to understand how they map to Secrets Manager configuration.
Fetch the JWT SVID from SPIRE

Your SPIRE agent already knows your AI agent workload by its Unix UID selector. When you ask for a JWT SVID, the agent checks the calling process against its registered entries and mints a short-lived token.

You will need jq to parse the JSON output from the SPIRE agent. This is a lightweight command-line JSON processor.

  • Install jq by running this command:
brew install jq

Seeing 'already installed'?

That is perfectly fine. If Homebrew tells you jq is already installed, you are good to go.

If the command fails entirely, ask for help troubleshooting your Homebrew installation.

  • Fetch a JWT SVID for your AI agent and save it to a shell variable by running this command:
JWT_SVID=$(bin/spire-agent api fetch jwt \
  -audience conjur \
  -socketPath /tmp/spire-agent/public/api.sock \
  -output json | jq -r '.[] | .svids[] | .svid')

What does this command do?

  • -audience conjur tells the SPIRE agent to include conjur in the aud claim of the JWT. This is the audience that CyberArk Secrets Manager will check when validating the token.
  • -socketPath points to the SPIRE agent's Workload API socket so the command can communicate with the agent.
  • -output json returns the response in JSON format so jq can extract just the raw token string.
  • Verify the token was captured by printing it:
echo "$JWT_SVID"

You should see a long string of characters separated by two dots. This is a standard JWT in the format header.payload.signature.

Seeing an empty output?

If the variable is empty, the SPIRE agent could not match your process to a registered workload. Double-check that you are running the command from the same user account (UID) that you registered the workload with in Step 2.

Confirm the agent is still healthy by running bin/spire-agent healthcheck -socketPath /tmp/spire-agent/public/api.sock.

If the agent is down, restart it using the same join token and startup command from the previous step. Help me troubleshoot my empty JWT SVID.

Decode the JWT and inspect the claims

A JWT SVID has three base64-encoded sections. The middle section is the payload, containing the identity claims that Secrets Manager will read during authentication.

  • Decode the JWT payload by running this command:
echo "$JWT_SVID" | cut -d. -f2 | tr '_-' '/+' | base64 -D 2>/dev/null | jq .

What does this command do?

  • cut -d. -f2 extracts the second section (the payload) of the JWT by splitting on the dot delimiter.
  • tr '_-' '/+' converts base64url encoding to standard base64 encoding so base64 can decode it.
  • base64 -D decodes the base64 string into the raw JSON payload.
  • jq . pretty-prints the JSON so the claims are easy to read.

You should see a JSON object containing several claims. Here is an example of what it looks like:

{
  "aud": [
    "conjur"
  ],
  "exp": 1714400000,
  "iat": 1714399700,
  "iss": "https://example.org",
  "sub": "spiffe://example.org/ai-agent"
}

What do these claims mean?

Each claim in the JWT SVID plays a specific role in the authentication flow with CyberArk Secrets Manager.

  • sub is the SPIFFE ID of your AI agent workload (spiffe://example.org/ai-agent). Secrets Manager uses this to look up the matching host identity.
  • aud is the intended audience (conjur). This ensures the token was minted specifically for Secrets Manager.
  • iss is the issuer URL. This identifies the SPIRE server that created the token. Secrets Manager checks this against the configured issuer variable.
  • exp and iat are the expiration and issued-at timestamps. The default TTL is 5 minutes, so these tokens are short-lived by design.

How do these claims map to Secrets Manager configuration?

When you set up the JWT authenticator in Secrets Manager later, you will configure variables that map directly to these claims.

  • token-app-property will be set to sub. This tells Secrets Manager which JWT claim contains the workload identity.
  • issuer will be set to match the iss value in your token. Secrets Manager rejects any JWT whose issuer does not match.
  • identity-path will tell Secrets Manager where to find the host definition that corresponds to the sub claim value.

Take note of the iss value in your decoded JWT. You will use it when configuring the Secrets Manager authenticator in Step 5.

Token already expired?

The default JWT SVID TTL is 5 minutes. If you spend more than 5 minutes between fetching and using the token, you will need to fetch a fresh one by re-running the JWT_SVID=... command. This is expected behavior for short-lived workload credentials.

You now have a cryptographic identity token ready for your AI agent. Next up, you will expose the JWKS endpoint through a Cloudflare Tunnel so CyberArk Secrets Manager can verify these tokens over the internet.

Expose the JWKS Endpoint with Cloudflare Tunnel

In the previous step, you fetched a JWT SVID from SPIRE and inspected its claims. One of those claims was the iss (issuer) field. For CyberArk Secrets Manager to trust that JWT, it needs to verify the signature against the issuer's public keys.

Those public keys are served by SPIRE's OIDC Discovery Provider at a JWKS endpoint. The problem is that Secrets Manager SaaS lives on the internet, but your SPIRE server runs locally. In this step, you will bridge that gap using a Cloudflare Tunnel to expose the local JWKS endpoint to the public internet.

In this step, get ready to:
  • Configure and start the SPIRE OIDC Discovery Provider to serve a JWKS endpoint locally.
  • Create a Cloudflare Tunnel that routes public traffic to the local OIDC Discovery Provider.
  • Verify the public JWKS and OIDC discovery endpoints are reachable from the internet.
Configure and start the OIDC Discovery Provider

The OIDC Discovery Provider is a helper binary you already built from the SPIRE source code. It connects to the SPIRE server's API socket, reads the signing keys, and serves them over HTTP at a /keys endpoint.

  • Create the conf/oidc/ directory and then the config file by running these commands:
mkdir -p conf/oidc
  • Create the OIDC Discovery Provider configuration file by running:
cat > conf/oidc/oidc-discovery-provider.conf << 'EOF'
# Log verbosity
log_level = "info"

# The public domain(s) this provider serves
domains = ["[[OIDC_DOMAIN="spire-oidc.yourdomain.com"]]"]

# Use HTTPS scheme in discovery document URLs
allow_insecure_scheme = false

# Local HTTP listener for Cloudflare Tunnel to connect to
insecure_addr = "127.0.0.1:8082"

# Connect to the SPIRE server API to get signing keys
server_api {
    address = "unix:///tmp/spire-server/private/api.sock"
}
EOF

What does this configuration do?

domains tells the provider which public domain it will be served from. This must match the subdomain you will set up with Cloudflare Tunnel shortly.

insecure_addr opens a local HTTP listener on port 8082. Cloudflare Tunnel will connect to this local address and handle TLS at its edge. allow_insecure_scheme = false ensures the OIDC discovery document advertises https:// URLs, which is correct since Cloudflare provides TLS.

server_api points to the SPIRE server's Unix socket so the provider can fetch the public signing keys directly.

  • Confirm the file was created by running:
ls conf/oidc/

You should see oidc-discovery-provider.conf in the output.

Don't see the file?

Make sure you ran the mkdir -p conf/oidc command first. The cat > command will fail silently if the directory does not exist.

Help me troubleshoot my OIDC Discovery Provider config file

Now that the configuration file is ready, you can start the OIDC Discovery Provider.

  • Start the OIDC Discovery Provider as a background process by running:
bin/oidc-discovery-provider -config conf/oidc/oidc-discovery-provider.conf &

What does this command do?

The -config flag points the provider to your configuration file. The trailing & runs it in the background so you can continue using the same terminal.

  • Verify the local JWKS endpoint is working by running:
curl http://127.0.0.1:8082/keys

You should see a JSON document containing a keys array with at least one public key object. This is the JWKS document that Secrets Manager will use to verify JWT signatures.

Seeing Connection refused?

Check that the OIDC Discovery Provider process is still running with jobs. If it exited, check the terminal output for error messages. A common cause is the SPIRE server not running. Confirm the SPIRE server is healthy with bin/spire-server healthcheck.

Also verify that the server_api socket path in your config (/tmp/spire-server/private/api.sock) matches the socket_path in your server.conf.

Help me debug my OIDC Discovery Provider

Create the Cloudflare Tunnel

The JWKS endpoint is working locally, but CyberArk Secrets Manager SaaS needs to reach it over the public internet. A Cloudflare Tunnel creates a secure outbound connection from your machine to Cloudflare's edge, letting you expose the local OIDC Discovery Provider under a public HTTPS URL without opening any firewall ports.

  • Authenticate cloudflared to your Cloudflare account by running:
cloudflared tunnel login

What happens during login?

This command opens your default browser and asks you to authorize cloudflared to access your Cloudflare account. Select the DNS zone you want to use for the tunnel. After authorization, a certificate file (cert.pem) is saved to ~/.cloudflared/.

  • Create a new tunnel named spire-oidc by running:
cloudflared tunnel create spire-oidc

What does this command do?

This registers the tunnel with Cloudflare and generates a credentials JSON file in ~/.cloudflared/. The output shows two important values you will need next: the tunnel ID (a UUID) and the path to the credentials file.

  • Note the tunnel ID and credentials file path from the output. You will use them in the tunnel configuration file.

Next, create the tunnel configuration file that tells cloudflared how to route traffic from your public hostname to the local OIDC Discovery Provider.

  • Create the tunnel configuration file by running:
cat > ~/.cloudflared/config.yml << EOF
# Tunnel UUID from cloudflared tunnel create output
tunnel: [[TUNNEL_ID="your tunnel ID from the create command"]]

# Path to credentials file from tunnel create output
credentials-file: [[CREDS_FILE="path to credentials file, e.g. /Users/you/.cloudflared/abc123.json"]]

# Route public hostname to local OIDC Discovery Provider
ingress:
  - hostname: [[OIDC_DOMAIN="spire-oidc.yourdomain.com"]]
    service: http://127.0.0.1:8082
  - service: http_status:404
EOF

What does this configuration do?

tunnel and credentials-file link this config to the specific tunnel you just created.

The ingress section maps incoming requests to your chosen hostname to the local OIDC Discovery Provider on port 8082. The final catch-all rule (http_status:404) returns a 404 for any requests that don't match.

Use the exact credentials file path

The credentials file path must be an absolute path to the JSON file. Copy the exact path from the output of cloudflared tunnel create. It typically looks like /Users/yourname/.cloudflared/<tunnel-id>.json.

  • Create a DNS CNAME record that points your chosen subdomain to the tunnel by running:
cloudflared tunnel route dns spire-oidc [[OIDC_DOMAIN="spire-oidc.yourdomain.com"]]

What does this command do?

This creates a CNAME DNS record in your Cloudflare DNS zone that points your subdomain to the tunnel's Cloudflare endpoint. When Secrets Manager SaaS queries this domain, Cloudflare routes the request through the tunnel to your local machine.

You should see a confirmation message that the DNS route was added.

Seeing a DNS record conflict?

If a CNAME or A record already exists for this subdomain, cloudflared will report a conflict. Delete the existing record from the Cloudflare dashboard (go to your domain, then DNS, then Records) and re-run the command.

Help me resolve a Cloudflare DNS conflict

Start the tunnel and verify the public endpoints

The tunnel is created and the DNS record is in place. Now you need to start the tunnel process so that Cloudflare can forward requests to your local OIDC Discovery Provider.

  • Start the Cloudflare Tunnel by running:
cloudflared tunnel run spire-oidc &

What does this command do?

This starts cloudflared and opens outbound connections to Cloudflare's edge network. Any HTTPS request to your public subdomain is forwarded through the tunnel to http://127.0.0.1:8082. The trailing & backgrounds the process so you can continue using the terminal.

Wait a few seconds for the tunnel to establish connections, then verify that the public endpoints are reachable.

  • Verify the public JWKS endpoint by running:
curl https://[[OIDC_DOMAIN="spire-oidc.yourdomain.com"]]/keys

You should see the same JWKS JSON document you saw when curling the local endpoint. This confirms that Cloudflare is correctly forwarding traffic to your OIDC Discovery Provider.

  • Verify the OIDC discovery document by running:
curl https://[[OIDC_DOMAIN="spire-oidc.yourdomain.com"]]/.well-known/openid-configuration

You should see a JSON document containing an issuer field and a jwks_uri field that points to your public /keys endpoint.

Why does the OIDC discovery document matter?

The /.well-known/openid-configuration endpoint is a standard OIDC discovery URL. It tells any JWT verifier (like Secrets Manager) where to find the signing keys (jwks_uri) and what issuer to expect (issuer). Secrets Manager uses this to automatically download and cache the public keys for signature verification.

Getting a 502 Bad Gateway or connection error?

A 502 means the tunnel is connected to Cloudflare but cannot reach the local OIDC Discovery Provider. Verify the provider is still running with jobs. Also confirm the port in your config.yml ingress rule (8082) matches the insecure_addr port in your OIDC config.

If you see a DNS resolution error, it may take a minute for the CNAME record to propagate. Wait 30 seconds and try again.

Help me debug my Cloudflare Tunnel setup

✔️ Awesome, I've got everything!

Great. Double check you have both the OIDC Discovery Provider and the Cloudflare Tunnel running as background processes. You can confirm with the jobs command.

ⓧ I'd like to double check the full code

Here is the complete OIDC Discovery Provider config file for reference:

log_level = "info"
domains = ["spire-oidc.yourdomain.com"]
allow_insecure_scheme = false
insecure_addr = "127.0.0.1:8082"

server_api {
    address = "unix:///tmp/spire-server/private/api.sock"
}

And here is what your Cloudflare Tunnel config file should look like (with your actual tunnel ID, credentials path, and domain):

tunnel: <your-tunnel-id>
credentials-file: /Users/<your-username>/.cloudflared/<your-tunnel-id>.json

ingress:
  - hostname: spire-oidc.yourdomain.com
    service: http://127.0.0.1:8082
  - service: http_status:404

Your JWKS endpoint is now publicly reachable over HTTPS. Next up, you will configure the JWT authenticator in CyberArk Secrets Manager to trust this endpoint and validate the JWTs your SPIRE agent issues.

Create the JWT Authenticator in Secrets Manager

In the previous step, you exposed your SPIRE JWKS endpoint to the internet via Cloudflare Tunnel. CyberArk Secrets Manager can now reach that endpoint to validate JWT SVIDs.

The next piece of the puzzle is telling Secrets Manager to actually trust those JWTs. You will create a JWT authenticator that points at your public JWKS endpoint and maps JWT claims to workload identities inside Secrets Manager.

In this step, get ready to:
  • Initialize the Secrets Manager CLI and log in to your SaaS tenant.
  • Create and load the JWT authenticator policy.
  • Set the authenticator variables and enable the authenticator.
Initialize the Secrets Manager CLI and log in

Before you can load policies or set variables, you need to point the conjur CLI at your Secrets Manager SaaS tenant and authenticate.

  • Initialize the CLI for your SaaS tenant by running this command:
conjur init saas -u [[SM_URL="https://your-subdomain.secretsmgr.cyberark.cloud/api"]]

What does this command do?

The conjur init saas command configures the CLI to communicate with your Secrets Manager SaaS instance. The -u flag provides the base API URL for your tenant.

  • Log in to Secrets Manager by running:
conjur login
  • The CLI will prompt you for your username and password. Enter your admin credentials.

You should see a confirmation message indicating a successful login.

Login failed?

Confirm that the URL you passed to conjur init saas ends with /api. A missing suffix causes connection errors.

Make sure your user account belongs to the Conjur_Cloud_Admins or Authn_Admins group. You need admin privileges to create authenticators.

Help me troubleshoot my Secrets Manager CLI login

Create and load the JWT authenticator policy

Secrets Manager uses declarative YAML policy files to define authenticators, variables, and access rules. You will create a policy file that defines the JWT authenticator and its configuration variables.

  • Create a policies/ directory inside your SPIRE project folder by running this command:
mkdir -p policies
  • Create the file policies/spiffe-auth.yml and paste the following policy into it:
# Define the spiffe-auth JWT authenticator
- !policy
    id: spiffe-auth
    body:
    # The webservice that workloads authenticate against
    - !webservice

    # Configuration variables for JWT validation
    - !variable jwks-uri
    - !variable token-app-property
    - !variable identity-path
    - !variable issuer

    # Groups for workload access and operator management
    - !group apps
    - !group operators

    # Allow the apps group to authenticate via this webservice
    - !permit
      role: !group apps
      privilege: [ read, authenticate ]
      resource: !webservice

    # Allow operators to manage the authenticator configuration
    - !permit
      role: !group operators
      privilege: [ read, update ]
      resource: !webservice

What does this policy do?

This policy creates a JWT authenticator called spiffe-auth inside Secrets Manager. The !webservice is the authentication endpoint that workloads will call.

The four !variable declarations are placeholders for the authenticator's configuration. jwks-uri tells Secrets Manager where to find the public keys for validating JWTs. token-app-property identifies which JWT claim maps to the workload identity.

The apps group gets read and authenticate privileges on the webservice. Any workload added to this group can authenticate using a JWT.

  • Save policies/spiffe-auth.yml.
  • Load the policy into the conjur/authn-jwt branch by running this command:
conjur policy load -f policies/spiffe-auth.yml -b conjur/authn-jwt

You should see JSON output listing the newly created resources, including the webservice, variables, and groups.

Why the conjur/authn-jwt branch?

Secrets Manager organizes JWT authenticators under the conjur/authn-jwt policy branch. Loading your policy here tells Secrets Manager that spiffe-auth is a JWT authenticator, not just a generic policy.

Seeing a 403 or permission error?

Loading a policy into conjur/authn-jwt requires create privileges on that branch. Confirm your logged-in user is a member of the Conjur_Cloud_Admins or Authn_Admins group.

Help me fix a permission error when loading my JWT authenticator policy

Set the authenticator variables and enable the authenticator

The policy you loaded created empty variable placeholders. Now you need to fill them in with the actual values that tell Secrets Manager how to validate SPIRE-issued JWTs.

  • Set the jwks-uri variable to your public Cloudflare Tunnel JWKS URL by running this command:
conjur variable set -i conjur/authn-jwt/spiffe-auth/jwks-uri -v [[JWKS_URL="https://spire-oidc.yourdomain.com/keys"]]
  • Set the token-app-property variable to sub by running:
conjur variable set -i conjur/authn-jwt/spiffe-auth/token-app-property -v sub
  • Set the identity-path variable to data/spiffe-apps by running:
conjur variable set -i conjur/authn-jwt/spiffe-auth/identity-path -v data/spiffe-apps
  • Set the issuer variable to your SPIRE OIDC issuer URL by running:
conjur variable set -i conjur/authn-jwt/spiffe-auth/issuer -v [[OIDC_ISSUER="https://spire-oidc.yourdomain.com"]]

What do these variables control?

  • jwks-uri is the public URL where Secrets Manager fetches the SPIRE signing keys to verify JWT signatures.
  • token-app-property tells Secrets Manager which JWT claim identifies the workload. The sub claim in a SPIRE JWT SVID contains the SPIFFE ID.
  • identity-path is the Secrets Manager policy path where workload host definitions live. The authenticator combines this path with the sub claim to find the matching host.
  • issuer must match the iss claim in the JWT. If these do not match, authentication fails.

Find your issuer URL

If you are unsure of the exact issuer value, check the OIDC discovery document you verified in the previous step. Run curl https://spire-oidc.yourdomain.com/.well-known/openid-configuration and look at the issuer field in the response. Use that exact value.

  • Enable the JWT authenticator by running:
conjur authenticator enable --id authn-jwt/spiffe-auth

You should see a confirmation that the authenticator is enabled.

Authenticator not enabling?

If you see an error about a missing variable, double-check that all four conjur variable set commands completed successfully. Each one should output a confirmation.

Make sure Secrets Manager can reach your JWKS URL. If the Cloudflare Tunnel from the previous step is not running, restart it with cloudflared tunnel run spire-oidc.

Help me troubleshoot enabling my JWT authenticator in Secrets Manager

✔️ Awesome, I've got everything!

Make sure you have saved policies/spiffe-auth.yml and that all four variables are set.

ⓧ I'd like to double check the full code

Here is the complete policies/spiffe-auth.yml file:

- !policy
    id: spiffe-auth
    body:
    - !webservice

    - !variable jwks-uri
    - !variable token-app-property
    - !variable identity-path
    - !variable issuer

    - !group apps
    - !group operators

    - !permit
      role: !group apps
      privilege: [ read, authenticate ]
      resource: !webservice

    - !permit
      role: !group operators
      privilege: [ read, update ]
      resource: !webservice

Your JWT authenticator is live and pointing at your SPIRE JWKS endpoint. Next up, you will create the AI agent workload identity inside Secrets Manager so the authenticator knows which workloads to trust.

Create the AI Agent Workload in Secrets Manager

In the previous step, you configured the JWT authenticator so Secrets Manager knows how to validate SPIRE-issued JWTs. The authenticator can verify signatures and read claims, but it doesn't yet know which workloads are allowed to authenticate.

In this step, you will create the AI agent's identity inside Secrets Manager, connect it to the JWT authenticator, and create a test secret for the agent to retrieve. This is the final piece of configuration before the full end-to-end authentication flow.

In this step, get ready to:
  • Define an AI agent host in Secrets Manager with the correct SPIFFE ID annotation.
  • Grant the host permission to use the JWT authenticator.
  • Create a test secret and give the AI agent permission to retrieve it.
Define the AI agent host in Secrets Manager

Secrets Manager needs a host record that represents the AI agent workload. When the agent authenticates with a JWT SVID, Secrets Manager reads the sub claim from the token and matches it to a host annotation.

How does the identity mapping work?

In the previous step, you set token-app-property to sub and identity-path to data/spiffe-apps. When a JWT arrives, Secrets Manager combines these two values to find the host.

For a token with sub = spiffe://example.org/ai-agent, Secrets Manager looks for a host at data/spiffe-apps/spiffe://example.org/ai-agent. That is the host you are about to create.

  • Create a new file called policies/authn-jwt-hosts.yml with the following content:
- !policy
  id: spiffe-apps
  body:
    - !group

    - !host
      id: spiffe://example.org/ai-agent
      annotations:
        authn-jwt/spiffe-auth/sub: spiffe://example.org/ai-agent

    - !grant
      role: !group
      member: !host spiffe://example.org/ai-agent

What does this policy do?

  • !policy id: spiffe-apps creates a policy namespace called spiffe-apps to hold all SPIFFE-authenticated workloads.
  • !group creates an unnamed group inside the policy. Any host in this group will inherit the permissions you assign to it later.
  • !host defines the AI agent's identity. The annotations block tells Secrets Manager to match this host when a JWT arrives with a sub claim of spiffe://example.org/ai-agent.
  • !grant makes the host a member of the policy group, so you can assign permissions at the group level.
  • Save policies/authn-jwt-hosts.yml.
  • Load the host policy into the data branch by running this command:
conjur policy load -f policies/authn-jwt-hosts.yml -b data
  • You should see JSON output listing the created resources, including the host data:host:spiffe-apps/spiffe://example.org/ai-agent.

Seeing a 403 or permission error?

Make sure you are logged into the conjur CLI with an account that belongs to the Conjur_Cloud_Admins or Authn_Admins group. Run conjur login to re-authenticate if your session has expired.

Help me fix a permission error when loading a policy into CyberArk Secrets Manager.

✔️ Awesome, I've got everything!

Great. Make sure you have saved policies/authn-jwt-hosts.yml and confirmed the policy loaded successfully.

ⓧ I'd like to double check the full code

Here is the complete policies/authn-jwt-hosts.yml file for comparison.

- !policy
  id: spiffe-apps
  body:
    - !group

    - !host
      id: spiffe://example.org/ai-agent
      annotations:
        authn-jwt/spiffe-auth/sub: spiffe://example.org/ai-agent

    - !grant
      role: !group
      member: !host spiffe://example.org/ai-agent
Grant the host access to the JWT authenticator

The host exists, but it is not yet allowed to use the JWT authenticator. The authenticator has an apps group that controls which workloads can authenticate through it. You need to add the spiffe-apps group as a member of that apps group.

  • Create a new file called policies/authn-jwt-grant.yml with the following content:
- !grant
  role: !group apps
  member: !group /data/spiffe-apps

What does this grant do?

This makes the /data/spiffe-apps group a member of the authenticator's apps group. Because the AI agent host is already a member of /data/spiffe-apps (from the previous policy), it inherits the read and authenticate privileges on the JWT authenticator webservice.

  • Save policies/authn-jwt-grant.yml.
  • Load the grant policy into the conjur/authn-jwt/spiffe-auth branch by running this command:
conjur policy load -f policies/authn-jwt-grant.yml -b conjur/authn-jwt/spiffe-auth
  • You should see JSON output confirming the grant was created successfully.

Seeing a 404 or branch not found?

The branch conjur/authn-jwt/spiffe-auth must already exist from loading the JWT authenticator policy in the previous step. If the branch is missing, go back to Step 5 and confirm you loaded policies/spiffe-auth.yml into the conjur/authn-jwt branch.

Help me troubleshoot a branch not found error when loading a grant policy.

Why two separate policy files?

The host policy loaded into the data branch, but the grant loaded into conjur/authn-jwt/spiffe-auth. Each policy can only reference resources within its own branch. The grant policy bridges the two branches by referencing the group from data using its full path /data/spiffe-apps.

✔️ Awesome, I've got everything!

Great. Make sure you have saved policies/authn-jwt-grant.yml and confirmed the grant loaded successfully.

ⓧ I'd like to double check the full code

Here is the complete policies/authn-jwt-grant.yml file for comparison.

- !grant
  role: !group apps
  member: !group /data/spiffe-apps
Create a test secret and grant the AI agent permission to retrieve it

The AI agent can now authenticate, but it has no secrets to access yet. You will create a test secret variable and give the spiffe-apps group permission to read it.

  • Create a new file called policies/test-secret.yml with the following content:
- !policy
  id: spiffe-apps
  body:
    - !variable test-secret

    - !permit
      role: !group
      privilege: [ read, execute ]
      resource: !variable test-secret

What does this policy do?

  • !variable test-secret creates a secret variable called test-secret inside the spiffe-apps policy.
  • !permit grants the spiffe-apps group read and execute privileges on the secret. The execute privilege is what allows a workload to retrieve the secret value through the REST API.

This policy loads into the same data branch as the host policy. Secrets Manager merges it into the existing spiffe-apps policy, adding the variable and permission alongside the host and group you already created.

  • Save policies/test-secret.yml.
  • Load the secret policy into the data branch by running this command:
conjur policy load -f policies/test-secret.yml -b data
  • You should see JSON output confirming the variable data:variable:spiffe-apps/test-secret was created.

Seeing a conflict error?

If you see a conflict, the variable may already exist from a previous attempt. This is safe to ignore. The policy load is additive, so re-loading does not overwrite existing resources.

Help me troubleshoot a conflict error when loading a Secrets Manager policy.

The variable exists, but it has no value yet. You need to store a secret in it so you have something to retrieve in the next step.

  • Set the test secret value by running this command:
conjur variable set -i data/spiffe-apps/test-secret -v "ai-agent-secret-value-2025"
  • You should see a confirmation message indicating the variable value was set.

What happens in the next step?

You now have all the pieces in place. The AI agent workload has a SPIFFE ID issued by SPIRE, a matching host identity in Secrets Manager, permission to authenticate through the JWT authenticator, and execute access on the test secret. In the next step, you will connect all of these together in a single authentication and retrieval flow.

✔️ Awesome, I've got everything!

Great. Confirm you have saved policies/test-secret.yml and set the secret value successfully.

ⓧ I'd like to double check the full code

Here is the complete policies/test-secret.yml file for comparison.

- !policy
  id: spiffe-apps
  body:
    - !variable test-secret

    - !permit
      role: !group
      privilege: [ read, execute ]
      resource: !variable test-secret

Your AI agent workload is now fully registered in Secrets Manager with a matching SPIFFE identity and access to a test secret. Next up, you will put it all together by fetching a live JWT SVID and using it to authenticate and retrieve the secret.

Authenticate and Retrieve a Secret

You've created the AI agent workload in Secrets Manager with the correct SPIFFE identity annotation and group membership. Every piece of the identity pipeline is now in place.

Time for the moment of truth. In this step, you'll use the AI agent's SPIRE identity to authenticate to Secrets Manager and retrieve a secret. No static credentials involved.

In this step, get ready to:
  • Fetch a fresh JWT SVID and authenticate to Secrets Manager.
  • Retrieve a test secret using the identity-based access token.
Fetch a JWT SVID and authenticate to Secrets Manager

The JWT SVID you fetched in an earlier step has a 5-minute TTL. It has already expired, so you need a fresh one.

This time, you'll save the token directly to a shell variable so you can pass it to Secrets Manager programmatically. You'll use jq to extract the token from the JSON output.

  • Install jq (a command-line JSON processor) by running:
brew install jq

Already have jq?

If jq is already installed, Homebrew will let you know. You can safely move on to the next command.

  • Fetch a fresh JWT SVID for the AI agent workload and save it to a shell variable by running:
JWT_SVID=$(bin/spire-agent api fetch jwt \
    -audience conjur \
    -socketPath /tmp/spire-agent/public/api.sock \
    -output json | jq -r '.[] | .svids[] | .svid')

What does this command do?

This command does three things in a single pipeline:

  • -audience conjur tells SPIRE to issue a JWT with conjur in the aud claim. Secrets Manager checks this claim to confirm the token is intended for it.
  • -output json returns the JWT in structured JSON instead of plain text.
  • The jq filter extracts just the raw token string from the nested JSON structure.
  • Confirm the token was captured by running:
echo $JWT_SVID

You should see a long encoded string starting with eyJ. This is your AI agent's cryptographic identity.

Seeing an empty output?

  • Make sure the SPIRE agent is still running. Run bin/spire-agent healthcheck -socketPath /tmp/spire-agent/public/api.sock to check.
  • Confirm the workload registration still exists by running bin/spire-server entry show. You should see the spiffe://example.org/ai-agent entry.
  • Verify that jq is installed by running jq --version. If it's not found, run brew install jq.

Why is my JWT SVID fetch returning empty?

Now you have a valid JWT SVID in your terminal session. The next step is to present this token to Secrets Manager's JWT authenticator endpoint.

  • Authenticate to Secrets Manager by sending the JWT to the authenticator endpoint. Run this curl command:
ACCESS_TOKEN=$(curl -s --request POST \
    "https://[[CONJUR_SUBDOMAIN="your-subdomain"]].secretsmgr.cyberark.cloud/api/authn-jwt/spiffe-auth/conjur/authenticate" \
    --header "Content-Type: application/x-www-form-urlencoded" \
    --header "Accept-Encoding: base64" \
    --data-urlencode "jwt=${JWT_SVID}")

What does this request do?

  • The POST request sends the JWT SVID in the request body using --data-urlencode. Secrets Manager extracts the sub claim (your SPIFFE ID) and matches it against the registered workload host.
  • The Accept-Encoding: base64 header tells Secrets Manager to return the access token as a base64-encoded string you can use directly in subsequent API calls.
  • Behind the scenes, Secrets Manager fetches the JWKS from your Cloudflare Tunnel endpoint to verify the JWT's signature. This is why the public JWKS endpoint was essential.
  • Confirm you received an access token by running:
echo $ACCESS_TOKEN

You should see a long base64-encoded string. This is your Secrets Manager access token, valid for 8 minutes.

Getting an empty response or error?

  • If the response is empty, your JWT may have expired. JWT SVIDs only last 5 minutes. Re-run the JWT_SVID=... command from above, then immediately retry the curl command.
  • If you see a 401 error, confirm the Cloudflare Tunnel is still running and the JWKS endpoint is reachable. Test with curl https://spire-oidc.yourdomain.com/keys.
  • Double check that your Secrets Manager subdomain in the URL matches the one you used during conjur init saas.

Why is my JWT authentication to Secrets Manager failing?

Why does the token expire so quickly?

Short-lived tokens are a core principle of zero-trust security. The JWT SVID lasts 5 minutes and the Secrets Manager access token lasts 8 minutes.

If a token is compromised, the attacker's window is measured in minutes, not months. The Secret Mission at the end of this project addresses this by automating token refresh.

Retrieve the test secret

You now have a valid access token. The final step in the pipeline is to use it to retrieve the test secret you created earlier.

  • Retrieve the secret from Secrets Manager by running:
curl -s \
    -H "Authorization: Token token=\"${ACCESS_TOKEN}\"" \
    "https://[[CONJUR_SUBDOMAIN="your-subdomain"]].secretsmgr.cyberark.cloud/api/secrets/conjur/variable/data%2Fspiffe-apps%2Ftest-secret"

What does this request do?

  • The GET request targets the /secrets/conjur/variable/ endpoint with the secret's path. The slashes in data/spiffe-apps/test-secret are URL-encoded as %2F.
  • The Authorization header passes the access token you received from the authentication step. Secrets Manager validates this token before returning the secret value.

You should see the exact value you stored in the test-secret variable printed in your terminal. That's the secret your AI agent just retrieved using nothing but its cryptographic SPIFFE identity.

Getting a 403 or empty response?

  • If your access token has expired (it lasts 8 minutes), re-run both the JWT SVID fetch and the authentication commands above, then retry this request.
  • If you get a 403 Forbidden error, the workload host may not have execute permission on the secret. Verify the grant policy from the previous step was loaded correctly.
  • Make sure the secret path in the URL matches exactly. The encoded path should be data%2Fspiffe-apps%2Ftest-secret.

Why am I getting a 403 when retrieving my secret from Secrets Manager?

Secret mission

Automate the Full JWT-to-Secret Pipeline in a Shell Script

You proved you can authenticate manually in Step 7. Now automate the entire JWT fetch, authenticate, and secret retrieval flow into a single reusable shell script that handles errors and token refresh by design.

Before you go

Clean Up Your Resources

This project runs entirely locally with free-tier cloud services, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • SPIRE server, agent, and OIDC Discovery Provider running as local background processes. No cost.
  • Cloudflare Tunnel named spire-oidc routing traffic to your local machine. Free tier.
  • CyberArk Secrets Manager resources including a JWT authenticator (conjur/authn-jwt/spiffe-auth), a workload host, and a test secret variable. No additional cost beyond your existing tenant.

Keep everything running

No action needed. Choose this if you plan to continue testing or extend the setup with additional workloads and secrets.

  • Your SPIRE server, agent, and OIDC Discovery Provider will continue running in their terminal windows.
  • Your Cloudflare Tunnel will keep serving the JWKS endpoint publicly.
  • Your Secrets Manager JWT authenticator and workload host remain configured and active.

Pause - I'll come back to this later

Stop the running processes to free up system resources. Your configuration files and Secrets Manager policies stay intact so you can restart everything later.

  • Switch to each terminal window running the SPIRE server, agent, and OIDC Discovery Provider. Press Ctrl+C in each to stop them.
  • Switch to the terminal running the Cloudflare Tunnel and press Ctrl+C to stop it.
  • Leave your Secrets Manager JWT authenticator and workload policies in place. They cost nothing and can be reused when you restart.
  • To resume later, restart the SPIRE server first, then the agent (with a new join token), then the OIDC Discovery Provider, and finally the Cloudflare Tunnel.

Delete - I don't want to use this again

Remove all project resources and start fresh. This removes local processes, the tunnel, and the Secrets Manager configuration.

  • Stop the SPIRE server, agent, and OIDC Discovery Provider by pressing Ctrl+C in each terminal window.
  • Delete the Cloudflare Tunnel by running cloudflared tunnel delete spire-oidc in your terminal.
  • Remove the DNS CNAME record you created in the Cloudflare dashboard for the tunnel subdomain.
  • In Secrets Manager, disable the JWT authenticator by running conjur authenticator disable --id authn-jwt/spiffe-auth.
  • Optionally remove the JWT authenticator and workload policies from Secrets Manager if you no longer need them.
  • Delete the cloned SPIRE repository folder and the .data directory to remove all local data.

Mission accomplished

That's a Wrap!

Nice work! You just built an end-to-end identity pipeline connecting open-source SPIRE to CyberArk Secrets Manager SaaS. Your AI agent now proves its identity with a cryptographic SPIFFE identity and retrieves secrets without a single static credential in sight.

You've learned how to:

  • Built SPIRE from source, stood up a local server and agent, and registered an AI agent workload with a SPIFFE ID to obtain short-lived JWT SVIDs.
  • Exposed the SPIRE JWKS endpoint publicly using the OIDC Discovery Provider and Cloudflare Tunnel, then configured a JWT authenticator in Secrets Manager to trust SPIRE-issued tokens.
  • Authenticated to Secrets Manager using a JWT SVID and retrieved a secret via the REST API, demonstrating the zero-trust pattern of workload identity replacing static credentials.
  • Secret Mission: Automated the full JWT-to-secret pipeline in a reusable shell script with token refresh handling.

Ready to quiz yourself?