nomad

Nomad JWT auth with GitHub Actions

Learn how JWT-based authentication works in HashiCorp Nomad using a custom GitHub Action as an example of machine-to-machine authentication.

HashiCorp Nomad supports JWT authentication methods, which allow users to authenticate into Nomad using tokens that can be verified via public keys. Primarily, JWT auth methods are used for machine-to-machine authentication, while OIDC auth methods are used for human-to-machine authentication.

This post explains how JWT authentication works and how to set it up in Nomad using a custom GitHub Action. The GitHub Action will use built-in GitHub identity tokens to obtain a short-lived Nomad token with limited permissions.

»How JWT-based authentication works

The first step in JWT-based authentication is the JSON Web Token (JWT) itself. JWTs are encoded pieces of JSON that contain information about the identity of some workload or machine. JWT is a generic format, but for authentication, JWTs will sometimes conform to the more specific OIDC spec and include keys such as “sub”, “iss”, or “aud”.

This example JWT decodes to the following JSON:

{  "jti": "eba60bec-a4e4-4787-9b16-20bed89d7092",  "sub": "repo:mikenomitch/nomad-gha-jwt-auth:ref:refs/heads/main:repository_owner:mikenomitch:job_workflow_ref:mikenomitch/nomad-gha-jwt-auth/.github/workflows/github-actions-demo.yml@refs/heads/main:repository_id:621402301",  "aud": "https://github.com/mikenomitch",  "ref": "refs/heads/main",  "sha": "1b568a7f1149e0699cbb89bd3e3ba040e26e5c0b",  "repository": "mikenomitch/nomad-gha-jwt-auth",  "repository_owner": "mikenomitch",  "repository_owner_id": "2732204",  "run_id": "5173139311",  "run_number": "31",  "run_attempt": "1",  "repository_visibility": "public",  "repository_id": "621402301",  "actor_id": "2732204",  "actor": "mikenomitch",  "workflow": "Nomad GHA Demo",  "head_ref": "",  "base_ref": "",  "event_name": "push",  "ref_type": "branch",  "workflow_ref": "mikenomitch/nomad-gha-jwt-auth/.github/workflows/github-actions-demo.yml@refs/heads/main",  "workflow_sha": "1b568a7f1149e0699cbb89bd3e3ba040e26e5c0b",  "job_workflow_ref": "mikenomitch/nomad-gha-jwt-auth/.github/workflows/github-actions-demo.yml@refs/heads/main",  "job_workflow_sha": "1b568a7f1149e0699cbb89bd3e3ba040e26e5c0b",  "runner_environment": "github-hosted",  "iss": "https://token.actions.githubusercontent.com",  "nbf": 1685937407,  "exp": 1685938307,  "iat": 1685938007}

(Note: If you ever want to decode or encode a JWT, jwt.io is a good tool.)

This specific JWT contains information about a GitHub workflow, including an owner, a GitHub Action name, a repository, and a branch. That is because it was issued by GitHub and is an identity token, meaning it is supposed to be used to verify the identity of this workload. Each run in a GitHub Action can be provisioned with one of these JWTs. (More on how they can be used later in this blog post.)

Importantly, aside from the information in the JSON, JWTs can be signed with a private key and verified with a public key. It is worth noting that while they are signed, their contents are still decodable by anybody, just not verified.

The public keys for JWTs can sometimes be found at idiomatically well-known URLs, such as JSON Web Key Sets (JWKs) URLs. For example, these GitHub public keys can be used to verify their identity tokens.

»JWT authentication in Nomad

Nomad can use external JWT identity tokens to issue its own Nomad ACL tokens with the JWT auth method. In order to set this up, Nomad needs:

  • Roles and/or policies that define access based on identity
  • An auth method that tells Nomad to trust JWTs from a specific source
  • A binding rule that tells Nomad how to map information from that source into Nomad concepts, like roles and policies
JWT authentication in Nomad via GitHub Actions

Here’s how to set up an authentication in Nomad to achieve the following rule:

I want any repo using an action called Nomad JWT Auth to get a Nomad ACL token that grants the action permissions for all the Nomad policies assigned to a specific role for their GitHub organization. Tokens should be valid for only one hour, and the action should be valid only for the main branch.

That may seem like a lot, but with Nomad JWT authentication, it’s actually fairly simple.

In older versions of Nomad, complex authentication like this was impossible. This forced administrators into using long-lived tokens with very high levels of permissions. If a token was leaked, admins would have to manually rotate all of their tokens stored in external stores. This made Nomad less safe and harder to manage. Now, tokens can be short-lived and after a one-time setup with identity-based rules, users don’t have to worry about managing Nomad tokens for external applications.

»Setting up JWT authentication

To set up the authentication, start by creating a simple policy that has write access to the namespace “app-dev” and another policy that has read access to the default namespace.

Create a namespace called app-dev:

nomad namespace apply "app-dev"

Write a policy file called app-developer.policy.hcl:

app-developer.policy.hcl

namespace "app-dev" {  policy = "write"} 

Then create it with this CLI command:

nomad acl policy apply -description "Access to app-dev namespace" app-developer app-developer.policy.hcl 

