Skip to main content

Authenticate to a JFrog Registry using Minted JWTs

When pulling container images from a JFrog Artifactory registry, you can avoid storing static registry credentials (username/password or access token) altogether. Instead, the platform mints a short-lived JWT at image pull time using a Strongbox JWT issuer, and exchanges it at JFrog's OIDC token endpoint for registry credentials.

This means no long-lived registry secret is stored anywhere in the system. Each image pull uses a freshly minted JWT, valid for five minutes, signed by a transit key that you control and can rotate.

This is an alternative to the vault/secret based registry credentials described in Registry and Images.

How it works

  1. A Strongbox JWT issuer signs JWTs with a transit key, and publishes a per-issuer OIDC discovery document and JWKS endpoint.
  2. JFrog is configured with an OIDC integration named strongbox that trusts the issuer via its discovery URL, plus an identity mapping that grants the JWT subject permission to pull images.
  3. The remote registry is configured to reference a JWT issuer and role instead of a vault and secret.
  4. At pull time, the system mints a JWT with the registry address as audience, posts it to https://<registry>/access/api/v1/oidc/token using the OAuth 2.0 token-exchange grant, and receives a username and access token which are used to authenticate the pull.

Prerequisites

  • A JFrog instance (SaaS, e.g. myorg.jfrog.io, or self-hosted) with administrator access.
  • JFrog must be able to reach the JWT issuer's OIDC discovery endpoint on the Control Tower API. For a SaaS JFrog instance this means the Control Tower API must be reachable from the Internet.

The examples below use myorg.jfrog.io as the registry address; replace it with your own.

Create a transit signing key

Create a transit key that the issuer will use to sign JWTs. The recommended cipher is ecdsa-p256.

supctl create strongbox transit-keys <<EOF
name: jfrog-jwt-key
cipher: ecdsa-p256
distribute:
to: all
EOF

Rotating this key later produces a new key id (kid), and both keys are served in the JWKS during the overlap window, so rotation does not interrupt image pulls.

Create a JWT issuer

supctl create strongbox jwt-issuers <<EOF
name: jfrog
signing-key: jfrog-jwt-key
distribute:
to: all
EOF

When the issuer leaf is omitted, the issuer URL is auto-derived as https://api.<global-domain>/<tenant-uuid>/jwt/jfrog. The effective URL is available in the read-only discovery-url leaf:

supctl show strongbox jwt-issuers jfrog --fields discovery-url

The <tenant-uuid> part of the auto-derived URL is your tenant's UUID, which can be obtained with:

supctl -j do strongbox get-tenant-uuid | jq -r '.uuid'

The OIDC discovery document is served unauthenticated at <discovery-url>/.well-known/openid-configuration and the signing keys at <discovery-url>/jwks. You can verify both with curl:

curl -s https://api.example.avassa.net/<tenant-uuid>/jwt/jfrog/.well-known/openid-configuration
curl -s https://api.example.avassa.net/<tenant-uuid>/jwt/jfrog/jwks

Create an issuer role for registry pulls

The role constrains what the minted JWTs may contain.

supctl create strongbox jwt-issuers jfrog roles <<EOF
name: docker-pull
allowed-audiences:
- myorg.jfrog.io
subject-template: "registry-pull"
default-ttl: 5m
max-ttl: 30m
EOF

Two details matter here:

  • The JWT minted at pull time uses the configured registry address as the aud claim, so allowed-audiences must include the exact address of the remote registry (including any non-standard port).
  • A subject-template without variables gives every minted JWT a fixed sub claim, which makes the identity mapping on the JFrog side deterministic.

Configure the OIDC integration in JFrog

In the JFrog Platform UI, go to Administration → General → Manage Integrations and create a new OIDC integration:

  • Name: strongbox
  • Provider Type: Generic OpenID Connect
  • Provider URL: the issuer's discovery-url from above, e.g. https://api.example.avassa.net/<tenant-uuid>/jwt/jfrog
  • Audience: myorg.jfrog.io
note

The integration name must be exactly strongbox. The platform sends provider_name: strongbox in the token exchange request, and JFrog uses it to select the integration.

Then add an identity mapping to the integration:

  • Claims JSON: {"sub": "registry-pull"} — must match the role's subject-template
  • Token scope: a user (or group) with read permission on the repositories you intend to pull from
  • Service: Artifactory

JFrog fetches the issuer's JWKS via the provider URL and validates the signature, issuer, audience and expiry of every exchanged JWT before issuing registry credentials. Menu names may differ slightly between JFrog versions; see the JFrog documentation on OIDC integrations for details.

Configure the remote registry

Instead of referencing a vault and secret, reference the JWT issuer and role:

supctl create image-registry remote-registries <<EOF
address: myorg.jfrog.io
credentials:
- repository: '*'
jwt-issuer: jfrog
jwt-role: docker-pull
EOF

As with vault-based credentials, the repository field can be used to scope credentials to specific repositories, and JWT-based and vault-based credential entries can be mixed in the same registry configuration.

Verify

You can perform the mint and token exchange manually to verify the setup end to end before deploying an application:

supctl -j do strongbox jwt-issuers jfrog roles docker-pull mint <<EOF | jq -r .jwt > jwt.txt
audiences:
- myorg.jfrog.io
EOF
curl -s https://myorg.jfrog.io/access/api/v1/oidc/token \
-H "Content-Type: application/json" \
-d "{
\"grant_type\": \"urn:ietf:params:oauth:grant-type:token-exchange\",
\"subject_token_type\": \"urn:ietf:params:oauth:token-type:id_token\",
\"subject_token\": \"$(cat jwt.txt)\",
\"provider_name\": \"strongbox\"
}"

A successful exchange returns a JSON object containing username and access_token. These are the credentials the system will use when pulling.

Finally, deploy an application that references an image in the registry:

name: my-app
services:
- name: my-service
containers:
- name: my-container
image: "myorg.jfrog.io/docker-local/my-container:latest"
mode: replicated
replicas: 1

Troubleshooting

  • Enable verbose-logging: true on the issuer and the role to log every mint attempt, including why a mint was rejected.
  • The pull mints JWTs with a five minute TTL; the role's max-ttl must therefore be at least 5m.
  • An "audience not allowed" error at mint time means the registry address does not match the role's allowed-audiences. They must match exactly, including any non-standard port.
  • A 401 from the token exchange usually means the identity mapping claims do not match the JWT's sub claim, or the integration is not named strongbox.
  • If JFrog reports that it cannot validate the token, verify that the issuer's discovery document and JWKS are reachable from JFrog by fetching <discovery-url>/.well-known/openid-configuration from a host outside your network.