From Machine Users to GitHub Apps

Mike Gouline
5 min readNov 13, 2023

Whenever a shared machine, such as a build server, needs access to your GitHub organisation, we traditionally opted for personal access tokens (PATs) or SSH keys created against a machine user. This works until you consider the security implications of not being able to attribute actions made by that user to any of the humans with access to it.

GitHub Apps is a more secure alternative, where apps can be installed in your organisation — not only a user — and granted granular permissions to repositories, packages, issues, etc. Unfortunately, the setup is slightly more involved than for PATs and SSH keys, so I decided to write up my experience with it.

Photo by Roman Synkevych on Unsplash

Alternatives

Before diving into GitHub Apps, I should mention that if your shared machine only requires access to a single repository, consider using deploy keys instead — they are SSH keys that you configure a repository, instead of a user or organisation. Note that while you can have many keys for one repository, you can only associate each key with one repository at a time. As a result, this is not a good solution for, say, a build server that needs to clone all repositories in your organisation.

If you are using a cloud service, also check that it does not already provide a GitHub App that you can install. With GitHub’s popularity, this covers many use cases.

App Setup

Initial configuration involves registering a new generic GitHub App and then installing it in your specific organisation. This will happen entirely in the web interface.

Register Your App

Go to your user or organisation settings, expand the “Developer settings” (at the bottom of the sidebar) and click “GitHub Apps”.

Now click “New GitHub App” and fill in the following sections (more information can be found here):

  • GitHub App nameglobally-unique name that describes your organisation and the purpose of this integration, e.g. “Acme CI”
  • Homepage URL — this app will remain private, so the homepage can be any valid URL, e.g. your company website
  • Webhook — uncheck “Active”, we will not use it
  • Permissions — configure all permissions that your shared machine needs, e.g. to clone repositories, you need at least “Read-only” access on “Contents” under “Repository permissions” (read more here)
  • Where can this GitHub App be installed? — if you are registering this app from a different user or organisation than where you will be installing it, select “Any account”

Save your new app by clicking “Create GitHub App”.

Install Your App

Inside your newly-registered app, note down the “App ID” for later.

Scroll down to “Private keys” and click “Generate a private key”. New PEM file will start downloading — you must keep it safe, it will be used for authentication. If it does get lost or compromised, you can always delete it and generate a new one.

Finally, open “Install App” in the sidebar and click “Install” next to your organisation. This grants your new app the permissions on your specific organisation that you configured in the previous section.

Authentication

Now that your app is installed, let’s look at authenticating against it from your shared machine. I will describe a simple custom implementation, to help you understand all the steps involved, but there is an easier way at the end if the Git client is all you need.

Custom Implementation

This sample is implemented in Python using jwt and requests packages, but any other alternatives would work just as well.

Note that I am omitting the retrieval of your organisation name, app ID and private key file, created in the previous section, because that is dependent on your environment. For example, you may want to use the secrets manager in your operating system or cloud provider.

We start by preparing your JSON web token (JWT) for authenticating against the GitHub API (you can read more here).

import jwt
import time

def get_encoded_jwt(app_id, private_key_path):
with open(private_key_path, "rb") as pem_file:
signing_key = jwt.jwk_from_pem(pem_file.read())

return jwt.JWT().encode(
payload={
"iat": int(time.time()),
"exp": int(time.time()) + 10 * 60, # 10 mins (maximum)
"iss": app_id,
},
key=signing_key,
alg="RS256",
)

First request retrieves the API URL to request a new access token for the installation in your organisation.

def get_access_token_url(encoded_jwt, org):
resp = requests.get(
url="https://api.github.com/app/installations",
headers={
"Accept": "application/vnd.github+json",
"Authorization": f"Bearer {encoded_jwt}",
"X-GitHub-Api-Version": "2022-11-28",
},
)
for installation in resp.json():
if installation["account"]["login"] == org:
return installation.get("access_tokens_url")
return None

The URL will be of the form https://github.com/app/installations/INSTALLATION_ID/access_tokens and you can alternatively find that INSTALLATION_ID by clicking the ⚙ (cog) icon next to your organisation in the “Install App” section of your app, and checking the end of that URL, e.g. .../installations/INSTALLATION_ID.

Now we can use that URL to request a new installation access token (see endpoint for details).

def get_access_token(encoded_jwt, access_token_url):
resp = requests.post(
url=access_tokens_url,
headers={
"Accept": "application/vnd.github+json",
"Authorization": f"Bearer {encoded_jwt}",
"X-GitHub-Api-Version": "2022-11-28",
},
)
return resp.json().get("token")

That’s it! You can now authenticate with GitHub using this token. Note that it expires after an hour, so your implementation will need to periodically refresh it.

When authenticating using anything that takes a username and a password (e.g. Git client), the access token is the password and x-access-token is the username.

Git Client

For the native Git client, used either directly or through another tool that calls out to it, you can configure a credential helper instead.

In a nutshell, Git allows you to configure any executable called git-credential-somename in your PATH to dynamically fetch credentials according to any custom logic you like.

There are several available for GitHub Apps, I prefer this one written in Go — https://github.com/Avinode/git-credential-github-apps. All you have to do is extract your platform-appropriate git-credential-github-apps binary the releases, place it somewhere under your PATH (e.g. /usr/local/bin) and execute the following:

git config --global credential.helper 'github-apps -privatekey <path to private key> -appid <App ID> -login <organization>'

Ensure that your user has a ~/.cache directory — that’s where the helper stores cached credentials until they expire.

If your existing setup clones repositories via SSH and you want backwards compatibility, you can force HTTPS URL replacement:

git config --global url."https://github.com/".insteadOf "git@github.com:"

Your Git client can now clone repositories as normal. This should also work for CLI tools that execute Git commands implicitly, but your results may vary and I recommend some testing.

--

--