How to build Go projects with multiple private dependencies in GitHub Actions

November 30, 2021 in Golang

Note: the same approach should work for any other environment that can download dependencies from Git - Ruby’s Bundler, JavaScript’s NPM, and so on.

Building with private dependencies feels like a Mission Impossible. What mechanism to use to give access? What config files to check? How to make sense of cryptic git error messages?

But fear not - there is a path without giving up private repos or giving out access tokens with broad permissions.

1. Allow access to dependency repos

The least compromising method to give access from one repo to another is deploy keys.

A deploy key is simply an SSH key allowing read-only access to a specific repo. (A characteristic feature is that the same deploy key can only be used to access one repo.) With deploy keys, if a repo’s secrets are compromised, the attacker only gains read-only access to its dependencies’ source code.

I suggest you create a new deploy key for every repo that needs access (although it is possible to use the same key from different places.)

For example, for 2 projects that share the same 3 private dependencies, you need 6 keys.

So, let’s create some deploy keys.

ssh-keygen -N '' -C '[email protected]:myorg/dep.git' -f dep.key

Then, go to “Settings” -> “Deploy keys” in the dependency repo, and add the public key (the one from the .pub file). Make sure to note the destination repo’s name in the title of the key, so you can easily revoke access later:

Repeat to generate and add keys for all dependencies. Keep private keys at hand, we will use them shortly.

2. Set up the project repo’s secrets

Now we can move to the project repo, the one that you want to build with those private dependencies.

Firstly, go to “Settings” -> “Secrets” and add every private key you created in step 1 as a separate secret:

Now you have DEP1_DEPLOY_KEY, DEP2_DEPLOY_KEY, and so on.

3. Set up the GitHub Actions workflow

To set up your workflow to use the keys to access repos, I suggest using the wonderful ssh-agent action. (The alternative is setting up host aliases in SSH config, and repo URL aliases in gitconfig, but ssh-agent covers everything with minimal configuration).

Among other features, ssh-agent can handle keys to multiple repos with no additional configuration.

Add such a step before any steps that do go get or go mod download from the private repos. (You only need it once per workflow job.)

- uses: webfactory/[email protected]
  with:
    ssh-private-key: |
      ${{ secrets.DEP1_DEPLOY_KEY }}
      ${{ secrets.DEP2_DEPLOY_KEY }}      

Remember in step 1, I asked you to set the key comment to the repo’s URL? This is used by the ssh-agent action to configure all the proper aliases for keys to work. If you check the action’s output, it should say something like:

Added deploy-key mapping: Use identity '/home/runner/.ssh/key-abc' for GitHub repository myorg/dep1
Added deploy-key mapping: Use identity '/home/runner/.ssh/key-cde' for GitHub repository myorg/dep2

4. Mark Go modules as private

And the final essential setup step is letting Go know that the modules are private. For this, set the GOPRIVATE environment variable.

Private, in this context, means that Go will always download the module directly from the repo, and will not attempt to checksum it against public records. (So, only add trusted modules to GOPRIVATE.)

In every job that downloads Go modules, set the GOPRIVATE environment variable:

env:
  GOPRIVATE: github.com/myorg # covers all repos under your org

or, you can enumerate every repo individually:

env:
  GOPRIVATE: github.com/myorg/dep1,github.com/myorg/dep2

Compatibility with developer’s machines

In case you haven’t already discovered this, developers also need to set GOPRIVATE on their machine. Other than that, they don’t need any SSH or Git setup, assuming they already have access to the repos.

Troubleshooting

Final words

If you follow everything correctly, builds with private dependencies are easy to set up and secure.

To build multiple project this way, just repeat the instructions for each one.

And to revoke access, it’s enough to remove the deploy key from a dependency repo - the private key would then have no access and pose no threat.

Buy me a coffee Liked the post? Treat me to a coffee