Skip to main content

GitHub App auth for agent repos

The agent reads scopes and actions from the repos you list in AGENT_REPO. By default, a private repo is cloned with a static token embedded in its URL:

AGENT_REPO="https://<git-token>@github.com/your-org/private-repo.git#main"

That token is long-lived, tied to a user account, and stops working the day it expires or gets rotated. A GitHub App replaces it with short-lived installation tokens that the agent mints on demand and renews on its own, with no restart and no token in the URL.

You declare credentials per GitHub organization, so an agent that serves repos from several orgs uses a different App for each one.

How it works

Each time the agent runs a git operation against a repo whose org has an App configured, it asks for a fresh installation token and authenticates with it. Tokens are cached per organization and re-minted before they expire, so a long-running agent keeps pulling without any manual rotation.

note

This applies only to HTTPS github.com remotes. SSH remotes, GitHub Enterprise, and other git providers are not affected and keep using the credentials you already configured for them.

Set up the GitHub App

In the GitHub organization that owns your agent repo, create a GitHub App with:

  • Repository permissions: Contents: Read-only and Metadata: Read.
  • A new private key. Download the .pem file when GitHub shows it, since you can't retrieve it later.

Install the App on the organization and grant it access to the repos the agent needs. Then note the App ID from the App's settings page. You can also note the Installation ID (the numeric ID at the end of the URL after installing), but it's optional — the agent resolves it from the organization when you leave it out.

Repeat this for every organization whose repos the agent clones.

Configure the agent

Pass one --github-app flag per organization:

agent \
--github-app "org=acme-core,app-id=123456,installation-id=78901234,private-key=/etc/np/keys/acme-core.pem" \
--github-app "org=acme-labs,app-id=654321,private-key-ssm-parameter=/np/gh/acme-labs-key"

Flag keys

KeyRequiredDescription
orgYesGitHub organization the App is installed in. Case-insensitive
app-idYesThe App's App ID or Client ID
installation-idNoPins the installation and skips the lookup. Resolved from the org when omitted
private-keyOne of the twoPath to the App's private key .pem file
private-key-ssm-parameterOne of the twoName of an AWS SSM SecureString parameter holding the PEM

Each organization can appear only once, and each entry needs exactly one key source.

warning

Inline PEM values aren't accepted. Point the agent at a file path or an SSM parameter name.

On Kubernetes

Don't set the flag by hand. The nullplatform-agent chart takes the same credentials under github.apps, one entry per organization, and renders them into the agent's NP_GITHUB_APPS environment variable for you.

In production, keep the PEMs in a Secret you manage and point the chart at it:

github:
secret:
create: false
name: my-github-app-keys
apps:
- org: acme-core
appId: "123456"
installationId: "78901234"
privateKeySecretKey: acme-core.pem
- org: acme-labs
appId: "654321"
privateKeySsmParameter: /np/gh/acme-labs-key
ValueDescription
github.apps[].orgGitHub organization the App is installed in
github.apps[].appIdThe App's App ID or Client ID
github.apps[].installationIdOptional. Resolved from the org when unset
github.apps[].privateKeySecretKeyPEM file from the Secret. Defaults to <org>.pem
github.apps[].privateKeySsmParameterAWS SSM parameter holding the PEM. Needs IRSA on the service account
github.apps[].privateKeyInline PEM. Requires github.secret.create: true, so dev only
github.secret.createWhether the chart creates the Secret from inline privateKey values
github.secret.nameExisting Secret holding one PEM per org
github.mountPathWhere the PEMs are mounted. Defaults to /etc/nullplatform/github-apps

Each entry needs exactly one key source. Leave github.apps empty to disable GitHub App auth entirely.

info

github.apps requires chart version 3.0.0 or later. It replaces githubTokenInit, which was removed in that release.

AWS permissions for SSM private keys

If you store a private key with private-key-ssm-parameter (or privateKeySsmParameter in the chart), the agent reads it from AWS Systems Manager at startup. Store the PEM in a parameter of type SecureString, and grant the agent's IAM identity:

ActionOnWhy
ssm:GetParameterEach parameter holding a PEMReads the parameter
kms:DecryptThe KMS key that encrypts those parametersThe agent always requests the value with decryption enabled

A minimal policy for two organizations looks like this:

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "ssm:GetParameter",
"Resource": [
"arn:aws:ssm:us-east-1:123456789012:parameter/np/gh/acme-core-key",
"arn:aws:ssm:us-east-1:123456789012:parameter/np/gh/acme-labs-key"
]
},
{
"Effect": "Allow",
"Action": "kms:Decrypt",
"Resource": "arn:aws:kms:us-east-1:123456789012:key/abcd1234-a123-456a-a12b-a123b4cd56ef"
}
]
}

Keep in mind:

  • Scope the resources. List only the parameters the agent actually reads, rather than granting parameter/*.

  • Customer managed keys need both sides. If your parameters use your own KMS key, allow the agent's role in the key policy too. An IAM policy alone isn't enough.

  • On EKS, use IRSA. Attach the policy to a role and annotate the agent's service account with it:

    serviceAccount:
    annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/nullplatform-agent-irsa
  • Outside EKS, the agent uses the standard AWS credential chain, so an instance profile or the environment's configured credentials work as usual.

note

These are the same permissions the agent needs for --ssm-apikey-parameter. If you already use that, extend the existing policy with the new parameter ARNs instead of creating a second role.

If you'd rather not grant SSM access at all, use private-key and mount the PEM as a file instead.

Migrate an existing agent

You can move one organization at a time, and the agent keeps running the whole way through.

  1. Add the App entry for the org and restart the agent.

  2. Drop the token from the URL. Once the org has an App configured, the agent strips any credentials embedded in the repo URL and authenticates with the installation token instead. You can simplify AGENT_REPO to a clean URL:

    AGENT_REPO="https://github.com/your-org/private-repo.git#main"
  3. Repeat for the remaining orgs. Any org without an entry keeps using the token in its URL, and the agent logs a warning naming that org so you can see what's still pending.

Repos already on disk are re-addressed in place, so there's no need to wipe the agent's working directory.

tip

Rolling back is just as simple. Remove the org's --github-app entry, put the token back in the repo URL, and restart.

Security notes

  • Private keys and minted tokens stay in memory. They're never written to disk or to logs.
  • Clone and fetch errors have any credentials in the repo URL redacted before they reach the logs.
  • Installation tokens are short-lived and scoped to the repos you granted the App, unlike a personal access token tied to a user account.