Skip to content
rodolfo.gg
Go back

How to use Go modules from private GitHub repositories.

CC BY-NC-ND 4.0
Rodolfo González González

How to use Go modules from private GitHub repositories.

The following tutorial explains how to use Go modules located in private repositories (e.g. GitHub).

Table of contents

Table of contents

Introduction

In 2018, the Go team introduced “modules” as the new standard for managing dependencies in Go projects. A module groups related packages that are versioned together and includes everything needed to compile a codebase. Since Go 1.11, with Go Modules, this management became simpler, more flexible, and officially supported by the project. Among its main capabilities are:

With Go Modules there is no longer a need to place code inside $GOPATH, a restriction that previously constrained Go development. This approach offers a more flexible project structure and makes it easier to work across different repositories.

Go modules provide a powerful way to reuse your own and third-party components in your programs. The Go standard library includes a large number of packages, but in many cases modules from other authors are needed for specific functions, or modules developed within the organization that, being proprietary software, we do not want to publish.

This raises a problem: a module hosted in a public repository, e.g. on GitHub, can be easily installed with:

go get github.com/gofiber/fiber/v3

Normal go get flow (public module on GitHub):

┌──────────────┐
│ Developer │
│ go get ... │
└──────┬───────┘
v
┌──────────────┐
│ Go command │
│ (resolve mod │
│ and version) │
└──────┬───────┘
│ queries
v
┌─────────────────────────┐
│ proxy.golang.org │
│ (cached zip/mod/info) │
└──────┬──────────────────┘
│ verifies checksums
v
┌──────────────────────────┐
│ sum.golang.org │
│ (transparency & integr.) │
└──────┬───────────────────┘
│ downloads module
v
┌─────────────────────────┐
│ Local module cache │
│ $GOMODCACHE │
└──────┬──────────────────┘
│ updates
v
┌─────────────────────────┐
│ go.mod / go.sum │
└─────────────────────────┘

But a module located in a private repository (e.g. on GitHub) often fails when running go get. This happens because Go tries to resolve dependencies with the public proxy and checksum database by default, and also Git (used internally by go get) needs valid credentials to clone the private repository. If you don’t configure authentication (HTTPS with token or SSH) and variables like GOPRIVATE, the download ends in access errors.

Context

ElementValue
Repositoryhttps://github.com/myorg/my-go-module (private)
ConsumersInternal Go programs of myorg
Development OSDebian 13 (Trixie)

Developers already belong to a team of the myorg organization with write permission on the repository. The guide assumes that each developer runs the steps on their own machine.


Which flow is right for me?

HTTPS + TokenSSH (personal key)SSH (deploy key)Hybrid (deploy key + HTTPS)
AuthenticationPersonal Access Token (PAT)User key pairPer-repository key pairDeploy key for reading, PAT for writing
ScopeAll user reposAll user reposSingle repositoryRead: one repo; write: all PAT repos
Centralized revocationAdmin revokes tokens from GitHub consoleAdmin removes user from teamAdmin deletes deploy key from repoCombines both mechanisms
ExpirationMandatory for fine-grained, configurable for classicKeys don’t expire by defaultKeys don’t expire by defaultDepends on token and key type
Corporate firewallsAlways works (port 443)May fail if port 22 is blockedSame as personal SSHRead: port 22; write: port 443
Credentials per hostOne per github.com; token must cover all reposNo conflictRequires alias in ~/.ssh/config per repoRequires alias + credential helper
Secret on diskToken in ~/.git-credentials (plain text)Private key in ~/.ssh/ (with passphrase)Same as personal SSHBoth: key + token
CI/CDSimpler: a string in a secretRequires mounting key fileIdeal: minimal access to one repoUncommon in CI/CD
Who configuresEach developer creates their tokenEach developer creates their keyAdmin adds the public key; dev generates the pairAdmin adds deploy key; dev configures both channels

Choose a flow and follow only that section. Each one is self-contained.


Flow A — HTTPS with Personal Access Token


A1. Packages

Terminal window
sudo apt update
sudo apt install -y git golang-go

Verify:

Terminal window
git --version # ≥ 2.x
go version # ≥ 1.21

A2. Personal Access Token (PAT)

Since ~/.git-credentials stores one credential per host, the token you use for github.com must cover all repositories you work with (public and private), not just my-go-module. Otherwise, Git will present the restricted token for any other repo and you’ll get 403.

If you already have a Classic PAT with scope repo

You don’t need to create another token. That token already covers all repositories your user has access to. Go directly to step A3.

Verify in Settings → Developer settings → Personal access tokens → Tokens (classic) that the repo scope is checked.

If you need to create a new token

Choose one variant:

Fine-grained token (recommended)

  1. Go to Settings → Developer settings → Personal access tokens → Fine-grained tokens → Generate new token.
  2. Configure:
    • Token name: something descriptive, e.g. dev-myorg.
    • Expiration: according to your organization’s policy.
    • Resource owner: select myorg.
    • Repository access: select All repositories.
    • Permissions → Repository permissions:
      • Contents: Read and write.
      • Metadata: Read-only (enabled automatically).
  3. Click Generate token and copy the token (you won’t see it again).

Classic token

  1. Go to Settings → Developer settings → Personal access tokens → Tokens (classic) → Generate new token (classic).
  2. Configure:
    • Note: something descriptive, e.g. dev-myorg.
    • Expiration: according to your organization’s policy.
    • Scopes: check repo.
  3. Click Generate token and copy the token.

A3. Credential Helper

The goal is for both git and go get to use the token transparently, without prompting you every time. Choose one option:

Option 1 — git-credential-store (file on disk, simpler)

Terminal window
git config --global credential.helper store

If ~/.git-credentials already existed with an entry for github.com, it must be replaced (Git uses the first match):

