Athens Go Module Proxy 0.20.0 on AWS User Guide
Overview
This guide covers launching and using the Athens Go Module Proxy 0.20.0 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg artifact and container registries listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.
Athens is a Go module proxy: it speaks the GOPROXY protocol, so go build, go mod download and go get fetch modules from it instead of from the internet. The first request for a module version downloads it (from proxy.golang.org, or straight from its git repository when it is not there) and stores it; every later request is served from your own instance, so builds keep working when an upstream repository is deleted or unreachable, and your CI fleet stops pulling the same modules from the internet over and over.
Upstream Athens listens on every interface with no authentication and no TLS, and it fetches any module path a client asks for, including paths that name hosts inside your network. The cloudimg image makes it a proxy you can safely put in a VPC:
- TLS and a password from the first boot. nginx serves the proxy on port 443 (
https://<host>/) with a certificate generated on your instance's first boot for its DNS name and IP addresses. Every proxy request needs HTTP basic authentication with the usergoproxyand a password generated on that first boot (written to/root/athens-credentials.txt, root only); only the/healthzhealth check is open. Athens itself listens on127.0.0.1only, and nothing listens on port 80. - An egress firewall for the proxy. Module paths are chosen by clients, so a path such as
10.0.0.5/xwould make an unprotected proxy connect to an internal address. On this image every connection that Athens, and thegoandgitprocesses it runs, opens to link local, metadata, carrier grade NAT or private (RFC 1918) addresses is refused by the kernel, with a second, independent block in the service's own cgroup. The rules load before Athens on every boot, and Athens refuses to start without them. - Checksums verified. Downloads are verified against the Go checksum database (
GOSUMDB=sum.golang.org); no module is exempt by default, and Athens also proxies the checksum database to your clients. - Your modules on their own disk. The module store lives on a separate 30 GiB gp3 data volume mounted at
/var/lib/athens, which you can resize on its own. - Locked down. Athens runs as an unprivileged user in a sandboxed service; Athens, nginx and the first boot service cannot reach the instance metadata service, and the AMI requires IMDSv2. Requests for host names the instance does not serve are dropped.
- Built from source. Athens 0.20.0 is compiled from the upstream release (pinned by SHA-256) with the pinned Go 1.27.3 toolchain, with its dependencies raised to versions that fix the published advisories in the upstream set, on fully patched Ubuntu 24.04 LTS with unattended security upgrades switched on.
What is on the image: Athens 0.20.0 (/opt/athens, licence and build provenance in /usr/share/doc/athens), Go 1.27.3 at /usr/local/go (Athens runs go mod download for every new module), git, nginx, Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent.
Default user: ubuntu · Architecture: x86_64 · Recommended instance type: m5.large
Prerequisites
- An AWS account subscribed to the listing, an EC2 key pair and a VPC subnet your build machines can reach. The instance needs outbound HTTPS to the internet (proxy.golang.org, sum.golang.org and the git hosts of the modules you use).
- A security group that allows TCP 22 (SSH) from your own networks only, and TCP 443 from your build machines and developers. Nothing else needs to be open.
Step 1: Launch the instance
In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick Athens Go Module Proxy 0.20.0 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large, your key pair, and a security group as above. The AMI attaches the 30 GiB data volume for /var/lib/athens automatically; increase its size in the launch wizard's storage section if you build against many modules.
With the AWS CLI (replace the values in angle brackets):
aws ec2 run-instances --region <region> --image-id <ami-id-from-the-listing> \
--instance-type m5.large --key-name <your-key-pair> \
--security-group-ids <your-security-group> --subnet-id <your-subnet> \
--metadata-options HttpTokens=required,HttpEndpoint=enabled \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=athens}]'
Step 2: Connect to your instance
| Version | SSH user |
|---|---|
| Athens Go Module Proxy 0.20.0 on Ubuntu 24.04 | ubuntu |
ssh -i <your-key.pem> ubuntu@<public-ip>
Step 3: Check the services
First boot generates the certificate and the proxy password; the egress firewall, Athens and nginx then start:
systemctl is-active athens nginx cloudimg-athens-egress
sudo test -f /var/lib/cloudimg/athens-firstboot.done && echo FIRSTBOOT_DONE
active
active
active
FIRSTBOOT_DONE
Only SSH and nginx listen on the network. Athens is on loopback behind nginx, and the DNS resolver listens on loopback only:
sudo ss -lntp | awk 'NR>1 {print $4, $6}' | sed -E 's/pid=[0-9]+,fd=[0-9]+/.../g' | sort
0.0.0.0:22 users:(("sshd",...),("systemd",...))
0.0.0.0:443 users:(("nginx",...),("nginx",...),("nginx",...))
127.0.0.1:3000 users:(("athens",...))
127.0.0.53%lo:53 users:(("systemd-resolve",...))
127.0.0.54:53 users:(("systemd-resolve",...))
[::]:22 users:(("sshd",...),("systemd",...))
[::]:443 users:(("nginx",...),("nginx",...),("nginx",...))
The module store is on its own volume:
df -h /var/lib/athens | awk 'NR==2 {print $2, $6}'
30G /var/lib/athens
Step 4: Get your proxy URL and password
The credentials file is readable by root only. This prints it with the password hidden; sudo cat /root/athens-credentials.txt shows the password itself:
sudo sed -E 's/^(ATHENS_PROXY_PASSWORD=).*/\1<hidden>/' /root/athens-credentials.txt
# Athens Go Module Proxy 0.20.0 - generated on first boot by athens-firstboot.service
# This password is unique to this instance; none was baked into the image. Keep this file secret.
# Every proxy request needs it (HTTP basic auth); only /healthz is open.
ATHENS_PROXY_URL=https://ec2-44-204-161-253.compute-1.amazonaws.com/
ATHENS_HOST=ec2-44-204-161-253.compute-1.amazonaws.com
ATHENS_PROXY_USER=goproxy
ATHENS_PROXY_PASSWORD=<hidden>
ATHENS_CA_CERT=/etc/nginx/tls/athens.crt
ATHENS_CERT_SHA256=F4:E5:5A:AE:6F:86:95:9F:BD:AD:B1:59:A1:72:57:47:DB:0B:D6:EF:EE:C3:2E:55:23:FB:E7:7B:C7:E7:94:A9
#
# Go clients: put the password in ~/.netrc (machine <ATHENS_HOST> login goproxy password <ATHENS_PROXY_PASSWORD>),
# then: export GOPROXY=https://<ATHENS_HOST>/ GOSUMDB=sum.golang.org SSL_CERT_FILE=<a copy of athens.crt>
# The certificate is self-signed: trust /etc/nginx/tls/athens.crt on your clients, or use your own (see the guide).
# New proxy password: sudo athens-rotate-password
# Change the name it serves as: sudo athens-set-hostname <your-dns-name>
# User guide: https://www.cloudimg.co.uk/guides/athens-go-module-proxy-0-20-0-on-ubuntu-24-04-aws/
Your instance shows its own DNS name and certificate fingerprint. To serve under your own name, see Step 8.
Step 5: Fetch a module through the proxy
This asks the proxy for the versions of github.com/google/uuid, first without the password and then with it. The password is handed to curl on standard input, so it never appears in the process list:
sudo bash -c '
P=$(grep "^ATHENS_PROXY_PASSWORD=" /root/athens-credentials.txt | cut -d= -f2-)
C=/etc/nginx/tls/athens.crt
curl -s --cacert $C -o /dev/null -w "no password -> HTTP %{http_code}\n" https://localhost/github.com/google/uuid/@v/list
printf "user = \"goproxy:%s\"\n" "$P" | curl -K - -s --cacert $C https://localhost/github.com/google/uuid/@v/list | tail -3; echo
printf "user = \"goproxy:%s\"\n" "$P" | curl -K - -s --cacert $C https://localhost/github.com/google/uuid/@v/v1.6.0.info; echo'
no password -> HTTP 401
v1.4.0
v1.5.0
v1.6.0
{"Version":"v1.6.0","Time":"2024-01-23T18:54:04Z","Origin":{"VCS":"git","URL":"https://github.com/google/uuid","Hash":"0f11ee6918f41a04c201eceeadf612a377bc7fbc","Ref":"refs/tags/v1.6.0"}}
When a version is first downloaded, Athens fetches it, checks it against sum.golang.org and stores the .info, .mod and .zip files under /var/lib/athens/storage; every later request for it is served from there.
Step 6: Use the proxy from your build machines
Copy the instance's certificate (/etc/nginx/tls/athens.crt) to your build machine and compare its fingerprint with ATHENS_CERT_SHA256 from Step 4. It is a leaf server certificate for this instance's names only (CA:FALSE, no certificate signing), so trusting it lets that machine trust this proxy and nothing else. On an Ubuntu or Debian build machine:
scp -i <your-key.pem> ubuntu@<public-ip>:/etc/nginx/tls/athens.crt ./athens.crt
openssl x509 -noout -fingerprint -sha256 -in ./athens.crt
sudo install -m 0644 ./athens.crt /usr/local/share/ca-certificates/athens.crt
sudo update-ca-certificates
The go command reads basic auth credentials for a proxy from ~/.netrc (or the file named by NETRC). On the build machine, with your instance's DNS name (or your own, Step 8) in place of <your-domain> and the password from Step 4:
( umask 077; printf 'machine <your-domain> login goproxy password %s\n' "$ATHENS_PROXY_PASSWORD" >> ~/.netrc )
export GOPROXY=https://<your-domain>/ GOSUMDB=sum.golang.org
go mod download
In CI, keep the password in the pipeline's secret store and write the .netrc at the start of the job, rather than keeping it in a file in the repository. go env -w GOPROXY=https://<your-domain>/ makes the setting permanent for a developer.
A first go get through the proxy, from a new module in an empty directory on a build machine:
export GOPROXY=https://<your-domain>/ GOSUMDB=sum.golang.org
go mod init example.com/demo
go get golang.org/x/text@v0.43.0
go env GOPROXY GOSUMDB
go: downloading golang.org/x/text v0.43.0
go: added golang.org/x/text v0.43.0
https://<your-domain>/
sum.golang.org
Private modules. The image holds no git credentials, and its egress firewall does not let Athens reach git servers on private addresses, so it serves public modules. For your own private modules, set GOPRIVATE=<your-git-host>/* on your build machines: the go command then fetches those directly with your own git credentials, skipping the proxy and the public checksum database for them only. Leave GOSUMDB on for everything else.
Step 7: Check the security posture
Everything but the health check needs the password, a request for a host name the instance does not serve is dropped without an answer, and nothing answers on port 80:
C=/etc/nginx/tls/athens.crt
curl -s --cacert $C -o /dev/null -w 'catalog, no password -> HTTP %{http_code}\n' https://localhost/catalog
curl -s --cacert $C -K <(printf 'user = "goproxy:not-the-password"\n') -o /dev/null -w 'wrong password -> HTTP %{http_code}\n' https://localhost/github.com/google/uuid/@v/list
curl -s --cacert $C -o /dev/null -w 'health check -> HTTP %{http_code}\n' https://localhost/healthz
curl -s --cacert $C -H 'Host: evil.example' -o /dev/null -w 'unknown host -> %{http_code}\n' https://localhost/healthz || true
curl -s -o /dev/null -w 'plain http :80 -> %{http_code}\n' --connect-timeout 3 http://127.0.0.1/ || true
catalog, no password -> HTTP 401
wrong password -> HTTP 401
health check -> HTTP 200
unknown host -> 000
plain http :80 -> 000
000 means no answer: nginx closes the connection for a host name it does not serve, and nothing listens on port 80.
The certificate is a leaf for the instance's names:
openssl x509 -noout -ext subjectAltName,basicConstraints,keyUsage,extendedKeyUsage -in /etc/nginx/tls/athens.crt
X509v3 Subject Alternative Name:
DNS:ec2-44-204-161-253.compute-1.amazonaws.com, IP Address:44.204.161.253, IP Address:172.31.93.135, IP Address:127.0.0.1, DNS:localhost
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Key Usage: critical
Digital Signature, Key Encipherment
X509v3 Extended Key Usage:
TLS Web Server Authentication
The egress firewall applies to every process running as the athens user. Loopback is allowed; link local (including the metadata service), carrier grade NAT, private and 0.0.0.0/8 addresses are refused, with the same ranges for IPv6 (unique local, link local, NAT64 and IPv4 mapped):
sudo iptables -S OUTPUT | grep CLOUDIMG
sudo iptables -S CLOUDIMG-ATHENS-EGRESS | grep -E '^-A' | grep -v icmp
-A OUTPUT -m owner --uid-owner 997 -j CLOUDIMG-ATHENS-EGRESS
-A CLOUDIMG-ATHENS-EGRESS -d 127.0.0.0/8 -o lo -j RETURN
-A CLOUDIMG-ATHENS-EGRESS -d 169.254.0.0/16 -p tcp -j REJECT --reject-with tcp-reset
-A CLOUDIMG-ATHENS-EGRESS -d 100.64.0.0/10 -p tcp -j REJECT --reject-with tcp-reset
-A CLOUDIMG-ATHENS-EGRESS -d 10.0.0.0/8 -p tcp -j REJECT --reject-with tcp-reset
-A CLOUDIMG-ATHENS-EGRESS -d 172.16.0.0/12 -p tcp -j REJECT --reject-with tcp-reset
-A CLOUDIMG-ATHENS-EGRESS -d 192.168.0.0/16 -p tcp -j REJECT --reject-with tcp-reset
-A CLOUDIMG-ATHENS-EGRESS -d 0.0.0.0/8 -p tcp -j REJECT --reject-with tcp-reset
Refused connections are reset at once (REJECT) rather than silently dropped, so a module path that names an internal address fails immediately instead of hanging a build. Try it as the athens user: the metadata service and a private address are refused, the public proxy is reachable:
for t in http://169.254.169.254/latest/meta-data/ http://10.0.0.1/ https://proxy.golang.org/; do
if code=$(sudo setpriv --reuid=athens --regid=athens --clear-groups -- curl -s -m 5 -o /dev/null -w '%{http_code}' "$t"); then echo "$t -> HTTP $code"; else echo "$t -> refused"; fi
done
http://169.254.169.254/latest/meta-data/ -> refused
http://10.0.0.1/ -> refused
https://proxy.golang.org/ -> HTTP 200
Athens runs as the unprivileged athens user. Its service also carries the same ranges as a cgroup IPAddressDeny list (a second block that does not depend on iptables), and nginx and the first boot service cannot reach the metadata service:
ps -o user:8=,args= -C athens
systemctl show athens nginx athens-firstboot -p Id -p IPAddressDeny
athens /opt/athens/athens -config_file=/etc/athens/config.toml
IPAddressDeny=0.0.0.0/8 169.254.0.0/16 64:ff9b:1::/48 64:ff9b::/96 172.16.0.0/12 ::ffff:0.0.0.0/96 192.168.0.0/16 fc00::/7 10.0.0.0/8 100.64.0.0/10 168.63.129.16/32 fe80::/10
Id=athens.service
IPAddressDeny=168.63.129.16/32 169.254.169.254/32 fd00:ec2::254/128
Id=nginx.service
IPAddressDeny=168.63.129.16/32 169.254.169.254/32 fd00:ec2::254/128
Id=athens-firstboot.service
The rules are applied by cloudimg-athens-egress.service in one atomic iptables-restore transaction per address family: a reload replaces the chain in a single step, a reload that fails leaves the previous rules in place, and stopping the unit does not remove them. sudo systemctl reload cloudimg-athens-egress re-applies them.
Step 8: Rotate the password, use your own DNS name
sudo athens-rotate-password replaces the proxy password with a new random one, writes it to /root/athens-credentials.txt and reloads nginx; the old password stops working at once. Update the .netrc on your build machines and your CI secret store afterwards. Rotate whenever the password may have been exposed.
Point a DNS record at the instance, then set the name the proxy is served as. This mints a new self signed leaf certificate for the name (and the instance's IP addresses), updates nginx and the URLs in the credentials file, and keeps the name across reboots. Give your build machines the new certificate afterwards:
sudo athens-set-hostname <your-domain>
To use a certificate from your own certificate authority instead, copy it over the generated pair and reload nginx (running athens-set-hostname again replaces it with a self signed one):
sudo install -m 0644 <your-domain>.crt /etc/nginx/tls/athens.crt
sudo install -m 0600 <your-domain>.key /etc/nginx/tls/athens.key
sudo nginx -t && sudo systemctl reload nginx
Step 9: Server components
| Component | Path |
|---|---|
| Athens application | /opt/athens/athens (read only, owner root) |
| Licence and build provenance | /usr/share/doc/athens/ |
| Configuration | /etc/athens/config.toml |
| Module store | /var/lib/athens/storage (data volume) |
| Go toolchain used by Athens | /usr/local/go (Go 1.27.3) |
| Service units | athens.service, cloudimg-athens-egress.service, athens-firstboot.service in /etc/systemd/system/ |
| Egress rules | /usr/local/sbin/athens-egress-apply (--print shows the ruleset) |
| nginx site and password file | /etc/nginx/sites-available/cloudimg-athens, /etc/nginx/athens.htpasswd |
| Certificate and key | /etc/nginx/tls/ |
| Helpers | /usr/local/sbin/athens-rotate-password, athens-set-hostname, athens-guard |
| Credentials | /root/athens-credentials.txt (mode 0600) |
/opt/athens/athens -version | head -3
grep -E '^(source|toolchain|changes)' /usr/share/doc/athens/BUILD-PROVENANCE.txt
nginx -v 2>&1
Build Details:
Version: v0.20.0
Date: 2026-11-07-14:42:07-UTC
source: https://github.com/gomods/athens/archive/refs/tags/v0.20.0.tar.gz
toolchain: go1.27.3 linux/amd64 (https://go.dev/dl/go1.27.3.linux-amd64.tar.gz, sha256 65efbc80fdc1fa2cd2631cad5b481777a68e8725f34e89f50fc6ac714e379b40)
changes: dependency raise only (go.mod, no source patch): github.com/klauspost/compress@v1.20.2 golang.org/x/net@v0.62.0 go.etcd.io/etcd/client/pkg/v3@v3.6.14
nginx version: nginx/1.24.0 (Ubuntu)
The settings that matter most:
grep -E '^(GoBinaryEnvVars|StorageType|Port|SumDBs|NoSumPatterns|GoDownloadEnabled|DownloadMode|NetworkMode)' /etc/athens/config.toml
GoBinaryEnvVars = ["GOPROXY=https://proxy.golang.org,direct", "GOSUMDB=sum.golang.org", "GOTOOLCHAIN=local", "GOVCS=*:git", "GIT_TERMINAL_PROMPT=0", "GIT_ALLOW_PROTOCOL=https"]
StorageType = "disk"
Port = "127.0.0.1:3000"
SumDBs = ["https://sum.golang.org"]
NoSumPatterns = []
GoDownloadEnabled = false
DownloadMode = "sync"
NetworkMode = "strict"
Athens fetches new modules from proxy.golang.org first and directly from git (over HTTPS only) when a module is not there, and verifies them against sum.golang.org. Change a setting by editing /etc/athens/config.toml with the names from the Athens configuration documentation, then sudo systemctl restart athens. Logs: sudo journalctl -u athens for Athens, /var/log/nginx/access.log for requests (with the real client address).
Step 10: Security recommendations
- Allow port 22 in the security group from your own networks only, and port 443 only from the networks of your build machines and developers.
- Keep the proxy password in your CI system's secret store, and rotate it (Step 8) when someone leaves or it may have been exposed.
- Keep
GOSUMDBon in your clients; useGOPRIVATEonly for your own private module paths. - Back up
/var/lib/athens(an EBS snapshot of the data volume is simplest) if your builds rely on modules that could disappear upstream. - Use your own DNS name and certificate (Step 8).
- Keep the operating system patched; unattended security upgrades are on, and
sudo apt-get update && sudo apt-get upgradeapplies everything else.
Support and licensing
Athens is open source under the MIT License; there is no per user or per CPU licence fee. The licence is in /usr/share/doc/athens/. cloudimg is not affiliated with or endorsed by the Athens project or the Go project. The AMI price covers cloudimg's hardening, first boot setup and support.
- Email: support@cloudimg.co.uk
- Website: https://www.cloudimg.co.uk
- Support: 24/7, with a 24 hour response SLA