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
- A Strongbox JWT issuer signs JWTs with a transit key, and publishes a per-issuer OIDC discovery document and JWKS endpoint.
- JFrog is configured with an OIDC integration named
strongboxthat trusts the issuer via its discovery URL, plus an identity mapping that grants the JWT subject permission to pull images. - The remote registry is configured to reference a JWT issuer and role instead of a vault and secret.
- At pull time, the system mints a JWT with the registry address as
audience, posts it to
https://<registry>/access/api/v1/oidc/tokenusing 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
audclaim, soallowed-audiencesmust include the exact address of the remote registry (including any non-standard port). - A
subject-templatewithout variables gives every minted JWT a fixedsubclaim, 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-urlfrom above, e.g.https://api.example.avassa.net/<tenant-uuid>/jwt/jfrog - Audience:
myorg.jfrog.io
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'ssubject-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: trueon 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-ttlmust therefore be at least5m. - An "audience not allowed" error at mint time means the registry
addressdoes not match the role'sallowed-audiences. They must match exactly, including any non-standard port. - A
401from the token exchange usually means the identity mapping claims do not match the JWT'ssubclaim, or the integration is not namedstrongbox. - 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-configurationfrom a host outside your network.