Terminal window
sed -i '\|://.*@github\.com|d' ~/.git-credentials 2>/dev/null

Store the credential:

Terminal window
# Replace YOUR_USERNAME and YOUR_TOKEN
printf 'protocol=https\nhost=github.com\nusername=YOUR_USERNAME\npassword=YOUR_TOKEN\n' \
| git credential approve

Verify and protect:

Terminal window
cat ~/.git-credentials
# You should see: https://YOUR_USERNAME:YOUR_TOKEN@github.com
chmod 600 ~/.git-credentials

Option 2 — git-credential-cache (in memory, more secure)

Terminal window
git config --global credential.helper 'cache --timeout=28800'

It will remember credentials for 8 hours. You’ll need to re-enter them when they expire or on restart.

Option 3 — gh auth (GitHub CLI)

Terminal window
sudo apt install -y gh
gh auth login # Choose HTTPS, paste your PAT
gh auth setup-git # Register gh as credential helper

A4. Force HTTPS for Go

Go may try to use SSH when resolving modules. To always use HTTPS:

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

Verify:

Terminal window
git config --global --get-regexp url
# url.https://github.com/.insteadof ssh://git@github.com/
# url.https://github.com/.insteadof git@github.com:

A5. Go variables for private modules

Go tries to validate modules against the public proxy and checksum database. Both fail with private repos. List only the private Go modules you use as dependencies, not all org repos:

Terminal window
cat >> ~/.bashrc << 'EOF'
# --- Go: private modules from myorg ---
export GOPRIVATE="github.com/myorg/my-go-module"
export GONOSUMDB="github.com/myorg/my-go-module"
export GONOSUMCHECK="github.com/myorg/my-go-module"
EOF
source ~/.bashrc
VariableEffect
GOPRIVATETells Go that the listed modules are private. Implies GONOSUMDB and GONOPROXY.
GONOSUMDBDoes not query sum.golang.org for these modules.
GONOSUMCHECKDoes not verify checksums against the public database.

Verify:

Terminal window
go env GOPRIVATE GONOSUMDB GONOSUMCHECK
# All three should show github.com/myorg/my-go-module

A6. Verify

Terminal window
# Git
git clone https://github.com/myorg/my-go-module.git /tmp/test-clone
rm -rf /tmp/test-clone
# Go
mkdir /tmp/test-go && cd /tmp/test-go
go mod init github.com/myorg/test-app
go get github.com/myorg/my-go-module@latest
rm -rf /tmp/test-go

A7. Daily workflow

Terminal window
git clone https://github.com/myorg/my-go-module.git
cd my-go-module
git checkout -b feature/new-function
# ... edit files ...
git add .
git commit -m "feat: new function XYZ"
git push origin feature/new-function

A8. Publish a new version

Terminal window
git checkout main && git pull
git tag v0.2.0
git push origin v0.2.0

Consumers update with:

Terminal window
go get github.com/myorg/my-go-module@v0.2.0

A9. Quick summary (all in one block)

Terminal window
sudo apt install -y git golang-go
# Credential helper (option 1)
git config --global credential.helper store
sed -i '\|://.*@github\.com|d' ~/.git-credentials 2>/dev/null
printf 'protocol=https\nhost=github.com\nusername=MY_USERNAME\npassword=MY_TOKEN\n' \
| git credential approve
chmod 600 ~/.git-credentials
# Force HTTPS for Go
git config --global url."https://github.com/".insteadOf "ssh://git@github.com/"
git config --global url."https://github.com/".insteadOf "git@github.com:"
# Go variables
cat >> ~/.bashrc << 'EOF'
# If you have multiple private modules, separate them with commas:
# export GOPRIVATE="github.com/myorg/my-go-module,github.com/myorg/another-module"
export GOPRIVATE="github.com/myorg/my-go-module"
export GONOSUMDB="github.com/myorg/my-go-module"
export GONOSUMCHECK="github.com/myorg/my-go-module"
EOF
source ~/.bashrc

A10. Troubleshooting

SymptomProbable causeSolution
410 Gone on go getGo tries to use proxy.golang.orgVerify that GOPRIVATE includes github.com/myorg/my-go-module
Checksum SECURITY ERRORGONOSUMCHECK is not definedAdd export GONOSUMCHECK="github.com/myorg/my-go-module"
go get downloads old versionGo local cacheRun go clean -modcache and retry
fatal: could not read UsernameCredential helper not configuredRun step A3
403 ForbiddenToken without sufficient permissionsVerify that the PAT has Contents: Read and write permission
403 on repos that worked beforeFine-grained token restricted to specific reposRecreate with All repositories or use Classic PAT with scope repo
Go tries to connect via SSHMissing insteadOf ruleRun step A4


Flow B — SSH with personal key


B1. Packages

Terminal window
sudo apt update
sudo apt install -y git golang-go openssh-client

Verify:

Terminal window
git --version # ≥ 2.x
go version # ≥ 1.21
ssh -V # OpenSSH ≥ 9.x

B2. Generate the SSH key

If you already have a key pair in ~/.ssh/id_ed25519, you can reuse it. If not:

Terminal window
ssh-keygen -t ed25519 -C "your-email@example.com"

Accept the default path and set a passphrase.

Verify:

Terminal window
ls -l ~/.ssh/id_ed25519 ~/.ssh/id_ed25519.pub

B3. Add the public key to GitHub

Copy the public key:

Terminal window
cat ~/.ssh/id_ed25519.pub

On GitHub:

  1. Go to Settings → SSH and GPG keys → New SSH key.
  2. Configure:
    • Title: e.g. debian-dev-myorg.
    • Key type: Authentication Key.
    • Key: paste the content.
  3. Click Add SSH key.