Write a policy file called default-read.policy.hcl:

default-read.policy.hcl

namespace "default" {  policy = "read"}

Then create it in the CLI:

nomad acl policy apply -description "Read access to default namespace" default-read default-read.policy.hcl 

Next, create roles that have access to this policy. Often these roles are team-based, such as “engineering” or “ops”, but in this case, create a role with the name of “org-” then our Github organization’s name: mikenomitch. Repositories in this organization should be able to deploy to the “app-dev” namespace, and we should be able to set up a GitHub Action to deploy them on merge.

Give this role access to the two new policies:

nomad acl role create -name="org-mikenomitch" -policy=app-developer -policy=default-read

Now, create a file defining an auth method for GitHub in auth-method.json:

auth-method.json

{  "JWKSURL": "https://token.actions.githubusercontent.com/.well-known/jwks",  "ExpirationLeeway": "1h",  "ClockSkewLeeway": "1h",  "ClaimMappings": {	"repository_owner": "repo_owner",	"repository_id": "repo_id",	"workflow": "workflow",	"ref": "ref"  }}

Then create it with the CLI:

nomad acl auth-method create -name="github" -type="JWT" -max-token-ttl="1h" -token-locality=global -config "@auth-method.json"

This tells Nomad to expect JWTs from GitHub, to verify them using the public key in JWKSURL, and to map key-value pairs found in the JWT to new names. This allows binding rules to be created using these values. A binding rule sets up the complex auth logic requirements stated in a block quote earlier in this post:

nomad acl binding-rule create \  -description 'repo name mapped to role name, on main branch, for “Nomad JWT Auth workflow"' \  -auth-method 'github' \  -bind-type 'role' \  -bind-name 'org-${value.repo_owner}' \  -selector 'value.workflow == "Nomad JWT Auth" and value.ref == "refs/heads/main"'

The selector field tells Nomad to match JWTs only with certain values in the ref, and workflow fields. The bind-type and bind-name fields tell Nomad to allow JWTs that match this selector to be matched to specific roles. In this case, they refer to roles that have a name matching the GitHub organization name. If you wanted more granular permissions, you could match role names to repository IDs using the repo_id field.

So, the JWTs for repositories in the mikenomitch organization are given an ACL token with the role org-mikenomitch, which in turn grants access to the app-developer and default-read policies.

»Nomad auth with a custom GitHub Action

Now you’re ready to use a custom GitHub Action to authenticate into Nomad. This will expose a short-lived Nomad token as an output, which can be used by another action that uses simple bash to deploy any files in the ./nomad-jobs directory to Nomad.

The code for this action is very simple, it just calls Nomad’s /v1/acl/login endpoint specifying the GitHub auth method and passes in the GitHub Action’s JWT as the login token. (See the code.)

To use this action, just push to GitHub with the following file at .github/workflows/github-actions-demo.yml

github-actions-demo.yml

name: Nomad JWT Auth on:  push:	branches:  	- main  	- master env:  PRODUCT_VERSION: "1.7.2"  NOMAD_ADDR: "https://my-nomad-addr:4646" jobs:  Nomad-JWT-Auth:	runs-on: ubuntu-latest	permissions:  	id-token: write  	contents: read	steps:  	- name: Checkout    	uses: actions/checkout@v3  	- name: Setup `nomad`    	uses: lucasmelin/setup-nomad@v1    	id: setup    	with:      	    version: ${{ env.PRODUCT_VERSION }}  	- name: Auth Into Nomad    	id: nomad-jwt-auth    	uses: mikenomitch/nomad-jwt-auth@v0.1.0    	with:      	    url: ${{ env.NOMAD_ADDR }}      	    caCertificate: ${{ secrets.NOMAD_CA_CERT }}    	continue-on-error: true  	- name: Deploy Jobs    	run: for file in ./nomad-jobs/*; do NOMAD_ADDR="${{ env.NOMAD_ADDR }}" NOMAD_TOKEN="${{ steps.nomad-jwt-auth.outputs.nomadToken }}" nomad run -detach "$file"; done

Now you have a simple CI/CD flow on GitHub Actions set up. This does not require manually managing tokens and is secured via identity-based rules and auto-expiring tokens.

»Possibilities for JWT authentication in Nomad

With the JWT auth method, you can enable efficient workflows for tools like GitHub Actions, simplifying management of Nomad tokens for external applications.

Machine-to-machine authentication is an important function in cloud infrastructure, yet implementing it correctly requires understanding several standards and protocols. Nomad’s introduction of JWT authentication methods provides the necessary building blocks to make setting up machine-to-machine auth simple. This auth method extends the authentication methods made available in Nomad 1.5, which introduced SSO and OIDC support. As organizations move towards zero trust security, Nomad users now have more choices when implementing access to their critical infrastructure.

To learn more about how HashiCorp provides a solid foundation for companies to safely migrate and secure their infrastructure, applications, and data as they move to a multi-cloud world, visit our zero trust security page.

To try the feature described in this post, download the latest version of HashiCorp Nomad.


Sign up for the latest HashiCorp news

By submitting this form, you acknowledge and agree that HashiCorp will process your personal information in accordance with the Privacy Policy.