Skip to main content
Knowledge Base

How do I securely give my CI server access to private Git repos?

Answer

A problem we run into constantly is the following: You want to run some sort of automation on your code (e.g., run automated tests or `terraform apply`) on a CI server (e.g., GitHub Actions, CircleCI, etc) against repo `foo`, but that repo includes references to another _private_ Git repo `bar`. **Example 1**: you have Terraform code in repo `foo` that uses a module from repo `bar`. ```hcl # foo/example/main.tf module "example" { # A reference to private repo bar. Note this is a Git/SSH URL. source = "git::git@github.com:acme/bar.git//example-module" } ``` **Example 2**: You have a Bash script in repo `foo` that calls `git clone` on repo `bar`. ```bash # foo/example/build.sh # A reference to private repo bar. Note this is an HTTPS URL. git clone https://github.com/acme/bar.git ``` Although most CI servers automatically give you access to the code in the repo where the build is running (e.g., `foo`), they do _not_ provide an automatic way to access code in other private repos (e.g., `bar`). **So the question is: how do you _securely_ give your CI server access to private Git repos?** - **Requirement 1: the solution must work completely non-interactively.** On your own computer, you can manually provide credentials when prompted, but on a CI server, you need an automated solution. - **Requirement 2: the solution must work with Git/SSH and HTTPS URLs.** Best practices for accessing private Git repos have changed over the years; in the past, Git/SSH URLs were typically recommended, so lots of code uses those. More recently, the tooling around HTTPS URLs has improved, and more and more code is using those. - **Requirement 3: credentials must be stored securely.** I deliberately use the term "securely" to indicate that this solution should _not_ involve storing credentials in an insecure manner: e.g., although you could put the credentials directly into your code (into the Git URLs used in `git clone`), storing credentials, in plain text, is _NOT_ secure: ```bash # foo/example/build.sh # DO NOT DO THIS. # Putting credentials directly in the code is NOT secure and is NOT a valid solution to this problem! git clone https://my-username:my-secret-password@github.com/acme/bar.git # DO NOT DO THIS. ``` So, how do you solve this? I'll share Gruntwork's recommendation below. Feedback, suggestions, and other ideas are very welcome! --- <ins datetime="2023-11-22T16:13:47Z"> <p><a href="https://support.gruntwork.io/hc/requests/110600">Tracked in ticket #110600</a></p> </ins>

Here's the approach we typically recommend. In the examples below, I'll use GitHub as the version control system and GitHub Actions as the CI server, but the same approach works with most other version control systems (e.g., GitLab, BitBucket, etc.) and CI servers (e.g., GitLab, CircleCI, etc.). 1. **Create a machine user in GitHub.** A _machine user_ is a user account that isn't used by any individual person (i.e., it is _not_ your personal GitHub account); instead, it's an account owned by your company that you use specifically for automation. Using a machine user ensures that (a) if any individual person leaves your company, all your automation doesn't suddenly break when that person loses access and (b) you can share the credentials for that account with multiple people on your team, using a secret manager tool such as 1Password, so you can make changes to your automations even if one particular person happens to be OOO. 2. **Grant the machine user access to the repos they need in automation.** In the example in the original question, you'll need to invite this machine user to repos `foo` (where the CI build will run) and `bar` (another private repo that contains code used in `foo`), and then, logged into GitHub as that machine user, accept those invites. 3. **Create a personal access token (PAT) for the machine user.** Logged in as the machine user, create a [personal access token (PAT)](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens), and ensure that token has at least read access to the repos you need in automation: in our example, that'll be repos `foo` and `bar`. You should store this PAT in a secret manager tool such as 1Password. 4. **Make the machine user username and PAT available to the CI server.** Next, you want to make the username and PAT of your machine user available to the CI build in a _secure_ manner. Most CI servers support some sort of secure way to store secrets (i.e., they are encrypted and have access controls). For example, with GitHub Actions, you can use [GitHub Actions Secrets](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions), which allow you to securely store secrets, and make them available to the build as environment variables. 5. **Update your Git configuration to use your machine user credentials.** As one of the first steps in your CI / CD build, you now need to take the machine user credentials from your CI server (as environment variables) and configure Git to use use them for all `git clone` operations. We struggled for a while to get just the right "incantation" to support all possible types of URLs until we stumbled across [this StackOverflow post](https://stackoverflow.com/questions/73840583/script-to-set-multiple-insteadof-cases-in-gitconfig) and so here, we'll show you the configuration that finally does the right thing. What you want is for your Git configuration (`~/.gitconfig`) to end up looking like this: ```ini [url "https://<MACHINE_USER_NAME>:<MACHINE_USER_PAT>@github.com"] insteadOf = ssh://git@github.com insteadOf = git@github.com insteadOf = https://github.com ``` Where `MACHINE_USER_NAME` is the username of your machine user and `MACHINE_USER_PAT` is the PAT for that machine user. This configuration uses [insteadOf](https://git-scm.com/docs/git-config#Documentation/git-config.txt-urlltbasegtinsteadOf) to tell Git that whenever it sees Git/SSH URLs (e.g., `ssh://git@github.com` and `git@github.com`) and HTTPS URLs (e.g., `https://github.com`), it should instead use an HTTPS URL with your machine user credentials in it. The best way to write such a configuration into your `~/.gitconfig` is to run the `git config` command a few times. Assuming that in GitHub Actions Secrets, you used the environment variables `MACHINE_USER_NAME` and `MACHINE_USER_PAT` for the machine user name and PAT, respectively, you can run the following commands at the start of your CI build to configure Git: ```bash git config --global --replace-all "url.https://$MACHINE_USER_NAME:$MACHINE_USER_PAT@github.com.insteadOf" ssh://git@github.com git config --global --add "url.https://$MACHINE_USER_NAME:$MACHINE_USER_PAT@github.com.insteadOf" git@github.com git config --global --add "url.https://$MACHINE_USER_NAME:$MACHINE_USER_PAT@github.com.insteadOf" https://github.com ``` 6. **Run the rest of your build!** Once you have this Git configuration in place, you can now run the rest of the build, and you'll be able to `git clone` private repos no matter how the URLs for those repos are formatted in your actual code! Try it out and let us know how it works for you!