B4. Configure ssh-agent

Terminal window
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519

For auto-start on each session:

Terminal window
cat >> ~/.bashrc << 'AGENT'
# --- Automatic SSH agent ---
if [ -z "$SSH_AUTH_SOCK" ]; then
eval "$(ssh-agent -s)" > /dev/null
ssh-add ~/.ssh/id_ed25519 2>/dev/null
fi
AGENT
source ~/.bashrc

B5. Force SSH for Go

Go may try HTTPS when resolving modules. To always use SSH:

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

Verify:

Terminal window
git config --global --get-regexp url
# url.git@github.com:.insteadof https://github.com/

B6. Go variables for private modules

Terminal window
cat >> ~/.bashrc << 'EOF'
# --- Go: private modules from myorg ---
export GOPRIVATE="github.com/myorg/my-go-module"
export GONOSUMDB="github.com/myorg/my-go-module"
export GONOSUMCHECK="github.com/myorg/my-go-module"
EOF
source ~/.bashrc

Verify:

Terminal window
go env GOPRIVATE GONOSUMDB GONOSUMCHECK
# All three should show github.com/myorg/my-go-module

B7. Verify

Terminal window
# SSH connection
ssh -T git@github.com
# Expected response:
# Hi YOUR_USERNAME! You've been authenticated, but GitHub does not provide shell access.
# Git
git clone git@github.com:myorg/my-go-module.git /tmp/test-clone
rm -rf /tmp/test-clone
# Go
mkdir /tmp/test-go && cd /tmp/test-go
go mod init github.com/myorg/test-app
go get github.com/myorg/my-go-module@latest
rm -rf /tmp/test-go

B8. Daily workflow

Terminal window
git clone git@github.com:myorg/my-go-module.git
cd my-go-module
git checkout -b feature/new-function
# ... edit files ...
git add .
git commit -m "feat: new function XYZ"
git push origin feature/new-function

B9. Publish a new version

Terminal window
git checkout main && git pull
git tag v0.2.0
git push origin v0.2.0

Consumers update with:

Terminal window
go get github.com/myorg/my-go-module@v0.2.0

B10. Quick summary (all in one block)

Terminal window
sudo apt install -y git golang-go openssh-client
ssh-keygen -t ed25519 -C "your-email@example.com"
# → Upload ~/.ssh/id_ed25519.pub to GitHub (Settings → SSH and GPG keys)
# Automatic SSH agent
cat >> ~/.bashrc << 'AGENT'
if [ -z "$SSH_AUTH_SOCK" ]; then
eval "$(ssh-agent -s)" > /dev/null
ssh-add ~/.ssh/id_ed25519 2>/dev/null
fi
AGENT
# Force SSH for Go
git config --global url."git@github.com:".insteadOf "https://github.com/"
# Go variables
cat >> ~/.bashrc << 'EOF'
# If you have multiple private modules, separate them with commas:
# export GOPRIVATE="github.com/myorg/my-go-module,github.com/myorg/another-module"
export GOPRIVATE="github.com/myorg/my-go-module"
export GONOSUMDB="github.com/myorg/my-go-module"
export GONOSUMCHECK="github.com/myorg/my-go-module"
EOF
source ~/.bashrc

B11. Troubleshooting

SymptomProbable causeSolution
410 Gone on go getGo tries to use proxy.golang.orgVerify that GOPRIVATE includes github.com/myorg/my-go-module
Checksum SECURITY ERRORGONOSUMCHECK is not definedAdd export GONOSUMCHECK="github.com/myorg/my-go-module"
go get downloads old versionGo local cacheRun go clean -modcache and retry
Permission denied (publickey)Key not loaded in agent or not registered on GitHubRun ssh-add -l; check the key on GitHub
ssh: connect to host github.com port 22: Connection refusedFirewall blocks port 22Add to ~/.ssh/config: Host github.com / Hostname ssh.github.com / Port 443 / User git
Go tries to connect via HTTPSMissing insteadOf ruleRun step B5
Host key verification failedFirst connection to GitHubRun ssh-keyscan github.com >> ~/.ssh/known_hosts


Flow C — SSH with deploy key


C1. Packages

Terminal window
sudo apt update
sudo apt install -y git golang-go openssh-client

Verify:

Terminal window
git --version # ≥ 2.x
go version # ≥ 1.21
ssh -V # OpenSSH ≥ 9.x

C2. Generate a dedicated key pair

Use a filename that identifies the repo:

Terminal window
ssh-keygen -t ed25519 -C "deploy-my-go-module" \
-f ~/.ssh/deploy_my-go-module

Set a passphrase if desired (recommended on development machines; in CI/CD it is usually left empty).


C3. Register the public key in the repository

Copy the public key:

Terminal window
cat ~/.ssh/deploy_my-go-module.pub

On GitHub:

  1. Go to the repository myorg/my-go-module → Settings → Deploy keys → Add deploy key.
  2. Configure:
    • Title: e.g. dev-rodolfo or ci-build-server.
    • Key: paste the content.
    • Allow write access: check it if you need to push. Leave it unchecked if you only need to consume the module with go get.
  3. Click Add key.

C4. Alias in ~/.ssh/config

With a deploy key, SSH needs to know which key to use for which repo. This is solved with a host alias:

Terminal window
cat >> ~/.ssh/config << 'SSHCFG'
# --- Deploy key: myorg/my-go-module ---
Host github-my-go-module
HostName github.com
User git
IdentityFile ~/.ssh/deploy_my-go-module
IdentitiesOnly yes
SSHCFG
chmod 600 ~/.ssh/config
DirectiveFunction
Host github-my-go-moduleArbitrary alias you will use in Git URLs and in insteadOf.
HostName github.comThe real server SSH will connect to.
User gitGitHub always uses the git user for SSH.
IdentityFileThe private key of the deploy key.
IdentitiesOnly yesPrevents ssh-agent from offering other keys.

