GoCD Continuous Delivery 26.1.0 on AWS User Guide
Overview
This guide covers launching and using the GoCD Continuous Delivery 26.1.0 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg Hardened Apache Subversion (SVN) & CI/CD Tools listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.
GoCD is the open source continuous delivery server from Thoughtworks. You model your build, test and release process as pipelines of stages, jobs and tasks, triggered by changes in your source repositories (materials) or by other pipelines, and GoCD runs the jobs on agents and shows where every change is in the path to production (the value stream map).
The cloudimg image adds:
- No shared password and no shared keys. First boot generates a 24 character
adminpassword on your instance and writes it to/etc/cloudimg-credentials.txt(root only). GoCD's password file keeps only a bcrypt hash of it. First boot also creates GoCD's agent auto-register key, server id, token-generation key and webhook secret, and GoCD creates its cipher key (cipher.aes, which encrypts stored secrets) at its first start on your instance. GoCD refuses to start on this image until first boot has secured it. - A credential for everything. Every page and every API call needs a signed-in user or the admin's credentials; anonymous requests get the sign-in page or
401. Sign-in attempts are rate-limited per client address. - A local agent, ready to build. The instance's own agent registers at first boot with this instance's auto-register key and is enabled. GoCD's automatic approval of local agents is off, and any other agent stays Pending until an administrator enables it.
- GoCD on loopback behind nginx. GoCD listens on
127.0.0.1:8153only; nginx serves it on port 443 with a certificate generated on your instance, and port 80 only redirects. nginx does not expose GoCD's agent endpoints: remote agents connect over an SSH tunnel (Step 8). - No proprietary service wrapper. GoCD's own packages start the server and agent through the Tanuki Java Service Wrapper, which is not open source. This image does not install those packages: it runs GoCD's unmodified
go.jarandagent-bootstrapper.jar(checked against their SHA-256) directly on Ubuntu's OpenJDK 21, under cloudimg's own systemd units, with the JVM options GoCD's wrapper configuration would have used. - Tasks kept away from GoCD's secrets and the metadata service. Pipeline tasks run as
gocdagent, a separate account that cannot read or change GoCD's configuration, cipher key or password file, inside a systemd sandbox that denies the EC2 instance metadata service. - Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.
What is on the image: GoCD 26.1.0 (build 22803; Apache-2.0, the server and agent jars from GoCD's signed Debian repository), OpenJDK 21, git and nginx from Ubuntu 24.04, Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent.
Default user: ubuntu · Architecture: x86_64 · Recommended instance type: m5.large
Read this before you share it: pipelines run commands on this instance. Pipeline tasks run on this instance as the
gocdagentuser. Anyone who can create or change a pipeline can run any command as that user, and those commands can reach your VPC and the internet (only the instance metadata service is blocked). Only let trusted people create or change pipelines, restrict outbound security group rules as far as your pipelines allow, keep the instance metadata hop limit at 1, and attach no instance profile (or a least-privilege one). Step 7 explains each point.Trademark notice. GoCD is a trademark of Thoughtworks, Inc. cloudimg is not affiliated with, endorsed by or sponsored by Thoughtworks; the name is used only to identify the open source software in this image, which is the unmodified upstream GoCD release.
Prerequisites
- An AWS account subscribed to the listing, an EC2 key pair and a VPC subnet.
- A security group that allows TCP 443 (GoCD over HTTPS) and TCP 80 (redirects to HTTPS) from the networks your users work from, and TCP 22 (SSH) from your own network only. GoCD needs no other inbound port.
Step 1: Launch the instance
In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick GoCD Continuous Delivery 26.1.0 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large, your key pair, a security group as above, and a subnet. GoCD keeps its pipeline history and artifacts under /var/lib/go-server, and the local agent keeps its working copies under /var/lib/go-agent, both on the root volume; raise its size under Configure storage if you keep many artifacts. Under Advanced details, leave IAM instance profile empty (or choose a least-privilege one), set Metadata version to V2 only and Metadata response hop limit to 1.
With the AWS CLI (replace the values in angle brackets; this sets a 50 GiB root volume and no instance profile):
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,HttpPutResponseHopLimit=1 \
--block-device-mappings 'DeviceName=/dev/sda1,Ebs={VolumeSize=50,VolumeType=gp3}' \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=gocd}]'
Step 2: Connect to your instance
| Version | SSH user |
|---|---|
| GoCD Continuous Delivery 26.1.0 on Ubuntu 24.04 | ubuntu |
ssh -i <your-key.pem> ubuntu@<public-ip>
Step 3: Check the services
First boot works out the instance's name, generates the certificate and GoCD's secrets, starts GoCD once to secure it, registers and enables the local agent, proves over HTTPS that anonymous requests are refused and the new password is accepted, and only then opens the web server. It takes about two minutes:
systemctl is-active go-server go-agent nginx gocd-set-host
active
active
active
active
gocd-set-host runs on every boot before nginx: it keeps the served name, the certificate and nginx's host rules in step (Step 9).
Only SSH (22) and nginx (80, 443) listen on the network. GoCD (8153) is on the loopback address only (Java shows it as the IPv4-mapped [::ffff:127.0.0.1]), and so is the agent's own status port 8152. The :53 entries are Ubuntu's local DNS resolver, :323 is the time service on the loopback address and :68 is the DHCP client:
sudo ss -lntup | awk 'NR>1 {print $1, $5}' | sort -u
tcp 0.0.0.0:22
tcp 0.0.0.0:443
tcp 0.0.0.0:80
tcp 127.0.0.53%lo:53
tcp 127.0.0.54:53
tcp [::]:22
tcp [::]:443
tcp [::]:80
tcp [::ffff:127.0.0.1]:8152
tcp [::ffff:127.0.0.1]:8153
udp 127.0.0.1:323
udp 127.0.0.53%lo:53
udp 127.0.0.54:53
udp 172.31.94.75%ens5:68
udp [::1]:323
The server runs as the go account and the agent as the separate gocdagent account, both on Ubuntu's OpenJDK 21, from cloudimg's units go-server.service and go-agent.service. The agent shows as two Java processes: GoCD's agent bootstrapper and the agent it downloads from the server and starts. There is no service wrapper process:
ps -o user:10=,args= -C java | sed -E 's# -[^ ]+.* -jar # ... -jar #'
pgrep -a wrapper || echo "no wrapper process"
java -version 2>&1 | head -1
gocdagent /usr/lib/jvm/java-21-openjdk-amd64/bin/java ... -jar /usr/share/go-agent/lib/agent-bootstrapper.jar -serverUrl http://127.0.0.1:8153/go
gocdagent /usr/lib/jvm/java-21-openjdk-amd64/bin/java ... -jar agent.jar -serverUrl http://127.0.0.1:8153/go -sslVerificationMode FULL
go /usr/lib/jvm/java-21-openjdk-amd64/bin/java ... -jar /usr/share/go-server/lib/go.jar
no wrapper process
openjdk version "21.0.12.1" 2026-08-18
If nginx is not running, first boot has not finished, or it failed, in which case the web server is never opened. Check systemctl status gocd-firstboot and sudo tail -40 /var/log/cloudimg/gocd-firstboot.log.
Step 4: Get your address and password
sudo cat /etc/cloudimg-credentials.txt
The file is readable by root only and holds the address, the admin user name and password and the certificate fingerprint:
# GoCD - generated on first boot. Unique to this instance; keep it safe.
# Sign in at GOCD_URL as GOCD_ADMIN_USER / GOCD_ADMIN_PASSWORD. Compare GOCD_TLS_SHA256 with the certificate your browser
# shows (or install your own certificate: see the guide). Rotate the password: sudo gocd-set-password
GOCD_URL=https://ec2-203-0-113-10.compute-1.amazonaws.com/go/
GOCD_ADMIN_USER=admin
GOCD_ADMIN_PASSWORD=<GOCD_ADMIN_PASSWORD>
GOCD_TLS_SHA256=EB:F0:B7:16:33:36:CA:31:51:F4:02:53:E6:15:B5:D4:5C:99:E2:41:5B:F2:41:9D:24:FD:3B:65:D7:01:07:7E
To see everything but the password:
sudo grep -E '^GOCD_(URL|ADMIN_USER|TLS_SHA256)=' /etc/cloudimg-credentials.txt
GOCD_URL=https://ec2-203-0-113-10.compute-1.amazonaws.com/go/
GOCD_ADMIN_USER=admin
GOCD_TLS_SHA256=EB:F0:B7:16:33:36:CA:31:51:F4:02:53:E6:15:B5:D4:5C:99:E2:41:5B:F2:41:9D:24:FD:3B:65:D7:01:07:7E
You can confirm the whole path from the instance itself. The helper goes through nginx over HTTPS with the certificate pinned. It checks that GoCD's pages and API refuse anonymous requests, that the agent endpoints and GoCD's anonymous health and version pages are not served, that wrong and well known default passwords are refused, and that this instance's password signs in on the login form and the API:
sudo /usr/local/sbin/gocd-verify.sh
GOCD_ROUNDTRIP_OK (HTTPS: 13 API routes + schedule + unsigned webhook 401 and 7 UI routes -> login anonymously, 11 agent endpoints 403, health/version 404, wrong/default passwords refused; the minted password accepted on the API and the login form, session admin)
Change the password with gocd-set-password. With no argument it generates a new 24 character password; with - it reads yours from standard input (at least 16 printable characters, no spaces, quotes or backslashes). It never accepts the password as an argument, so it does not appear in the process list or your shell history. GoCD picks up the change within a few seconds without a restart, so running pipelines are not interrupted. The command updates /etc/cloudimg-credentials.txt, and it is also the way back in if the password is lost:
$ sudo gocd-set-password
gocd-set-password: the admin password was replaced (sudo cat /etc/cloudimg-credentials.txt)
To choose your own:
$ read -rs NEWPW && printf '%s' "$NEWPW" | sudo gocd-set-password - ; unset NEWPW
gocd-set-password: the admin password was replaced (sudo cat /etc/cloudimg-credentials.txt)
The password file (/etc/gocd-auth/password.properties, readable by root and the go server account only) keeps a bcrypt hash at cost 12, never the password:
sudo sed -nE 's/^(admin:\$2y\$[0-9]+\$).*/\1.../p' /etc/gocd-auth/password.properties
admin:$2y$12$...
Step 5: Sign in
Open GOCD_URL in your browser. The certificate is self-signed until you install your own (Step 9), so the browser warns once; compare the fingerprint it shows with GOCD_TLS_SHA256. GoCD shows its sign-in page; use admin and the password from Step 4. There is no sign-up page:

