The following tutorial explains how to use Go modules located in private repositories (e.g. GitHub).
Table of contents
Table of contents
- Flow A — HTTPS with Personal Access Token
- Flow B — SSH with personal key
- Flow C — SSH with deploy key
- C1. Packages
- C2. Generate a dedicated key pair
- C3. Register the public key in the repository
- C4. Alias in
~/.ssh/config - C5. Configure
ssh-agent - C6. Redirect Go to the SSH alias
- C7. Go variables for private modules
- C8. Verify
- C9. Daily workflow
- C10. Publish a new version
- C11. Quick summary (all in one block)
- C12. Troubleshooting
- Flow D — Hybrid: deploy key (read) + HTTPS (write)
- D1. Packages
- D2. Generate the deploy key
- D3. Register the deploy key as read-only
- D4. Alias in
~/.ssh/config - D5. Configure
ssh-agent - D6. Personal Access Token for writing
- D7.
insteadOf+pushInsteadOfrules - D8. Go variables for private modules
- D9. Verify
- D10. Daily workflow
- D11. Publish a new version
- D12. Quick summary (all in one block)
- D13. Troubleshooting
- Flow E — Deploy key for Go only + HTTPS for development
- E1. Packages
- E2. Generate the deploy key
- E3. Register the deploy key as read-only
- E4. Alias in
~/.ssh/config - E5. Configure
ssh-agent - E6. Personal Access Token + Credential Helper for HTTPS
- E7. Go wrapper with
GIT_CONFIG_* - E8. Go variables for private modules
- E9. Verify
- E10. Daily workflow
- E11. Publish a new version
- E12. Quick summary (all in one block)
- E13. Troubleshooting
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:
- Dependency versioning using Semantic Versioning (SemVer).
- Simplified dependency management commands (e.g.,
go get,go mod tidy). - Automatic generation of a manifest file (go.mod) with detailed information about dependencies.
- Automatic downloading and caching of required dependencies.
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/v3Normal 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
| Element | Value |
|---|---|
| Repository | https://github.com/myorg/my-go-module (private) |
| Consumers | Internal Go programs of myorg |
| Development OS | Debian 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 + Token | SSH (personal key) | SSH (deploy key) | Hybrid (deploy key + HTTPS) | |
|---|---|---|---|---|
| Authentication | Personal Access Token (PAT) | User key pair | Per-repository key pair | Deploy key for reading, PAT for writing |
| Scope | All user repos | All user repos | Single repository | Read: one repo; write: all PAT repos |
| Centralized revocation | Admin revokes tokens from GitHub console | Admin removes user from team | Admin deletes deploy key from repo | Combines both mechanisms |
| Expiration | Mandatory for fine-grained, configurable for classic | Keys don’t expire by default | Keys don’t expire by default | Depends on token and key type |
| Corporate firewalls | Always works (port 443) | May fail if port 22 is blocked | Same as personal SSH | Read: port 22; write: port 443 |
| Credentials per host | One per github.com; token must cover all repos | No conflict | Requires alias in ~/.ssh/config per repo | Requires alias + credential helper |
| Secret on disk | Token in ~/.git-credentials (plain text) | Private key in ~/.ssh/ (with passphrase) | Same as personal SSH | Both: key + token |
| CI/CD | Simpler: a string in a secret | Requires mounting key file | Ideal: minimal access to one repo | Uncommon in CI/CD |
| Who configures | Each developer creates their token | Each developer creates their key | Admin adds the public key; dev generates the pair | Admin 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
sudo apt updatesudo apt install -y git golang-goVerify:
git --version # ≥ 2.xgo version # ≥ 1.21A2. 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)
- Go to Settings → Developer settings → Personal access tokens → Fine-grained tokens → Generate new token.
- 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).
- Contents:
- Token name: something descriptive, e.g.
- Click Generate token and copy the token (you won’t see it again).
Classic token
- Go to Settings → Developer settings → Personal access tokens → Tokens (classic) → Generate new token (classic).
- Configure:
- Note: something descriptive, e.g.
dev-myorg. - Expiration: according to your organization’s policy.
- Scopes: check
repo.
- Note: something descriptive, e.g.
- 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)
git config --global credential.helper storeIf ~/.git-credentials already existed with an entry for github.com, it must be
replaced (Git uses the first match):
sed -i '\|://.*@github\.com|d' ~/.git-credentials 2>/dev/nullStore the credential:
# Replace YOUR_USERNAME and YOUR_TOKENprintf 'protocol=https\nhost=github.com\nusername=YOUR_USERNAME\npassword=YOUR_TOKEN\n' \ | git credential approveVerify and protect:
cat ~/.git-credentials# You should see: https://YOUR_USERNAME:YOUR_TOKEN@github.comchmod 600 ~/.git-credentialsOption 2 — git-credential-cache (in memory, more secure)
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)
sudo apt install -y ghgh auth login # Choose HTTPS, paste your PATgh auth setup-git # Register gh as credential helperA4. Force HTTPS for Go
Go may try to use SSH when resolving modules. To always use HTTPS:
git config --global url."https://github.com/".insteadOf "ssh://git@github.com/"git config --global url."https://github.com/".insteadOf "git@github.com:"Verify:
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:
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| Variable | Effect |
|---|---|
GOPRIVATE | Tells Go that the listed modules are private. Implies GONOSUMDB and GONOPROXY. |
GONOSUMDB | Does not query sum.golang.org for these modules. |
GONOSUMCHECK | Does not verify checksums against the public database. |
Verify:
go env GOPRIVATE GONOSUMDB GONOSUMCHECK# All three should show github.com/myorg/my-go-moduleA6. Verify
# Gitgit clone https://github.com/myorg/my-go-module.git /tmp/test-clonerm -rf /tmp/test-clone
# Gomkdir /tmp/test-go && cd /tmp/test-gogo mod init github.com/myorg/test-appgo get github.com/myorg/my-go-module@latestrm -rf /tmp/test-goA7. Daily workflow
git clone https://github.com/myorg/my-go-module.gitcd 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-functionA8. Publish a new version
git checkout main && git pullgit tag v0.2.0git push origin v0.2.0Consumers update with:
go get github.com/myorg/my-go-module@v0.2.0A9. Quick summary (all in one block)
sudo apt install -y git golang-go
# Credential helper (option 1)git config --global credential.helper storesed -i '\|://.*@github\.com|d' ~/.git-credentials 2>/dev/nullprintf 'protocol=https\nhost=github.com\nusername=MY_USERNAME\npassword=MY_TOKEN\n' \ | git credential approvechmod 600 ~/.git-credentials
# Force HTTPS for Gogit config --global url."https://github.com/".insteadOf "ssh://git@github.com/"git config --global url."https://github.com/".insteadOf "git@github.com:"
# Go variablescat >> ~/.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"EOFsource ~/.bashrcA10. Troubleshooting
| Symptom | Probable cause | Solution |
|---|---|---|
410 Gone on go get | Go tries to use proxy.golang.org | Verify that GOPRIVATE includes github.com/myorg/my-go-module |
Checksum SECURITY ERROR | GONOSUMCHECK is not defined | Add export GONOSUMCHECK="github.com/myorg/my-go-module" |
go get downloads old version | Go local cache | Run go clean -modcache and retry |
fatal: could not read Username | Credential helper not configured | Run step A3 |
403 Forbidden | Token without sufficient permissions | Verify that the PAT has Contents: Read and write permission |
403 on repos that worked before | Fine-grained token restricted to specific repos | Recreate with All repositories or use Classic PAT with scope repo |
| Go tries to connect via SSH | Missing insteadOf rule | Run step A4 |
Flow B — SSH with personal key
B1. Packages
sudo apt updatesudo apt install -y git golang-go openssh-clientVerify:
git --version # ≥ 2.xgo version # ≥ 1.21ssh -V # OpenSSH ≥ 9.xB2. Generate the SSH key
If you already have a key pair in ~/.ssh/id_ed25519, you can reuse it.
If not:
ssh-keygen -t ed25519 -C "your-email@example.com"Accept the default path and set a passphrase.
Verify:
ls -l ~/.ssh/id_ed25519 ~/.ssh/id_ed25519.pubB3. Add the public key to GitHub
Copy the public key:
cat ~/.ssh/id_ed25519.pubOn GitHub:
- Go to Settings → SSH and GPG keys → New SSH key.
- Configure:
- Title: e.g.
debian-dev-myorg. - Key type:
Authentication Key. - Key: paste the content.
- Title: e.g.
- Click Add SSH key.
B4. Configure ssh-agent
eval "$(ssh-agent -s)"ssh-add ~/.ssh/id_ed25519For auto-start on each session:
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/nullfiAGENT
source ~/.bashrcB5. Force SSH for Go
Go may try HTTPS when resolving modules. To always use SSH:
git config --global url."git@github.com:".insteadOf "https://github.com/"Verify:
git config --global --get-regexp url# url.git@github.com:.insteadof https://github.com/B6. Go variables for private modules
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 ~/.bashrcVerify:
go env GOPRIVATE GONOSUMDB GONOSUMCHECK# All three should show github.com/myorg/my-go-moduleB7. Verify
# SSH connectionssh -T git@github.com# Expected response:# Hi YOUR_USERNAME! You've been authenticated, but GitHub does not provide shell access.
# Gitgit clone git@github.com:myorg/my-go-module.git /tmp/test-clonerm -rf /tmp/test-clone
# Gomkdir /tmp/test-go && cd /tmp/test-gogo mod init github.com/myorg/test-appgo get github.com/myorg/my-go-module@latestrm -rf /tmp/test-goB8. Daily workflow
git clone git@github.com:myorg/my-go-module.gitcd 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-functionB9. Publish a new version
git checkout main && git pullgit tag v0.2.0git push origin v0.2.0Consumers update with:
go get github.com/myorg/my-go-module@v0.2.0B10. Quick summary (all in one block)
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 agentcat >> ~/.bashrc << 'AGENT'if [ -z "$SSH_AUTH_SOCK" ]; then eval "$(ssh-agent -s)" > /dev/null ssh-add ~/.ssh/id_ed25519 2>/dev/nullfiAGENT
# Force SSH for Gogit config --global url."git@github.com:".insteadOf "https://github.com/"
# Go variablescat >> ~/.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"EOFsource ~/.bashrcB11. Troubleshooting
| Symptom | Probable cause | Solution |
|---|---|---|
410 Gone on go get | Go tries to use proxy.golang.org | Verify that GOPRIVATE includes github.com/myorg/my-go-module |
Checksum SECURITY ERROR | GONOSUMCHECK is not defined | Add export GONOSUMCHECK="github.com/myorg/my-go-module" |
go get downloads old version | Go local cache | Run go clean -modcache and retry |
Permission denied (publickey) | Key not loaded in agent or not registered on GitHub | Run ssh-add -l; check the key on GitHub |
ssh: connect to host github.com port 22: Connection refused | Firewall blocks port 22 | Add to ~/.ssh/config: Host github.com / Hostname ssh.github.com / Port 443 / User git |
| Go tries to connect via HTTPS | Missing insteadOf rule | Run step B5 |
Host key verification failed | First connection to GitHub | Run ssh-keyscan github.com >> ~/.ssh/known_hosts |
Flow C — SSH with deploy key
C1. Packages
sudo apt updatesudo apt install -y git golang-go openssh-clientVerify:
git --version # ≥ 2.xgo version # ≥ 1.21ssh -V # OpenSSH ≥ 9.xC2. Generate a dedicated key pair
Use a filename that identifies the repo:
ssh-keygen -t ed25519 -C "deploy-my-go-module" \ -f ~/.ssh/deploy_my-go-moduleSet 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:
cat ~/.ssh/deploy_my-go-module.pubOn GitHub:
- Go to the repository myorg/my-go-module → Settings → Deploy keys → Add deploy key.
- Configure:
- Title: e.g.
dev-rodolfoorci-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.
- Title: e.g.
- 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:
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 yesSSHCFG
chmod 600 ~/.ssh/config| Directive | Function |
|---|---|
Host github-my-go-module | Arbitrary alias you will use in Git URLs and in insteadOf. |
HostName github.com | The real server SSH will connect to. |
User git | GitHub always uses the git user for SSH. |
IdentityFile | The private key of the deploy key. |
IdentitiesOnly yes | Prevents ssh-agent from offering other keys. |
C5. Configure ssh-agent
eval "$(ssh-agent -s)"ssh-add ~/.ssh/deploy_my-go-moduleFor auto-start on each session:
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/nullfiAGENT
source ~/.bashrcC6. Redirect Go to the SSH alias
With deploy keys, the insteadOf rule must be per repository:
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:
git config --global --get-regexp url# url.git@github-my-go-module:myorg/my-go-module.insteadof https://github.com/myorg/my-go-moduleC7. Go variables for private modules
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 ~/.bashrcVerify:
go env GOPRIVATE GONOSUMDB GONOSUMCHECK# All three should show github.com/myorg/my-go-moduleC8. Verify
# SSH connection with the aliasssh -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.
# Gitgit clone git@github-my-go-module:myorg/my-go-module.git /tmp/test-clonerm -rf /tmp/test-clone
# Gomkdir /tmp/test-go && cd /tmp/test-gogo mod init github.com/myorg/test-appgo get github.com/myorg/my-go-module@latestrm -rf /tmp/test-goC9. Daily workflow
git clone git@github-my-go-module:myorg/my-go-module.gitcd 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-functionC10. Publish a new version
git checkout main && git pullgit tag v0.2.0git push origin v0.2.0Consumers update with:
go get github.com/myorg/my-go-module@v0.2.0C11. Quick summary (all in one block)
sudo apt install -y git golang-go openssh-client
# Generate deploy keyssh-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 aliascat >> ~/.ssh/config << 'SSHCFG'Host github-my-go-module HostName github.com User git IdentityFile ~/.ssh/deploy_my-go-module IdentitiesOnly yesSSHCFGchmod 600 ~/.ssh/config
# Automatic SSH agentcat >> ~/.bashrc << 'AGENT'if [ -z "$SSH_AUTH_SOCK" ]; then eval "$(ssh-agent -s)" > /dev/null ssh-add ~/.ssh/deploy_my-go-module 2>/dev/nullfiAGENT
# Redirect Go to the SSH aliasgit config --global url."git@github-my-go-module:myorg/my-go-module".insteadOf \ "https://github.com/myorg/my-go-module"
# Go variablescat >> ~/.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"EOFsource ~/.bashrcC12. Troubleshooting
| Symptom | Probable cause | Solution |
|---|---|---|
410 Gone on go get | Go tries to use proxy.golang.org | Verify that GOPRIVATE includes github.com/myorg/my-go-module |
Checksum SECURITY ERROR | GONOSUMCHECK is not defined | Add export GONOSUMCHECK="github.com/myorg/my-go-module" |
go get downloads old version | Go local cache | Run go clean -modcache and retry |
ssh -T github-my-go-module says Permission denied | Key not loaded, alias misconfigured, or deploy key not registered | Verify with ssh-add -l; check ~/.ssh/config; confirm deploy key on GitHub |
ssh -T responds with your username instead of the repo | ssh-agent offers your personal key | Add IdentitiesOnly yes to the Host block in ~/.ssh/config |
go get requests HTTPS authentication | Missing insteadOf rule | Run step C6 |
ERROR: Repository not found | Deploy key deleted or alias points to wrong repo | Confirm in Settings → Deploy keys and check ~/.ssh/config |
Key already in use when adding | GitHub does not allow the same key in more than one repo | Generate a separate pair per repository |
Push rejected (Write access denied) | Deploy key without write permission | Admin enables Allow write access, or use Flow D (hybrid) |
ssh: connect to host github.com port 22: Connection refused | Firewall blocks port 22 | Add to ~/.ssh/config inside the Host block: Hostname ssh.github.com / Port 443 |
Host key verification failed | First connection to GitHub | Run 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
sudo apt updatesudo apt install -y git golang-go openssh-clientVerify:
git --version # ≥ 2.xgo version # ≥ 1.21ssh -V # OpenSSH ≥ 9.xD2. Generate the deploy key
ssh-keygen -t ed25519 -C "deploy-my-go-module" \ -f ~/.ssh/deploy_my-go-moduleD3. Register the deploy key as read-only
Copy the public key:
cat ~/.ssh/deploy_my-go-module.pubOn GitHub:
- Go to myorg/my-go-module → Settings → Deploy keys → Add deploy key.
- Configure:
- Title: e.g.
dev-rodolfo-readonly. - Key: paste the content.
- Allow write access: DO NOT check it (read-only).
- Title: e.g.
- Click Add key.
D4. Alias in ~/.ssh/config
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 yesSSHCFG
chmod 600 ~/.ssh/configD5. Configure ssh-agent
eval "$(ssh-agent -s)"ssh-add ~/.ssh/deploy_my-go-moduleFor auto-start:
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/nullfiAGENT
source ~/.bashrcD6. 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
git config --global credential.helper store
# Remove previous github.com entrysed -i '\|://.*@github\.com|d' ~/.git-credentials 2>/dev/null
# Store the credentialprintf 'protocol=https\nhost=github.com\nusername=YOUR_USERNAME\npassword=YOUR_TOKEN\n' \ | git credential approve
chmod 600 ~/.git-credentialsD7. insteadOf + pushInsteadOf rules
Here is the key piece. Git applies insteadOf first, and then
pushInsteadOf overrides only for write operations:
# Reads (go get, git pull, git clone) → SSH deploy keygit 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:
| Operation | Rewrite chain | Authentication |
|---|---|---|
go get | HTTPS → SSH (deploy key) | Deploy key, read-only |
git pull | HTTPS → SSH (deploy key) | Deploy key, read-only |
git clone | HTTPS → SSH (deploy key) | Deploy key, read-only |
git push | HTTPS → SSH → HTTPS | PAT, 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:
git config --global --get-regexp url# url.git@github-my-go-module:myorg/my-go-module.insteadof https://github.com/myorg/my-go-moduleD8. Go variables for private modules
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 ~/.bashrcVerify:
go env GOPRIVATE GONOSUMDB GONOSUMCHECK# All three should show github.com/myorg/my-go-moduleD9. Verify
# 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-gogo mod init github.com/myorg/test-appgo get github.com/myorg/my-go-module@latestrm -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-pushcd /tmp/test-pushgit checkout -b test-push-verifygit 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-verifyrm -rf /tmp/test-pushD10. Daily workflow
# Clone (goes via deploy key thanks to insteadOf, but transparent)git clone https://github.com/myorg/my-go-module.gitcd 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 pushInsteadOfgit push origin feature/new-functionD11. Publish a new version
git checkout main && git pullgit tag v0.2.0git push origin v0.2.0Consumers update with:
go get github.com/myorg/my-go-module@v0.2.0D12. Quick summary (all in one block)
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 yesSSHCFGchmod 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/nullfiAGENT
# --- Write channel: HTTPS + PAT ---
git config --global credential.helper storesed -i '\|://.*@github\.com|d' ~/.git-credentials 2>/dev/nullprintf 'protocol=https\nhost=github.com\nusername=MY_USERNAME\npassword=MY_TOKEN\n' \ | git credential approvechmod 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"EOFsource ~/.bashrcD13. Troubleshooting
| Symptom | Probable cause | Solution |
|---|---|---|
410 Gone on go get | Go tries to use proxy.golang.org | Verify that GOPRIVATE includes github.com/myorg/my-go-module |
Checksum SECURITY ERROR | GONOSUMCHECK is not defined | Add export GONOSUMCHECK="github.com/myorg/my-go-module" |
go get downloads old version | Go local cache | Run go clean -modcache and retry |
ssh -T github-my-go-module says Permission denied | Key not loaded, alias misconfigured, or deploy key not registered | Verify with ssh-add -l; check ~/.ssh/config; confirm deploy key on GitHub |
ssh -T responds with your username instead of the repo | ssh-agent offers your personal key | Add IdentitiesOnly yes to the Host block |
go get fails with 401/403 | Missing insteadOf rule; Go tries HTTPS without deploy key | Run step D7 |
Push rejected (Write access denied) | Missing pushInsteadOf rule; push goes via read-only deploy key | Run step D7 |
| Push requests username/password | Credential helper not configured or token not stored | Run step D6 |
| Push fails with 403 | PAT without permissions on the repo | Verify scope repo (classic) or Contents: Read and write (fine-grained) |
Key already in use | GitHub does not allow the same key in more than one repo | Generate a separate pair per repository |
ssh: connect to host github.com port 22: Connection refused | Firewall blocks port 22 | Add Hostname ssh.github.com and Port 443 to the Host block |
Host key verification failed | First connection to GitHub | Run 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 runs | insteadOf active | Protocol | Authentication |
|---|---|---|---|
go get, go build, go mod tidy | Yes (via wrapper) | SSH | Deploy key (read-only) |
git clone, git pull | No | HTTPS | PAT (read/write) |
git push | No | HTTPS | PAT (read/write) |
E1. Packages
sudo apt updatesudo apt install -y git golang-go openssh-clientVerify 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:
git --version # ≥ 2.31 (Debian 13 ships 2.47+)go version # ≥ 1.21ssh -V # OpenSSH ≥ 9.xE2. Generate the deploy key
Same as in Flows C and D: a dedicated key with a name that identifies the repo.
ssh-keygen -t ed25519 -C "deploy-my-go-module" \ -f ~/.ssh/deploy_my-go-moduleE3. 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:
cat ~/.ssh/deploy_my-go-module.pubOn GitHub:
- Go to myorg/my-go-module → Settings → Deploy keys → Add deploy key.
- Configure:
- Title: e.g.
go-readonly-rodolfo. - Key: paste the content.
- Allow write access: do NOT check it (read-only).
- Title: e.g.
- 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.
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 yesSSHCFG
chmod 600 ~/.ssh/configE5. Configure ssh-agent
eval "$(ssh-agent -s)"ssh-add ~/.ssh/deploy_my-go-moduleFor auto-start:
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/nullfiAGENT
source ~/.bashrcE6. 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
git config --global credential.helper store
# Clear previous github.com entrysed -i '\|://.*@github\.com|d' ~/.git-credentials 2>/dev/null
# Store the credentialprintf 'protocol=https\nhost=github.com\nusername=YOUR_USERNAME\npassword=YOUR_TOKEN\n' \ | git credential approve
chmod 600 ~/.git-credentialsRemember 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_CONFIG_COUNT=N— how many entries there areGIT_CONFIG_KEY_0,GIT_CONFIG_KEY_1, … — the key of each entryGIT_CONFIG_VALUE_0,GIT_CONFIG_VALUE_1, … — the value of each entry
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):
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 ~/.bashrcWhat happens step by step
- The developer runs
go get github.com/myorg/my-go-module@latest. - Bash intercepts
goand runs the wrapper function. - The function sets
GIT_CONFIG_COUNT=1,GIT_CONFIG_KEY_0, andGIT_CONFIG_VALUE_0as environment variables of the process. - The function calls the real Go binary with
command go "$@". - Go needs to download the module. It executes git internally.
- Git inherits the environment variables from its parent process (Go).
- Git sees
GIT_CONFIG_COUNT=1and reads the injected configuration entry. - The entry says: “rewrite
https://github.com/myorg/my-go-moduletogit@github-my-go-module:myorg/my-go-module”. - Git connects using the alias
github-my-go-module. - SSH looks up the alias in
~/.ssh/config, finds the deploy key, and uses it. - GitHub authorizes the read. The module is downloaded.
- 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
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.
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 ~/.bashrcVerify:
go env GOPRIVATE GONOSUMDB GONOSUMCHECK# All three should show github.com/myorg/my-go-moduleE9. 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)
# 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 wrappermkdir /tmp/test-go && cd /tmp/test-gogo mod init github.com/myorg/test-appgo get github.com/myorg/my-go-module@latestrm -rf /tmp/test-goHTTPS (development channel)
# git clone goes via HTTPS (no insteadOf, uses the PAT)git clone https://github.com/myorg/my-go-module.git /tmp/test-devcd /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 HTTPSgit checkout -b test-push-verifygit 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-verifyrm -rf /tmp/test-devThe 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:
# Development: everything via HTTPSgit clone https://github.com/myorg/my-go-module.gitcd 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@latestE11. Publish a new version
git checkout main && git pullgit tag v0.2.0git push origin v0.2.0Consumers update with:
go get github.com/myorg/my-go-module@v0.2.0E12. Quick summary (all in one block)
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 yesSSHCFGchmod 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/nullfiAGENT
# --- Development channel (git): HTTPS + PAT ---
git config --global credential.helper storesed -i '\|://.*@github\.com|d' ~/.git-credentials 2>/dev/nullprintf 'protocol=https\nhost=github.com\nusername=MY_USERNAME\npassword=MY_TOKEN\n' \ | git credential approvechmod 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"EOFsource ~/.bashrcE13. Troubleshooting
| Symptom | Probable cause | Solution |
|---|---|---|
410 Gone on go get | Go tries to use proxy.golang.org | Verify that GOPRIVATE includes github.com/myorg/my-go-module |
Checksum SECURITY ERROR | GONOSUMCHECK is not defined | Add export GONOSUMCHECK="github.com/myorg/my-go-module" |
go get downloads old version | Go local cache | Run go clean -modcache and retry |
go get fails with 401/403 | Wrapper not active; Go tries HTTPS without deploy key | Verify with type go (should say “go is a function”); run source ~/.bashrc |
ssh -T github-my-go-module says Permission denied | Key not loaded, alias misconfigured, or deploy key not registered | Verify with ssh-add -l; check ~/.ssh/config; confirm deploy key on GitHub |
ssh -T responds with your username instead of the repo | ssh-agent offers your personal key | Add IdentitiesOnly yes to the Host block in ~/.ssh/config |
git clone/pull asks for username/password | Credential helper not configured | Run step E6 |
git push fails with 403 | PAT without permissions on the repo | Verify scope repo (classic) or Contents: Read and write (fine-grained) |
go get works but git also goes via SSH | There is an active global insteadOf rule | Run git config --global --get-regexp url and remove extra rules with git config --global --unset url.XXX.insteadOf |
Key already in use | GitHub does not allow the same key in more than one repo | Generate a separate pair per repository |
ssh: connect to host github.com port 22: Connection refused | Firewall blocks port 22 | Add Hostname ssh.github.com and Port 443 to the Host block |
Host key verification failed | First connection to GitHub | Run ssh-keyscan github.com >> ~/.ssh/known_hosts |
| The wrapper does not activate in scripts or CI | Non-interactive scripts do not load ~/.bashrc | Use source ~/.bashrc at the beginning of the script, or export the GIT_CONFIG_* variables directly in the CI environment |