C5. Configure ssh-agent

Terminal window
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/deploy_my-go-module

For auto-start on each session:

Terminal window
cat >> ~/.bashrc << 'AGENT'
# --- Automatic SSH agent (deploy key my-go-module) ---
if [ -z "$SSH_AUTH_SOCK" ]; then
eval "$(ssh-agent -s)" > /dev/null
ssh-add ~/.ssh/deploy_my-go-module 2>/dev/null
fi
AGENT
source ~/.bashrc

C6. Redirect Go to the SSH alias

With deploy keys, the insteadOf rule must be per repository:

Terminal window
git config --global url."git@github-my-go-module:myorg/my-go-module".insteadOf \
"https://github.com/myorg/my-go-module"

When Go tries to download https://github.com/myorg/my-go-module, Git will rewrite it to the alias, SSH will look up the corresponding Host block and use the deploy key.

Verify:

Terminal window
git config --global --get-regexp url
# url.git@github-my-go-module:myorg/my-go-module.insteadof https://github.com/myorg/my-go-module

C7. Go variables for private modules

Terminal window
cat >> ~/.bashrc << 'EOF'
# --- Go: private modules from myorg ---
export GOPRIVATE="github.com/myorg/my-go-module"
export GONOSUMDB="github.com/myorg/my-go-module"
export GONOSUMCHECK="github.com/myorg/my-go-module"
EOF
source ~/.bashrc

Verify:

Terminal window
go env GOPRIVATE GONOSUMDB GONOSUMCHECK
# All three should show github.com/myorg/my-go-module

C8. Verify

Terminal window
# SSH connection with the alias
ssh -T github-my-go-module
# Expected response (note it shows the REPO, not a user):
# Hi myorg/my-go-module! You've been authenticated, but GitHub does not
# provide shell access.
# Git
git clone git@github-my-go-module:myorg/my-go-module.git /tmp/test-clone
rm -rf /tmp/test-clone
# Go
mkdir /tmp/test-go && cd /tmp/test-go
go mod init github.com/myorg/test-app
go get github.com/myorg/my-go-module@latest
rm -rf /tmp/test-go

C9. Daily workflow

Terminal window
git clone git@github-my-go-module:myorg/my-go-module.git
cd my-go-module
git checkout -b feature/new-function
# ... edit files ...
git add .
git commit -m "feat: new function XYZ"
git push origin feature/new-function

C10. Publish a new version

Terminal window
git checkout main && git pull
git tag v0.2.0
git push origin v0.2.0

Consumers update with:

Terminal window
go get github.com/myorg/my-go-module@v0.2.0

C11. Quick summary (all in one block)

Terminal window
sudo apt install -y git golang-go openssh-client
# Generate deploy key
ssh-keygen -t ed25519 -C "deploy-my-go-module" -f ~/.ssh/deploy_my-go-module
# → Admin adds ~/.ssh/deploy_my-go-module.pub
# to myorg/my-go-module → Settings → Deploy keys
# SSH alias
cat >> ~/.ssh/config << 'SSHCFG'
Host github-my-go-module
HostName github.com
User git
IdentityFile ~/.ssh/deploy_my-go-module
IdentitiesOnly yes
SSHCFG
chmod 600 ~/.ssh/config
# Automatic SSH agent
cat >> ~/.bashrc << 'AGENT'
if [ -z "$SSH_AUTH_SOCK" ]; then
eval "$(ssh-agent -s)" > /dev/null
ssh-add ~/.ssh/deploy_my-go-module 2>/dev/null
fi
AGENT
# Redirect Go to the SSH alias
git config --global url."git@github-my-go-module:myorg/my-go-module".insteadOf \
"https://github.com/myorg/my-go-module"
# Go variables
cat >> ~/.bashrc << 'EOF'
# If you have multiple private modules, separate them with commas:
# export GOPRIVATE="github.com/myorg/my-go-module,github.com/myorg/another-module"
export GOPRIVATE="github.com/myorg/my-go-module"
export GONOSUMDB="github.com/myorg/my-go-module"
export GONOSUMCHECK="github.com/myorg/my-go-module"
EOF
source ~/.bashrc

C12. Troubleshooting

SymptomProbable causeSolution
410 Gone on go getGo tries to use proxy.golang.orgVerify that GOPRIVATE includes github.com/myorg/my-go-module
Checksum SECURITY ERRORGONOSUMCHECK is not definedAdd export GONOSUMCHECK="github.com/myorg/my-go-module"
go get downloads old versionGo local cacheRun go clean -modcache and retry
ssh -T github-my-go-module says Permission deniedKey not loaded, alias misconfigured, or deploy key not registeredVerify with ssh-add -l; check ~/.ssh/config; confirm deploy key on GitHub
ssh -T responds with your username instead of the repossh-agent offers your personal keyAdd IdentitiesOnly yes to the Host block in ~/.ssh/config
go get requests HTTPS authenticationMissing insteadOf ruleRun step C6
ERROR: Repository not foundDeploy key deleted or alias points to wrong repoConfirm in Settings → Deploy keys and check ~/.ssh/config
Key already in use when addingGitHub does not allow the same key in more than one repoGenerate a separate pair per repository
Push rejected (Write access denied)Deploy key without write permissionAdmin enables Allow write access, or use Flow D (hybrid)
ssh: connect to host github.com port 22: Connection refusedFirewall blocks port 22Add to ~/.ssh/config inside the Host block: Hostname ssh.github.com / Port 443
Host key verification failedFirst connection to GitHubRun ssh-keyscan github.com >> ~/.ssh/known_hosts