The dashboard lists your pipelines, grouped, with the result of each stage of their latest run. Until you create one it opens GoCD's pipeline wizard. This is the dashboard with the pipeline from Step 6:

Agents lists the build agents. The instance's own agent, cloudimg-local-agent, registered at first boot with this instance's auto-register key and is enabled and idle:

Under Admin > Authorization Configuration you can see how sign-in works: GoCD's bundled password file plugin, reading /etc/gocd-auth/password.properties. The admin user is GoCD's only administrator:

Add users by adding lines to the password file: name:bcrypt-hash, one per user (for example, a hash from htpasswd -nbBC 12 <name> <password> on a trusted machine). Make users administrators, or give them only view or operate rights on pipeline groups, under Admin > Security > Role Configuration and the pipeline groups' permissions. Anyone who can edit a pipeline can run commands on this instance (Step 7), so give edit rights only to trusted people. For single sign-on, install an authorization plugin such as GoCD's LDAP or GitHub OAuth plugin.
Step 6: Create and run a pipeline
Choose Admin > Pipelines > Create a new pipeline (or Add new pipeline on the dashboard), and fill in the wizard: a material (your git repository's URL and branch; use Test Connection), a pipeline name, a stage and a job with its task, for example a script such as make test. Save + Run This Pipeline starts it. GoCD then runs the pipeline whenever the material changes (it polls the repository every minute) and on Trigger.
The job's Console tab shows the task's output. It ran as the gocdagent user on this instance, in the local agent's working directory:

The same from a script, with GoCD's REST API. This round trip on the instance itself creates a pipeline whose material is a small public repository on GitHub and whose one task prints who it ran as, lists the checkout, tries the instance metadata service and tries to read GoCD's configuration; it runs the pipeline, waits for the result and prints the job's console. The admin password is read from the credentials file into a file only, never printed or put on a command line:
D=$(mktemp -d) && cd "$D" && umask 077
HOST=$(sudo sed -n 's#^GOCD_URL=https://\([^/]*\)/.*$#\1#p' /etc/cloudimg-credentials.txt)
B="https://${HOST}/go"
C=(-s --max-time 60 --cacert /etc/ssl/gocd/gocd.crt --resolve "${HOST}:443:127.0.0.1" -H 'Accept: application/vnd.go.cd+json')
# the password goes into a curl config file (printf is a shell builtin), never onto a command line
printf 'user = "admin:%s"\n' "$(sudo sed -n 's/^GOCD_ADMIN_PASSWORD=//p' /etc/cloudimg-credentials.txt)" > auth.cfg
A=("${C[@]}" -K auth.cfg)
# 1. a pipeline: one git material, one stage, one job, one task
cat > pipeline.json <<'JSON'
{"group": "guide", "pipeline": {"name": "hello-cloudimg",
"materials": [{"type": "git", "attributes": {"url": "https://github.com/octocat/Hello-World.git", "branch": "master"}}],
"stages": [{"name": "build", "jobs": [{"name": "hello", "tasks": [{"type": "exec", "attributes": {"command": "/bin/sh",
"arguments": ["-c", "echo \"Hello from GoCD as $(id -un) in $(pwd)\"; ls; echo \"metadata service from a task: $(curl -s -m 3 -o /dev/null -w '%{http_code}' -X PUT -H 'X-aws-ec2-metadata-token-ttl-seconds: 60' http://169.254.169.254/latest/api/token)\"; cat /etc/go/cruise-config.xml > /dev/null || true"]}}]}]}]}}
JSON
curl "${A[@]}" -o /dev/null -w 'create pipeline: %{http_code}\n' -X POST -H 'X-GoCD-Confirm: true' \
-H 'Content-Type: application/json' --data @pipeline.json "$B/api/admin/pipelines"
# 2. run it (GoCD needs a few seconds to read the material first) and wait for the result
for i in $(seq 1 24); do
S=$(curl "${A[@]}" -o /dev/null -w '%{http_code}' -X POST -H 'X-GoCD-Confirm: true' "$B/api/pipelines/hello-cloudimg/schedule")
[ "$S" = 202 ] && break; sleep 5
done
echo "schedule: $S"
for i in $(seq 1 60); do
read -r N R < <(curl "${A[@]}" "$B/api/pipelines/hello-cloudimg/history" | python3 -c 'import json,sys; p=json.load(sys.stdin)["pipelines"]; print(p[0]["counter"], p[0]["stages"][0].get("result", "") if p else "")' 2>/dev/null)
case "$R" in Passed|Failed|Cancelled) break ;; esac; sleep 3
done
echo "result: $R"
# 3. the job's console log: the task's own output (&1 / &2) and the job's result
curl "${A[@]}" "$B/files/hello-cloudimg/$N/build/1/hello/cruise-output/console.log" | grep -E '^(&1|&2|j0)\|' | sed -E 's/^..\|[0-9:.]+ //'
cd / && rm -rf "$D"
create pipeline: 200
schedule: 202
result: Passed
Hello from GoCD as gocdagent in /var/lib/go-agent/pipelines/hello-cloudimg
README
metadata service from a task: 000
cat: /etc/go/cruise-config.xml: Permission denied
[go] Current job status: passed
000 means the task could not reach the metadata service at all, and the configuration file is refused to the agent's account. To remove the example pipeline again:
D=$(mktemp -d) && cd "$D" && umask 077
HOST=$(sudo sed -n 's#^GOCD_URL=https://\([^/]*\)/.*$#\1#p' /etc/cloudimg-credentials.txt)
B="https://${HOST}/go"
printf 'user = "admin:%s"\n' "$(sudo sed -n 's/^GOCD_ADMIN_PASSWORD=//p' /etc/cloudimg-credentials.txt)" > auth.cfg
A=(-s --max-time 60 --cacert /etc/ssl/gocd/gocd.crt --resolve "${HOST}:443:127.0.0.1" -H 'Accept: application/vnd.go.cd+json' -K auth.cfg)
curl "${A[@]}" -o /dev/null -w 'delete pipeline: %{http_code}\n' -X DELETE "$B/api/admin/pipelines/hello-cloudimg"
curl "${A[@]}" -o /dev/null -w 'delete pipeline group: %{http_code}\n' -X DELETE "$B/api/admin/pipeline_groups/guide"
cd / && rm -rf "$D"
delete pipeline: 200
delete pipeline group: 200
From your workstation, use the same calls with https://<host>/go/api/.... GoCD's API takes the admin's user name and password (HTTP Basic, rate-limited per client address like the sign-in form): keep the password in a curl config file (-K), not on the command line. Use the Accept: application/vnd.go.cd+json header and, for POST and DELETE, X-GoCD-Confirm: true.
Step 7: CI risk: who can run what
GoCD is a tool for running other people's code, so who may define pipelines is the main security decision. On this image:
- Pipeline tasks run on the instance as the
gocdagentuser. The local agent runs every job insidego-agent.service, asgocdagent. Anyone who can create or change a pipeline (or the repository a pipeline builds, if the pipeline runs its scripts) can run any command as that user. - Tasks can reach your VPC and the internet. Builds need to fetch code, packages and artifacts, so only the instance metadata service is blocked for the agent. A task can connect to anything the instance's security group and routing allow: restrict outbound security group rules as far as your pipelines allow, and do not put this instance in a subnet that can reach systems a pipeline author should not.
- Tasks cannot read GoCD's secrets. The server runs as
go; its configuration (/etc/go/cruise-config.xml), the cipher key that decrypts stored secrets (/etc/go/cipher.aes), the password file and the server's data are owned bygoor root and closed to other accounts, so a task running asgocdagentcannot read or change them. Secrets a pipeline uses (secure environment variables) are handed to its jobs by design, so a pipeline author can still read the secrets of the pipelines they can edit. - What a task cannot do here. The agent's sandbox applies to every task: no
sudoorsu(no new privileges), no capabilities, a read-only system outside/var/lib/go-agentand/var/log/go-agent, a private/tmp, and no route to the instance metadata service (169.254.169.254 and its IPv6 address). Neithergocdagentnorgohas a login shell; both are in no group but their own (notdocker), cannot usecronorat, and SSH refuses them, so a task cannot start a process outside the sandbox. - Keep the hop limit at 1. The metadata block covers the GoCD services. A metadata hop limit of 1 (Step 1) keeps any container you later run on this host from fetching IMDSv2 tokens as well.
- Attach no instance profile, or a least-privilege one. Other software on the instance, and root, can still reach the metadata service, so an instance profile's permissions are only as safe as the instance. Give pipelines cloud credentials as GoCD secure variables (or through a remote agent's own role) scoped to what the jobs need.
- Untrusted work goes on separate agents. To build code from people you do not fully trust, disable the local agent (Agents, select it, Disable) and run those jobs on separate agent machines that hold nothing you would miss (Step 8).
The sandbox and the metadata block, as systemd applies them to the agent (and so to every task), the server and nginx:
systemctl show go-agent -p User -p NoNewPrivileges -p ProtectSystem -p ProtectHome -p PrivateTmp -p CapabilityBoundingSet -p UMask
systemctl show go-server go-agent nginx -p Id -p IPAddressDeny
UMask=0027
CapabilityBoundingSet=
User=gocdagent
PrivateTmp=yes
ProtectHome=yes
ProtectSystem=strict
NoNewPrivileges=yes
IPAddressDeny=169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128
Id=go-server.service
IPAddressDeny=169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128
Id=go-agent.service
IPAddressDeny=169.254.169.254/32 168.63.129.16/32 fd00:ec2::254/128
Id=nginx.service
The two accounts and the ways out of the sandbox that are closed:
id go; id gocdagent
getent passwd go gocdagent | cut -d: -f1,7
cat /etc/cron.allow
sudo sshd -T | grep -iE '^(denyusers|passwordauthentication|permitrootlogin)'
uid=997(go) gid=987(go) groups=987(go)
uid=995(gocdagent) gid=986(gocdagent) groups=986(gocdagent)
go:/usr/sbin/nologin
gocdagent:/usr/sbin/nologin
root
permitrootlogin no
passwordauthentication no
denyusers go
denyusers gocdagent
GoCD's secrets and who may read them; the agent's account is refused:
sudo stat -c '%U:%G %a %n' /etc/go /etc/go/cruise-config.xml /etc/go/cipher.aes /etc/gocd-auth /etc/gocd-auth/password.properties /var/lib/go-server
sudo -u gocdagent head -c 0 /etc/go/cruise-config.xml /etc/go/cipher.aes /etc/gocd-auth/password.properties || echo "refused to gocdagent"
go:go 750 /etc/go
go:go 640 /etc/go/cruise-config.xml
go:go 640 /etc/go/cipher.aes
root:go 750 /etc/gocd-auth
root:go 640 /etc/gocd-auth/password.properties
go:go 750 /var/lib/go-server
head: cannot open '/etc/go/cruise-config.xml' for reading: Permission denied
head: cannot open '/etc/go/cipher.aes' for reading: Permission denied
head: cannot open '/etc/gocd-auth/password.properties' for reading: Permission denied
refused to gocdagent
Step 8: Agents and plugins
The local agent. cloudimg-local-agent is enabled at first boot because it presented this instance's auto-register key; first boot then removes the key from the agent's files. GoCD's automatic approval of agents on the same machine is off, so any other agent that registers stays Pending until an administrator enables it on the Agents page. To run more jobs at once, add agents on other machines rather than more work on this one.
Remote agents connect over SSH. nginx does not serve GoCD's agent endpoints on port 443, and GoCD itself listens on loopback only, so an agent on another machine reaches the server through an SSH tunnel to this instance. On the agent machine (with GoCD's agent installed from gocd.org and an SSH key for this instance):
ssh -N -L 8153:127.0.0.1:8153 -i <your-key.pem> ubuntu@<public-ip>
Start the agent with http://localhost:8153/go as its server URL (for example java -jar agent-bootstrapper.jar -serverUrl http://localhost:8153/go, or that URL in the packaged agent's configuration), and enable it on the Agents page when it shows as Pending. Keep the tunnel running (for example with autossh or a systemd unit). Give each remote agent its own key pair, and a machine you are willing to let pipeline authors control.
Plugins. GoCD's bundled plugins (the password file and LDAP authorization plugins, the YAML and JSON configuration repository plugins, and others) are installed with it. Add more by putting the plugin's jar in /var/lib/go-server/plugins/external (owned by go) and restarting GoCD (sudo systemctl restart go-server). Each plugin runs inside GoCD with its full access: install only plugins you trust and keep them updated.
Step 9: Your own host name and certificate
On every boot gocd-set-host works out the name to serve: the name you set, else the instance's public DNS name from the instance metadata service (IMDSv2). GoCD's site URL follows it:
systemctl is-enabled gocd-set-host.service
sudo gocd-set-hostname --boot
enabled
gocd-set-hostname: unchanged (ec2-203-0-113-10.compute-1.amazonaws.com)
Use your own DNS name. Point a DNS record at the instance (an Elastic IP keeps the address fixed), then run sudo gocd-set-hostname gocd.example.com. The name is kept in /etc/cloudimg/gocd-hostname for every later boot, the credentials file is updated, nginx serves that name and redirects the instance's other addresses to it, and browser requests from other origins are refused. Because the certificate in place was the one cloudimg generated, it is re-created for the new name.
Which certificates are replaced. Only a certificate cloudimg generated is ever re-created: its SHA-256 fingerprint is recorded, root owned, in /var/lib/cloudimg/gocd-cert.sha256, and a certificate is treated as cloudimg's only while it matches that record:
sudo cat /var/lib/cloudimg/gocd-cert.sha256
openssl x509 -in /etc/ssl/gocd/gocd.crt -noout -fingerprint -sha256
EB:F0:B7:16:33:36:CA:31:51:F4:02:53:E6:15:B5:D4:5C:99:E2:41:5B:F2:41:9D:24:FD:3B:65:D7:01:07:7E
sha256 Fingerprint=EB:F0:B7:16:33:36:CA:31:51:F4:02:53:E6:15:B5:D4:5C:99:E2:41:5B:F2:41:9D:24:FD:3B:65:D7:01:07:7E
Install your own certificate (from your CA, or Let's Encrypt with a DNS challenge) by putting the full chain and the key at the same paths. Anything there that does not match the record is yours and is never replaced, not on boot and not when the name changes:
sudo install -m 0644 -o root -g root fullchain.pem /etc/ssl/gocd/gocd.crt
sudo install -m 0600 -o root -g root privkey.pem /etc/ssl/gocd/gocd.key
sudo nginx -t && sudo systemctl reload nginx
Go back to a generated certificate by removing both files and running the helper with the name to serve:
$ sudo rm /etc/ssl/gocd/gocd.crt /etc/ssl/gocd/gocd.key
$ sudo gocd-set-hostname gocd.example.com
To return to the instance's own public name on every boot, sudo rm /etc/cloudimg/gocd-hostname && sudo gocd-set-hostname --boot.
Step 10: Backups and updates
GoCD keeps its configuration in /etc/go (including cipher.aes, which decrypts every secure variable) and its database, pipeline history and artifacts in /var/lib/go-server; the password file is /etc/gocd-auth/password.properties. Back them up with an EBS snapshot of the root volume, with GoCD's own Admin > Server Maintenance > Backup (it writes to /var/lib/go-server/artifacts/serverBackups), or by stopping GoCD and copying the folders. A backup holds the cipher key, so keep it as safe as the instance:
sudo systemctl stop go-agent go-server
sudo tar -C / -czf /root/gocd-backup.tar.gz etc/go etc/gocd-auth var/lib/go-server
sudo systemctl start go-server go-agent
Ubuntu packages, OpenJDK and nginx included, are updated by unattended security upgrades:
systemctl is-enabled unattended-upgrades apt-daily-upgrade.timer
enabled
enabled
GoCD itself is not an apt package on this image (GoCD's packages would install the proprietary service wrapper), so unattended upgrades does not touch it. To move to a newer GoCD release, back up first, then stop both services, replace /usr/share/go-server/lib/go.jar and /usr/share/go-agent/lib/agent-bootstrapper.jar with the jars from the new release's packages (owned by root, mode 0644; check their checksums against GoCD's release page), and start the services again: GoCD upgrades its database on start, and the agent downloads its matching agent jars from the server. cloudimg also publishes a new version of this listing for new GoCD releases.
Security notes
- Credential first. Anonymous users get the sign-in page or
401and nothing else, on every page and API route. GoCD's anonymous health and version endpoints and its agent endpoints are not served by nginx. - Rate-limited sign-in. The login form is limited per client address (10 a minute), and so are requests carrying credentials to the API.
- Exact listeners. Only 22, 80 and 443 listen on the network (Step 3); GoCD and the agent's status port are on loopback. Port 80 only redirects to HTTPS. Open the ports in the security group only to the networks that need them.
- Forwarded headers. nginx sends GoCD the configured host name,
https, port 443 and the real client address (X-Forwarded-Foris set from the connection, never passed through), drops client-suppliedForwardedheaders, and refuses state-changing requests and WebSocket upgrades from other origins. Responses carry HSTS. - Instance metadata. GoCD's server and agent (and so every pipeline task), nginx, first boot and the helper commands are denied the EC2 instance metadata service by systemd (Step 7). Launch with IMDSv2 required and a hop limit of 1, as in Step 1.
- Secrets per instance. The admin password, GoCD's cipher key, agent auto-register key, server id, token-generation key and webhook secret, the local agent's identity, the HTTPS certificate and the SSH host keys are all created on your instance's first boot; none is in the image.
Build provenance and licence
GoCD 26.1.0 (build 22803) comes from GoCD's Debian repository at download.gocd.org, with the repository's signing key pinned to fingerprint 322259C82D3082B3E32AEC2ED8843F288816C449 and the packages' SHA-256 checked against the signed index. The build does not install the packages: it unpacks them without running their scripts and copies out only go.jar, agent-bootstrapper.jar and GoCD's licence, byte for byte, then removes the repository again. GoCD's packages also carry the Tanuki Java Service Wrapper, which is proprietary, and the build checks that no part of it is on the image:
head -4 /usr/share/doc/cloudimg/gocd-BUILD-INFO.txt
sha256sum /usr/share/go-server/lib/go.jar /usr/share/go-agent/lib/agent-bootstrapper.jar
dpkg-query -W go-server go-agent 2>&1
sudo find / -xdev \( -name 'wrapper*.jar' -o -name 'libwrapper*' -o -name 'wrapper.conf' -o -name 'wrapper-license*' \) 2>/dev/null | wc -l
GoCD Continuous Delivery 26.1.0-22803 on Ubuntu 24.04 (cloudimg AWS bundle member dev-cicd)
go-server_26.1.0-22803_all.deb sha256 70bd6ba0b6fbfdbd8be8d5ffce2d8941ac285e7ae757eac27af7b85bc080e868 and go-agent_26.1.0-22803_all.deb sha256 c2d122ed855e019d07d5a89590f128d98e957f05136ff6985e6de8b735265063, downloaded at build time from https://download.gocd.org (Signed-By exactly key 322259C82D3082B3E32AEC2ED8843F288816C449); NOT installed: unpacked with dpkg-deb -x
shipped from them, unmodified: /usr/share/go-server/lib/go.jar sha256 315080a2e8b342acd3fc741ed5bd0da7e28b4ad504bfc9d3f202fe5890266a31; /usr/share/go-agent/lib/agent-bootstrapper.jar sha256 69424acb64cd93e5a02cbf5bd3207833707b4607ff1fd4aecdce1042f6d82511; Apache-2.0 LICENSE
NOT shipped: the debs' Tanuki Java Service Wrapper (wrapper binary, libwrapper, wrapper.jar, wrapper.conf) and their maintainer scripts; the server and agent run under cloudimg's own systemd units
315080a2e8b342acd3fc741ed5bd0da7e28b4ad504bfc9d3f202fe5890266a31 /usr/share/go-server/lib/go.jar
69424acb64cd93e5a02cbf5bd3207833707b4607ff1fd4aecdce1042f6d82511 /usr/share/go-agent/lib/agent-bootstrapper.jar
dpkg-query: no packages found matching go-server
dpkg-query: no packages found matching go-agent
0
GoCD is licensed under the Apache License 2.0; the libraries inside its jars carry their own open source licences. GoCD's source: github.com/gocd/gocd (tag 26.1.0). GoCD's security advisories are published on GitHub; cloudimg publishes a new version of this listing when a release fixes one.
Troubleshooting
- The sign-in page refuses the password. Read the password again from
/etc/cloudimg-credentials.txt(the user name isadmin). Aftergocd-set-passwordthe old one is refused. 429 Too Many Requests. Too many sign-in attempts or API calls with credentials from one address (10 sign-ins a minute). Wait a minute.403on a form or WebSocket from another site, or the connection dropped with no answer. The request named a host or origin the instance does not serve. Use the name inGOCD_URL, or set yours withgocd-set-hostname(Step 9).- A pipeline stays scheduled and never runs. No enabled agent is idle: the local agent is disabled or busy, or a remote agent is still Pending (Step 8). Check the Agents page.
- A material fails to update. GoCD (as
go) and the agent (asgocdagent) both fetch the repository. Check the URL with Test Connection; for a private repository add its credentials to the material (they are stored encrypted with the instance's cipher key), and make sure the security group allows the outbound connection. - A task cannot reach the metadata service or use
sudo. That is the sandbox (Step 7). Give pipelines cloud credentials as secure variables, or run them on a remote agent with its own role. - Logs:
journalctl -u go-serverandjournalctl -u go-agent,/var/log/go-server/and/var/log/go-agent/,/var/log/nginx/access.logand/var/log/nginx/error.log, and/var/log/cloudimg/gocd-firstboot.log.
Support
cloudimg images come with 24/7 support. Email support@cloudimg.co.uk with your AWS account ID, the instance ID and the output of sudo /usr/local/sbin/gocd-verify.sh. For GoCD itself, see the GoCD documentation.