A service that exchanges OIDC identity tokens with GitHub and Oxide short-lived tokens. The source is released under the MPL 2.0 license.
The recommended way to interact with the service from GitHub Actions is to use oxidecomputer/oidcx-action.
The server exposes a single endpoint, POST /exchange, which exchanges a JWT
from a trusted OpenID Connect identity provider with a temporary token from one
of the supported services.
The JWT must have an aud (audience) matching the protocol and hostname of the
oidcx instance (for example https://oidcx.example.com). This
ensures a JWT meant for oidcx cannot be used for other services.
The claims in the JWT and in the request must adhere to the configured authorization policy. The authorization policy differs between deployment, so it's recommended to read the deployment's documentation to see what requests will be allowed.
Once a request is authorized, a JSON payload with a single access_token field
will be returned, containing the requested access token.
To request GitHub tokens, the JSON request body must containg the fields:
caller_identity: JWT token used for authorizing the request.service: must begithub.repositories: list of repositories to request access to. They must all belong to the same organization or user. Issue separate exchange calls if you need access to repositories belonging to different users or orgs.permissions: list of permissions the token should be granted. Each permission is in the form ofscope:level, where the scope is one of the scopes supported by GitHub App installation tokens and the level is eitherreadorwrite.
An example of a valid request:
{
"jwt": "eyJhbGciOiJIUz...",
"service": "github",
"repositories": ["oxidecomputer/oidcx"],
"permissions": ["contents:write", "pull_requests:write"]
}Tokens will be valid for an hour.
Note that the permissions that can be granted and the repositories that can be accessed depend on the permissions the underlying GitHub App generating the token has, and whether it is installed in the repositories you are trying to access. Even if a request is authorized by oidcx, it might be rejected if the GitHub App cannot generate the requested token.
To request tokens to access an Oxide silo, the JSON request must contain the fields:
caller_identity: JWT token used for authorizing the request.service: must beoxide.silo: URL of the silothe token is requested for.durationnumber of seconds the token should be valid for.
An example of a valid request:
{
"jwt": "eyJhbGciOiJIUz...",
"service": "github",
"silo": "https://oxide.sys.rack2.eng.oxide.computer",
"duration": 3600
}Note that credentials for the requested silo must be present in oidcx's configuration. The resulting token will have the same level of access as the credential in the configuration.
The authorization policy is defined using the Polar language.
oidcx will check whether the allow_request(claims, request) Polar
query is authorized, passing the OIDC claims in the claims parameter and the
Oxide or GitHub token request in the request parameter.
A very simple authorization policy can be:
allow_request(claims, request) if
claims.iss == "https://token.actions.githubusercontent.com" and
claims.repository == "oxidecomputer/oidcx" and
request matches GitHub and
request.repository == "oxidecomputer/oidcx-action" and
request.permission == "contents:write";This policy checks whether the OIDC claims indicate the request came from GitHub
Actions (with claims.iss) and the repository requesting the token is the
intended one (with claims.repository). It then checks that the request is a
GitHub token request, and that the requested repository and permission is the
allowed one.
More advanced Polar policies can be written. For example:
allow_request(claims, request) if
claims.iss == "https://token.actions.githubusercontent.com" and
claims.repository_owner == "oxidecomputer" and
allow_from_github_actions(claims, request);
allow_from_github_actions(claims, request: GitHub) if
claims.repository == request.repository and
request.permission == "contents:write";
allow_from_github_actions(claims, request: Oxide) if
request.silo == "https://oxide.sys.rack2.eng.oxide.computer" and
request.duration <= 3600 and
claims.repository in [
"oxidecomputer/oidc-exchnage",
"oxidecomputer/oidcx-action",
];The policy above defines rules in allow_request() that all requests authorized
by allow_from_github_actions() must abide by, a policy allowing a repository
to get a GitHub token with write access to itself (by checking that the
requested repository matches the claimed repository), and a request allowing two
repositories to request an Oxide token.
The claims argument in Polar policies contain all of the claims included in
the OIDC JWT. It must include iss and aud (note that the audience is checked
by oidcx before the Polar policy, so you don't need to check it
again), and the rest of the claims depend on what your identity provider claims.
For GitHub Actions, GitHub provides a list of included claims.
The request argument in Polar policies can be of type Oxide when the user
requested an Oxide token. The two fields available are silo (the URL to the
silo) and duration (the number of seconds the token will be valid for).
The request argument in Polar policies can be of type GitHub when the user
requested a GitHub token. The two fields available are repository (the name of
one of the repositories being requested) and permission (the name of one of
the requested permissions).
To simplify how policies are written, when authorizing GitHub token requests
oidcx will individually test whether all permutations of repositories
and permissions are valid, and reject the request if any of them are not. This
means in the policy you only check one (repository, permission) permutation at
a time.
The main configuration of the service is defined into a TOML file. Multiple
files can be passed to the command line, and they will be merged. If no path to
a configuration file is passed the settings.toml file from the current
directory will be loaded.
# Path to the Polar file defining the authorization policy. Required.
policy_path = "path/to/policy.polar"
# Expected content of the `aud` claim in JWTs. JWTs with different audiences
# will be rejected. For compatibility with oxidecomputer/oidcx-action,
# this must be the URL the service is deployed to. It's strongly recommended to
# use the server URL in other scenarios too. Required.
audience = "https://hostname.of.oidcx.example"
# Port to bind the service to. Optional, defaults to 8080.
port = 8080
# Directory to store log files into. Optional, if missing logs will be emitted
# to stdout.
log_directory = "path/to/logs"
# The [[providers]] block defines one OIDC identity provider authorized to issue
# JWTs accepted by oidcx. Multiple blocks can be provided to support
# more than one IdP. The URL needs to point to the provider's OpenID config URL.
# At least one is required for oidcx to do anything useful.
[[providers]]
url = "https://token.actions.githubusercontent.com/.well-known/openid-configuration"
# The [oxide_silos] block defines the list of Oxide silos a token can be
# requested for, and the credential used to generate those tokens. The block is
# optional, and if omitted no Oxide silo tokens will be issued.
[oxide_silos]
"https://oxide.sys.rack2.eng.oxide.computer" = "oxide-token-helloworld"
"https://example.sys.rack2.eng.oxide.computer" = "oxide-token-helloworld"
# The [github] block defines the GitHub App used to issue GitHub tokens. The app
# must be installed on all repositories a token can be generated for, and must
# have all the permissions a repository might decide to request. The block is
# optional, and if omitted no GitHub tokens will be issued.
[github]
client_id = "Iv2AAAAAAAAAAAAAAAAA"
private_key_path = "path/to/private-key.pem"