Flow D — Hybrid: deploy key (read) + HTTPS (write)

This flow is for when the deploy key is configured as read-only (ideal for go get) but you need to push for development. It combines SSH for reads with HTTPS for writes.


D1. Packages

Terminal window
sudo apt update
sudo apt install -y git golang-go openssh-client

Verify:

Terminal window
git --version # ≥ 2.x
go version # ≥ 1.21
ssh -V # OpenSSH ≥ 9.x

D2. Generate the deploy key

Terminal window
ssh-keygen -t ed25519 -C "deploy-my-go-module" \
-f ~/.ssh/deploy_my-go-module

D3. Register the deploy key as read-only

Copy the public key:

Terminal window
cat ~/.ssh/deploy_my-go-module.pub

On GitHub:

  1. Go to myorg/my-go-module → Settings → Deploy keys → Add deploy key.
  2. Configure:
    • Title: e.g. dev-rodolfo-readonly.
    • Key: paste the content.
    • Allow write access: DO NOT check it (read-only).
  3. Click Add key.

D4. Alias in ~/.ssh/config

Terminal window
cat >> ~/.ssh/config << 'SSHCFG'
# --- Deploy key: myorg/my-go-module (read-only) ---
Host github-my-go-module
HostName github.com
User git
IdentityFile ~/.ssh/deploy_my-go-module
IdentitiesOnly yes
SSHCFG
chmod 600 ~/.ssh/config

D5. Configure ssh-agent

Terminal window
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/deploy_my-go-module

For auto-start:

Terminal window
cat >> ~/.bashrc << 'AGENT'
# --- Automatic SSH agent (deploy key my-go-module) ---
if [ -z "$SSH_AUTH_SOCK" ]; then
eval "$(ssh-agent -s)" > /dev/null
ssh-add ~/.ssh/deploy_my-go-module 2>/dev/null
fi
AGENT
source ~/.bashrc

D6. Personal Access Token for writing

Pushes will go via HTTPS, so you need a PAT configured in a credential helper. If you already have one working, you can reuse it. If not:

Create the token

Follow the instructions in step A2 (Classic with scope repo or fine-grained with All repositories and Contents: Read and write).

Configure the credential helper

Terminal window
git config --global credential.helper store
# Remove previous github.com entry
sed -i '\|://.*@github\.com|d' ~/.git-credentials 2>/dev/null
# Store the credential
printf 'protocol=https\nhost=github.com\nusername=YOUR_USERNAME\npassword=YOUR_TOKEN\n' \
| git credential approve
chmod 600 ~/.git-credentials

D7. insteadOf + pushInsteadOf rules

Here is the key piece. Git applies insteadOf first, and then pushInsteadOf overrides only for write operations:

Terminal window
# Reads (go get, git pull, git clone) → SSH deploy key
git config --global url."git@github-my-go-module:myorg/my-go-module".insteadOf \
"https://github.com/myorg/my-go-module"
# Pushes → back to HTTPS (uses PAT from credential helper)
git config --global url."https://github.com/myorg/my-go-module".pushInsteadOf \
"git@github-my-go-module:myorg/my-go-module"

The resulting flow:

OperationRewrite chainAuthentication
go getHTTPS → SSH (deploy key)Deploy key, read-only
git pullHTTPS → SSH (deploy key)Deploy key, read-only
git cloneHTTPS → SSH (deploy key)Deploy key, read-only
git pushHTTPS → SSH → HTTPSPAT, read/write

The developer uses normal HTTPS URLs for everything, and Git resolves underneath which credential to use based on the direction of the operation.

Verify:

myorg/my-go-module
git config --global --get-regexp url
# url.git@github-my-go-module:myorg/my-go-module.insteadof https://github.com/myorg/my-go-module

D8. Go variables for private modules

Terminal window
cat >> ~/.bashrc << 'EOF'
# --- Go: private modules from myorg ---
export GOPRIVATE="github.com/myorg/my-go-module"
export GONOSUMDB="github.com/myorg/my-go-module"
export GONOSUMCHECK="github.com/myorg/my-go-module"
EOF
source ~/.bashrc

Verify:

Terminal window
go env GOPRIVATE GONOSUMDB GONOSUMCHECK
# All three should show github.com/myorg/my-go-module

D9. Verify

Terminal window
# SSH connection (deploy key)
ssh -T github-my-go-module
# Expected response (shows the REPO, not a user):
# Hi myorg/my-go-module! You've been authenticated, but GitHub does not
# provide shell access.
# Go (read via deploy key)
mkdir /tmp/test-go && cd /tmp/test-go
go mod init github.com/myorg/test-app
go get github.com/myorg/my-go-module@latest
rm -rf /tmp/test-go
# Git clone (read via deploy key) + push (write via HTTPS)
git clone https://github.com/myorg/my-go-module.git /tmp/test-push
cd /tmp/test-push
git checkout -b test-push-verify
git commit --allow-empty -m "test: verify hybrid push"
git push origin test-push-verify
# If the push works, the configuration is correct.
# Clean up the test branch:
git push origin --delete test-push-verify
rm -rf /tmp/test-push

D10. Daily workflow

Terminal window
# Clone (goes via deploy key thanks to insteadOf, but transparent)
git clone https://github.com/myorg/my-go-module.git
cd my-go-module
git checkout -b feature/new-function
# ... edit files ...
git add .
git commit -m "feat: new function XYZ"
# Push goes via HTTPS thanks to pushInsteadOf
git push origin feature/new-function

D11. Publish a new version

Terminal window
git checkout main && git pull
git tag v0.2.0
git push origin v0.2.0

Consumers update with:

Terminal window
go get github.com/myorg/my-go-module@v0.2.0

D12. Quick summary (all in one block)

Terminal window
sudo apt install -y git golang-go openssh-client
# --- Read channel: deploy key ---
ssh-keygen -t ed25519 -C "deploy-my-go-module" -f ~/.ssh/deploy_my-go-module
# → Admin adds ~/.ssh/deploy_my-go-module.pub as read-only deploy key
# to myorg/my-go-module → Settings → Deploy keys
cat >> ~/.ssh/config << 'SSHCFG'
Host github-my-go-module
HostName github.com
User git
IdentityFile ~/.ssh/deploy_my-go-module
IdentitiesOnly yes
SSHCFG
chmod 600 ~/.ssh/config
cat >> ~/.bashrc << 'AGENT'
if [ -z "$SSH_AUTH_SOCK" ]; then
eval "$(ssh-agent -s)" > /dev/null
ssh-add ~/.ssh/deploy_my-go-module 2>/dev/null
fi
AGENT
# --- Write channel: HTTPS + PAT ---
git config --global credential.helper store
sed -i '\|://.*@github\.com|d' ~/.git-credentials 2>/dev/null
printf 'protocol=https\nhost=github.com\nusername=MY_USERNAME\npassword=MY_TOKEN\n' \
| git credential approve
chmod 600 ~/.git-credentials
# --- Routing ---
git config --global url."git@github-my-go-module:myorg/my-go-module".insteadOf \
"https://github.com/myorg/my-go-module"
git config --global url."https://github.com/myorg/my-go-module".pushInsteadOf \
"git@github-my-go-module:myorg/my-go-module"
# --- Go variables ---
cat >> ~/.bashrc << 'EOF'
# If you have multiple private modules, separate them with commas:
# export GOPRIVATE="github.com/myorg/my-go-module,github.com/myorg/another-module"
export GOPRIVATE="github.com/myorg/my-go-module"
export GONOSUMDB="github.com/myorg/my-go-module"
export GONOSUMCHECK="github.com/myorg/my-go-module"
EOF
source ~/.bashrc

D13. Troubleshooting

SymptomProbable causeSolution
410 Gone on go getGo tries to use proxy.golang.orgVerify that GOPRIVATE includes github.com/myorg/my-go-module
Checksum SECURITY ERRORGONOSUMCHECK is not definedAdd export GONOSUMCHECK="github.com/myorg/my-go-module"
go get downloads old versionGo local cacheRun go clean -modcache and retry
ssh -T github-my-go-module says Permission deniedKey not loaded, alias misconfigured, or deploy key not registeredVerify with ssh-add -l; check ~/.ssh/config; confirm deploy key on GitHub
ssh -T responds with your username instead of the repossh-agent offers your personal keyAdd IdentitiesOnly yes to the Host block
go get fails with 401/403Missing insteadOf rule; Go tries HTTPS without deploy keyRun step D7
Push rejected (Write access denied)Missing pushInsteadOf rule; push goes via read-only deploy keyRun step D7
Push requests username/passwordCredential helper not configured or token not storedRun step D6
Push fails with 403PAT without permissions on the repoVerify scope repo (classic) or Contents: Read and write (fine-grained)
Key already in useGitHub does not allow the same key in more than one repoGenerate a separate pair per repository
ssh: connect to host github.com port 22: Connection refusedFirewall blocks port 22Add Hostname ssh.github.com and Port 443 to the Host block
Host key verification failedFirst connection to GitHubRun ssh-keyscan github.com >> ~/.ssh/known_hosts

Flow E — Deploy key for Go only + HTTPS for development

This flow achieves total separation between Go and git. Go uses SSH with the deploy key to download private modules. The developer uses HTTPS with their PAT for all git work (clone, pull, push). Neither interferes with the other.

The problem with flows C and D is that the insteadOf rule lives in the global git config (~/.gitconfig). That means all git operations see it, including those the developer runs directly. In Flow D this is mitigated with pushInsteadOf, but reads (clone, pull) still go via SSH, which forces using the deploy key alias to clone.

The solution in this flow is not to put any insteadOf rule in the global config. Instead, a shell wrapper is used that injects the rule only when Go executes git, using the environment variables GIT_CONFIG_COUNT, GIT_CONFIG_KEY_N, and GIT_CONFIG_VALUE_N. These variables are available since Git 2.31 (Debian 13 ships 2.47+). Git treats them as highest-priority configuration, but they only exist during the execution of the wrapped command.

The result:

Who runsinsteadOf activeProtocolAuthentication
go get, go build, go mod tidyYes (via wrapper)SSHDeploy key (read-only)
git clone, git pullNoHTTPSPAT (read/write)
git pushNoHTTPSPAT (read/write)

E1. Packages

Terminal window
sudo apt update
sudo apt install -y git golang-go openssh-client

Verify that you have Git ≥ 2.31, which is the version that introduced support for GIT_CONFIG_COUNT. Debian 13 ships 2.47+, so this should be met:

Terminal window
git --version # ≥ 2.31 (Debian 13 ships 2.47+)
go version # ≥ 1.21
ssh -V # OpenSSH ≥ 9.x

E2. Generate the deploy key

Same as in Flows C and D: a dedicated key with a name that identifies the repo.

Terminal window
ssh-keygen -t ed25519 -C "deploy-my-go-module" \
-f ~/.ssh/deploy_my-go-module

E3. Register the deploy key as read-only

The only thing Go needs to do with the module is download it. It does not need to write. That is why the deploy key is registered as read-only (minimum privilege):

Copy the public key:

Terminal window
cat ~/.ssh/deploy_my-go-module.pub

On GitHub:

  1. Go to myorg/my-go-module → Settings → Deploy keys → Add deploy key.
  2. Configure:
    • Title: e.g. go-readonly-rodolfo.
    • Key: paste the content.
    • Allow write access: do NOT check it (read-only).
  3. Click Add key.

E4. Alias in ~/.ssh/config

The alias works exactly as in Flows C and D. See the detailed explanation of each directive in step C4.

Terminal window
cat >> ~/.ssh/config << 'SSHCFG'
# --- Deploy key: myorg/my-go-module (read-only, for Go) ---
Host github-my-go-module
HostName github.com
User git
IdentityFile ~/.ssh/deploy_my-go-module
IdentitiesOnly yes
SSHCFG
chmod 600 ~/.ssh/config

E5. Configure ssh-agent

Terminal window
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/deploy_my-go-module

For auto-start:

Terminal window
cat >> ~/.bashrc << 'AGENT'
# --- Automatic SSH agent (deploy key my-go-module) ---
if [ -z "$SSH_AUTH_SOCK" ]; then
eval "$(ssh-agent -s)" > /dev/null
ssh-add ~/.ssh/deploy_my-go-module 2>/dev/null
fi
AGENT
source ~/.bashrc

E6. Personal Access Token + Credential Helper for HTTPS

The developer uses HTTPS for all git work. The PAT and the credential helper are configured exactly as in Flow A.

If you already have a Classic PAT with scope repo working, you can reuse it. If you need to create one, follow the detailed instructions in step A2.

Configure the credential helper

Terminal window
git config --global credential.helper store
# Clear previous github.com entry
sed -i '\|://.*@github\.com|d' ~/.git-credentials 2>/dev/null
# Store the credential
printf 'protocol=https\nhost=github.com\nusername=YOUR_USERNAME\npassword=YOUR_TOKEN\n' \
| git credential approve
chmod 600 ~/.git-credentials

Remember that the credential helper stores one credential per host, so the token must cover all your repos on github.com.


E7. Go wrapper with GIT_CONFIG_*

Here is the piece that makes this flow unique. In Flows C and D, the insteadOf rule is placed in ~/.gitconfig (global config), where it affects all git commands, whether run by Go or by the developer. Here, in contrast, the rule is injected only when Go executes git, using environment variables.

Git 2.31+ recognizes three environment variables that allow injecting ad-hoc configuration entries:

Git treats these entries with the highest priority (above ~/.gitconfig and .git/config). But since they are environment variables, they only exist during the execution of the process that defines them.

The wrapper is a shell function that wraps the go binary. Every time you type go in the terminal, bash runs the function instead of the binary directly. The function sets the GIT_CONFIG_* variables and then calls the real binary with command go (which skips the function and runs the binary from $PATH):

Terminal window
cat >> ~/.bashrc << 'GOWRAP'
# --- Go: inject insteadOf only for Go ---
go() {
GIT_CONFIG_COUNT=1 \
GIT_CONFIG_KEY_0="url.git@github-my-go-module:myorg/my-go-module.insteadOf" \
GIT_CONFIG_VALUE_0="https://github.com/myorg/my-go-module" \
command go "$@"
}
GOWRAP
source ~/.bashrc

What happens step by step

  1. The developer runs go get github.com/myorg/my-go-module@latest.
  2. Bash intercepts go and runs the wrapper function.
  3. The function sets GIT_CONFIG_COUNT=1, GIT_CONFIG_KEY_0, and GIT_CONFIG_VALUE_0 as environment variables of the process.
  4. The function calls the real Go binary with command go "$@".
  5. Go needs to download the module. It executes git internally.
  6. Git inherits the environment variables from its parent process (Go).
  7. Git sees GIT_CONFIG_COUNT=1 and reads the injected configuration entry.
  8. The entry says: “rewrite https://github.com/myorg/my-go-module to git@github-my-go-module:myorg/my-go-module”.
  9. Git connects using the alias github-my-go-module.
  10. SSH looks up the alias in ~/.ssh/config, finds the deploy key, and uses it.
  11. GitHub authorizes the read. The module is downloaded.
  12. Go and git finish. The environment variables disappear.

When the developer runs git clone https://github.com/myorg/my-go-module.git directly, it does not go through the wrapper. Git sees no GIT_CONFIG_* variables, applies no insteadOf, and uses HTTPS with the PAT from the credential helper.

Verify the wrapper is active

Terminal window
type go
# go is a function
# go ()
# {
# GIT_CONFIG_COUNT=1 ...
# }

If it says go is /usr/bin/go or similar, the wrapper was not loaded. Run source ~/.bashrc.


E8. Go variables for private modules

The same reasons as always: Go must not consult the public proxy or the checksum database for private modules.

Terminal window
cat >> ~/.bashrc << 'EOF'
# --- Go: private modules from myorg ---
# If you have multiple private modules, separate them with commas:
# export GOPRIVATE="github.com/myorg/my-go-module,github.com/myorg/another-module"
export GOPRIVATE="github.com/myorg/my-go-module"
export GONOSUMDB="github.com/myorg/my-go-module"
export GONOSUMCHECK="github.com/myorg/my-go-module"
EOF
source ~/.bashrc

Verify:

Terminal window
go env GOPRIVATE GONOSUMDB GONOSUMCHECK
# All three should show github.com/myorg/my-go-module

E9. Verify

Verification has two independent parts: confirm that Go uses the deploy key, and confirm that git uses HTTPS with the PAT.

Deploy key (Go channel)

Terminal window
# SSH connection with the alias (should say the repo, not a username)
ssh -T github-my-go-module
# Hi myorg/my-go-module! You've been authenticated, but GitHub does not
# provide shell access.
# go get uses the deploy key via the wrapper
mkdir /tmp/test-go && cd /tmp/test-go
go mod init github.com/myorg/test-app
go get github.com/myorg/my-go-module@latest
rm -rf /tmp/test-go

HTTPS (development channel)

Terminal window
# git clone goes via HTTPS (no insteadOf, uses the PAT)
git clone https://github.com/myorg/my-go-module.git /tmp/test-dev
cd /tmp/test-dev
# Confirm that the remote is pure HTTPS (not rewritten to SSH)
git remote -v
# origin https://github.com/myorg/my-go-module.git (fetch)
# origin https://github.com/myorg/my-go-module.git (push)
# push works via HTTPS
git checkout -b test-push-verify
git commit --allow-empty -m "test: verify HTTPS push"
git push origin test-push-verify
# If it works, the configuration is correct.
git push origin --delete test-push-verify
rm -rf /tmp/test-dev

The check with git remote -v is important: if the URLs show git@github-my-go-module:... instead of https://..., there is an active global insteadOf rule that should not be there. Remove it with git config --global --unset.


E10. Daily workflow

The developer uses HTTPS as usual. No need to know that Go uses a different channel. Everything is transparent:

Terminal window
# Development: everything via HTTPS
git clone https://github.com/myorg/my-go-module.git
cd my-go-module
git checkout -b feature/new-function
# ... edit files ...
git add .
git commit -m "feat: new function XYZ"
git push origin feature/new-function
# Module consumption: Go uses the deploy key automatically
# (in any other Go project from myorg)
go get github.com/myorg/my-go-module@latest

E11. Publish a new version

Terminal window
git checkout main && git pull
git tag v0.2.0
git push origin v0.2.0

Consumers update with:

Terminal window
go get github.com/myorg/my-go-module@v0.2.0

E12. Quick summary (all in one block)

Terminal window
sudo apt install -y git golang-go openssh-client
# --- Read channel (Go): deploy key ---
ssh-keygen -t ed25519 -C "deploy-my-go-module" -f ~/.ssh/deploy_my-go-module
# → Admin adds ~/.ssh/deploy_my-go-module.pub as a read-only deploy key
# in myorg/my-go-module → Settings → Deploy keys
cat >> ~/.ssh/config << 'SSHCFG'
Host github-my-go-module
HostName github.com
User git
IdentityFile ~/.ssh/deploy_my-go-module
IdentitiesOnly yes
SSHCFG
chmod 600 ~/.ssh/config
cat >> ~/.bashrc << 'AGENT'
if [ -z "$SSH_AUTH_SOCK" ]; then
eval "$(ssh-agent -s)" > /dev/null
ssh-add ~/.ssh/deploy_my-go-module 2>/dev/null
fi
AGENT
# --- Development channel (git): HTTPS + PAT ---
git config --global credential.helper store
sed -i '\|://.*@github\.com|d' ~/.git-credentials 2>/dev/null
printf 'protocol=https\nhost=github.com\nusername=MY_USERNAME\npassword=MY_TOKEN\n' \
| git credential approve
chmod 600 ~/.git-credentials
# --- Go wrapper (injects insteadOf only for Go) ---
cat >> ~/.bashrc << 'GOWRAP'
go() {
GIT_CONFIG_COUNT=1 \
GIT_CONFIG_KEY_0="url.git@github-my-go-module:myorg/my-go-module.insteadOf" \
GIT_CONFIG_VALUE_0="https://github.com/myorg/my-go-module" \
command go "$@"
}
GOWRAP
# --- Go variables ---
cat >> ~/.bashrc << 'EOF'
# If you have multiple private modules, separate them with commas:
# export GOPRIVATE="github.com/myorg/my-go-module,github.com/myorg/another-module"
export GOPRIVATE="github.com/myorg/my-go-module"
export GONOSUMDB="github.com/myorg/my-go-module"
export GONOSUMCHECK="github.com/myorg/my-go-module"
EOF
source ~/.bashrc

E13. Troubleshooting

SymptomProbable causeSolution
410 Gone on go getGo tries to use proxy.golang.orgVerify that GOPRIVATE includes github.com/myorg/my-go-module
Checksum SECURITY ERRORGONOSUMCHECK is not definedAdd export GONOSUMCHECK="github.com/myorg/my-go-module"
go get downloads old versionGo local cacheRun go clean -modcache and retry
go get fails with 401/403Wrapper not active; Go tries HTTPS without deploy keyVerify with type go (should say “go is a function”); run source ~/.bashrc
ssh -T github-my-go-module says Permission deniedKey not loaded, alias misconfigured, or deploy key not registeredVerify with ssh-add -l; check ~/.ssh/config; confirm deploy key on GitHub
ssh -T responds with your username instead of the repossh-agent offers your personal keyAdd IdentitiesOnly yes to the Host block in ~/.ssh/config
git clone/pull asks for username/passwordCredential helper not configuredRun step E6
git push fails with 403PAT without permissions on the repoVerify scope repo (classic) or Contents: Read and write (fine-grained)
go get works but git also goes via SSHThere is an active global insteadOf ruleRun git config --global --get-regexp url and remove extra rules with git config --global --unset url.XXX.insteadOf
Key already in useGitHub does not allow the same key in more than one repoGenerate a separate pair per repository
ssh: connect to host github.com port 22: Connection refusedFirewall blocks port 22Add Hostname ssh.github.com and Port 443 to the Host block
Host key verification failedFirst connection to GitHubRun ssh-keyscan github.com >> ~/.ssh/known_hosts
The wrapper does not activate in scripts or CINon-interactive scripts do not load ~/.bashrcUse source ~/.bashrc at the beginning of the script, or export the GIT_CONFIG_* variables directly in the CI environment

Share this post on:

Previous Post
How to restrict IP address access to a Cloudflare proxy host.
Next Post
Je l’aime à mourir: María Félix or Francis Cabrel?