Roundup Issue Tracker 2.6.0 on AWS User Guide
Overview
This guide covers launching and using the Roundup Issue Tracker 2.6.0 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg developer and API tools listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.
Roundup is a mature open source issue and bug tracker written in Python, used for many years by projects including Python itself. It keeps issues with priorities, statuses, keywords, assignees and a nosy list, a full history on every item and saved queries, and gives you a web interface, a REST API and a command-line admin tool.
The cloudimg image runs Roundup as a private tracker:
- HTTPS from the first boot. nginx serves Roundup on port 443 with a certificate generated on your instance's first boot. Port 80 only redirects to HTTPS, other names for the instance redirect to its main address, and a request for any other name gets no answer.
- Sign-in required. Anonymous visitors see the login page and nothing else: no issue list, no self-registration, no user search and no REST or XML-RPC access. Roundup refuses to start if its schema grants anonymous visitors more, unless you explicitly opt in (Step 8).
- No default login. The image contains no password, no certificate, no
secret_keyand no host name. First boot generates them and writes the admin login to/var/lib/cloudimg/roundup-credentials.txt(root only). Roundup stores the password as a PBKDF2 hash, and it is set through standard input, never on a command line. - CSRF protection on. Every web form carries Roundup's anti-CSRF token, and Roundup checks the
Origin,RefererandHostheaders against the tracker's address, so another website cannot log you in or change anything with your browser. The REST API accepts writes only from the tracker's own origin. - Database on a socket. PostgreSQL 16 listens on its local socket only and Roundup signs in to it as its Linux user, so there is no database password to manage or leak. The database itself is created on your instance's first boot: the image ships an empty one.
- Rate limits that cannot be dodged. Logins, form posts, REST writes and any request carrying credentials are limited per client address. nginx always replaces the
X-Forwarded-Forheader with the address it accepted the connection from, so a client cannot pretend to be someone else. - No credentials in the logs. The access log records each request's path and status without its query string, and nginx's error log does not record refused requests.
- Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.
What is on the image: Roundup 2.6.0 (from the PyPI source release, checked against its published SHA-256 at build time) with the classic tracker template, gunicorn 26.2.0, PostgreSQL 16, nginx, Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent. The build record and the Roundup and gunicorn licences are in /usr/share/doc/cloudimg-roundup/.
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.
- A security group that allows TCP 443 (the tracker), TCP 80 (redirects to HTTPS) and TCP 22 (SSH). Open 443 to the networks your team works from; keep 22 open to your own networks only.
Step 1: Launch the instance
In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick Roundup Issue Tracker 2.6.0 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large (a t3.small is enough for a small team), your key pair, your subnet and the security group above.
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=roundup}]'
First boot gives the tracker the instance's public IPv4 address as its name. An Elastic IP keeps that address, and so the tracker's address and certificate, the same across stops and starts; Step 9 moves the tracker to your own domain.
Step 2: Connect to your instance
| Version | SSH user |
|---|---|
| Roundup Issue Tracker 2.6.0 on Ubuntu 24.04 | ubuntu |
ssh -i <your-key.pem> ubuntu@<public-ip>
Step 3: Check the services
First boot takes under a minute: it creates the PostgreSQL database, sets the tracker's address, generates the certificate, the secrets and the admin password, and only then starts Roundup and nginx. Check that everything is running:
systemctl is-active postgresql@16-main roundup-egress-guard roundup nginx \
&& sudo test -f /var/lib/cloudimg/roundup-firstboot.done && echo FIRSTBOOT_DONE
active
active
active
active
FIRSTBOOT_DONE
Only SSH and nginx listen on the network. Roundup (gunicorn) listens on the loopback address 127.0.0.1:8917 behind nginx, and PostgreSQL has no network port at all:
sudo ss -lntp | awk 'NR>1 && $4 !~ /^127\.0\.0\.5[34]/ {print $4, $6}' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g' | sort -u | tee /dev/stderr \
| awk '$1 !~ /^127\./ {sub(/.*:/, "", $1); print $1}' | sort -un | xargs | grep -qx '22 80 443' && echo "network listeners: 22 80 443"
0.0.0.0:22 users:(("sshd"),("systemd"))
0.0.0.0:443 users:(("nginx"),("nginx"),("nginx"))
0.0.0.0:80 users:(("nginx"),("nginx"),("nginx"))
127.0.0.1:8917 users:(("gunicorn"),("gunicorn"),("gunicorn"),("gunicorn"))
[::]:22 users:(("sshd"),("systemd"))
[::]:443 users:(("nginx"),("nginx"),("nginx"))
[::]:80 users:(("nginx"),("nginx"),("nginx"))
network listeners: 22 80 443
Step 4: Get your tracker address and admin login
Run sudo cat /var/lib/cloudimg/roundup-credentials.txt to see the tracker's address, the admin user name and its password. The file is readable by root only and holds these settings, all generated for this instance:
sudo sed -n 's/^\(ROUNDUP_[A-Z0-9_]*\)=.*/\1/p' /var/lib/cloudimg/roundup-credentials.txt | tee /dev/stderr \
| grep -qx ROUNDUP_ADMIN_PASSWORD && echo "credentials present"
ROUNDUP_URL
ROUNDUP_HOST
ROUNDUP_ADMIN_USER
ROUNDUP_ADMIN_PASSWORD
ROUNDUP_TLS_CERT
ROUNDUP_TLS_SHA256_FINGERPRINT
ROUNDUP_ANONYMOUS_ACCESS
credentials present
ROUNDUP_URL is https://<your instance's public IPv4 address>/, ROUNDUP_ADMIN_USER is admin and ROUNDUP_ADMIN_PASSWORD is its password. The settings that are not secret can be shown on their own:
sudo grep -E '^ROUNDUP_(URL|ADMIN_USER|TLS_SHA256_FINGERPRINT|ANONYMOUS_ACCESS)=' /var/lib/cloudimg/roundup-credentials.txt \
| tee /dev/stderr | grep -qx 'ROUNDUP_ANONYMOUS_ACCESS=login-required'
ROUNDUP_URL=https://203.0.113.10/
ROUNDUP_ADMIN_USER=admin
ROUNDUP_TLS_SHA256_FINGERPRINT=33:B7:80:...:8D
ROUNDUP_ANONYMOUS_ACCESS=login-required
Step 5: Sign in to the web interface
Open ROUNDUP_URL in your browser. The certificate is self-signed and generated on your instance, so the browser warns about it the first time: check that the fingerprint it shows matches ROUNDUP_TLS_SHA256_FINGERPRINT, then continue. For a certificate browsers trust already, use your own domain (Step 9).
Until you sign in, Roundup shows only its login box:

Sign in as admin with ROUNDUP_ADMIN_PASSWORD. Create New under Issues raises an issue; the issue list shows everything open, and every column header sorts it:

Each issue keeps its messages, files, nosy list (the people notified of changes) and a full history of every change:

User List and Add User under Administration manage accounts (Step 7):

The image ships one account, admin, plus Roundup's built-in anonymous user, which stands for visitors who have not signed in.
Step 6: Use the REST API
Roundup's REST API lives under ROUNDUP_URL + rest/. It takes HTTP Basic authentication, and because CSRF protection is on, a request that changes something must come from the tracker's own origin (Origin and Referer headers) and carry X-Requested-With. This creates an issue as admin and reads its title back; the login goes in a curl configuration file readable only by you, so the password never appears on a command line:
C=/var/lib/cloudimg/roundup-credentials.txt
U="$(sudo sed -n 's/^ROUNDUP_URL=//p' $C)"; H="$(sudo sed -n 's/^ROUNDUP_HOST=//p' $C)"
AUTH="$(mktemp /dev/shm/roundup-auth.XXXXXX)"
sudo awk 'index($0,"ROUNDUP_ADMIN_USER=")==1{u=substr($0,20)} index($0,"ROUNDUP_ADMIN_PASSWORD=")==1{p=substr($0,24)}
END{printf "user = \"%s:%s\"\n", u, p}' $C > "$AUTH"
R=(curl -s --cacert /etc/ssl/roundup/roundup.crt --resolve "$H:443:127.0.0.1" -H "Origin: ${U%/}" -H "Referer: $U" -H 'X-Requested-With: rest')
ID="$("${R[@]}" -K "$AUTH" -H 'Content-Type: application/json' \
-d '{"title": "First issue from the REST API", "priority": "bug"}' "${U}rest/data/issue" \
| python3 -c 'import json, sys; print(json.load(sys.stdin)["data"]["id"])')"
"${R[@]}" -K "$AUTH" "${U}rest/data/issue/$ID" \
| python3 -c 'import json, sys; print("read back:", json.load(sys.stdin)["data"]["attributes"]["title"])' \
| tee /dev/stderr | grep -qx 'read back: First issue from the REST API' && echo "created and read back"
shred -u "$AUTH"
read back: First issue from the REST API
created and read back
Without a login the API refuses everything, and a write from another origin is refused even with the right password:
C=/var/lib/cloudimg/roundup-credentials.txt
U="$(sudo sed -n 's/^ROUNDUP_URL=//p' $C)"; H="$(sudo sed -n 's/^ROUNDUP_HOST=//p' $C)"
AUTH="$(mktemp /dev/shm/roundup-auth.XXXXXX)"
sudo awk 'index($0,"ROUNDUP_ADMIN_USER=")==1{u=substr($0,20)} index($0,"ROUNDUP_ADMIN_PASSWORD=")==1{p=substr($0,24)}
END{printf "user = \"%s:%s\"\n", u, p}' $C > "$AUTH"
R=(curl -s --cacert /etc/ssl/roundup/roundup.crt --resolve "$H:443:127.0.0.1")
A="$("${R[@]}" -w ' %{http_code}' "${U}rest/data/issue")"
X="$("${R[@]}" -K "$AUTH" -w ' %{http_code}' -H 'Origin: https://evil.example' -H 'X-Requested-With: rest' \
-H 'Content-Type: application/json' -d '{"title": "from another site"}' "${U}rest/data/issue")"
shred -u "$AUTH"
printf '%s\n' "${A% *}" | python3 -c 'import json, sys; print("anonymous read:", sys.argv[1], json.load(sys.stdin)["error"]["msg"])' "${A##* }"
printf '%s\n' "${X% *}" | python3 -c 'import json, sys; print("cross-origin write:", sys.argv[1], json.load(sys.stdin)["error"]["msg"])' "${X##* }" \
| tee /dev/stderr | grep -q '^cross-origin write: 400 ' && [ "${A##* }" = 403 ] && echo "both refused"
anonymous read: 403 Forbidden.
cross-origin write: 400 Client is not allowed to use Rest Interface.
both refused
To call the API from another machine, use the tracker's address with your certificate (--cacert roundup.crt, copied from /etc/ssl/roundup/roundup.crt) and the same headers. Roundup's REST documentation is at roundup-tracker.org.
Step 7: Add users and change passwords
Add accounts in the web interface (Add User), or with Roundup's own admin tool through roundup-adm, which runs it on this tracker as the Roundup service user:
sudo roundup-adm list user
1: admin
2: anonymous
sudo roundup-adm create user username=alice address=alice@example.com roles=User
sudo roundup-rotate-password alice
roundup-adm refuses arguments that carry password= (they would land in the process list and sudo's log) and refuses initialise and nuke, which wipe the tracker. roundup-rotate-password <username> sets a new random password for that user and prints it once; with no user name it rotates the admin password and writes the new one to the credentials file. Users can change their own password under Your Details.
Step 8: Sign-in is required
Anonymous visitors get the login page only:
H="$(sudo sed -n 's/^ROUNDUP_HOST=//p' /var/lib/cloudimg/roundup-credentials.txt)"
curl -s --cacert /etc/ssl/roundup/roundup.crt --resolve "$H:443:127.0.0.1" "https://$H/issue" \
| grep -oE 'name="__login_name"|Hello, [a-z]+|First issue' | sort -u | tee /dev/stderr | grep -qx 'name="__login_name"' && echo "anonymous visitors get the login form only"
name="__login_name"
anonymous visitors get the login form only
The image keeps it that way on every start: roundup.service runs roundup-config-check first, which refuses to start Roundup if the tracker's schema.py gives the anonymous user more than the login page (viewing, self-registration, user search, REST, XML-RPC or email access), if the CSRF checks are weakened or the REST API is opened to any origin (*), if debugging or plain http is switched on, or if a database password, a TCP database host or an inline secret_key appears in config.ini. If you really want a public tracker, create /etc/roundup/allow-anonymous as well as editing schema.py, and keep the rate limits in Step 10 in mind.
Step 9: Use your own domain
Point a DNS name at the instance (an Elastic IP keeps it stable), then make it the tracker's address. This generates a new certificate for that name, serves the tracker on it (the instance's other names redirect to it), sets Roundup's own address, which its CSRF checks compare with, and restarts Roundup:
sudo roundup-set-site tracker.example.com
To use a certificate from a public CA instead, for example one issued by certbot with a DNS challenge, run roundup-set-site first, then replace /etc/ssl/roundup/roundup.crt (full chain) and /etc/ssl/roundup/roundup.key (mode 0600) and run sudo systemctl reload nginx.
Step 10: What the front door refuses
nginx answers only for the tracker's own names and passes requests to Roundup on loopback:
H="$(sudo sed -n 's/^ROUNDUP_HOST=//p' /var/lib/cloudimg/roundup-credentials.txt)"
{ curl -s -o /dev/null -w '%{http_code}\n' --cacert /etc/ssl/roundup/roundup.crt --resolve "$H:443:127.0.0.1" "https://$H/"
curl -s -o /dev/null -w '%{http_code}\n' -k -H 'Host: someone-else.example' https://127.0.0.1/
curl -s -o /dev/null -w '%{http_code}\n' --resolve "$H:80:127.0.0.1" "http://$H/"
} | xargs | tee /dev/stderr | grep -qx '200 000 301' && echo "front door OK"
200 000 301
front door OK
- Rate limits. Every request that can log in or change something (any method other than
GET,HEADandOPTIONS: web logins, form posts, REST and XML-RPC writes) and every request carrying anAuthorizationheader is limited to 30 a minute per client address (bursts of 20), on every path and every spelling of it. Everything else is limited to 20 a second (bursts of 200), with at most 64 connections per address. Over the limit, nginx answers429. Browsing while signed in uses the general limit. Roundup adds its own limit per user name: after 4 failed REST or XML-RPC logins in 10 minutes it answers429to that user, even with the right password, until the 10 minutes pass (api_failed_login_limitandapi_failed_login_interval_in_secin/opt/roundup/tracker/config.ini). - X-Forwarded-For. nginx replaces any
X-Forwarded-Forheader with the address it accepted the connection from and blanksX-Real-IP,X-Forwarded-Host,Forwardedand similar headers, and gunicorn trusts forwarded headers only from127.0.0.1, so a client cannot change the address the rate limits and logs use. If you put a load balancer in front of the instance, every client then shares the load balancer's address for the rate limits. - Sizes and types. Request bodies (file attachments included) are limited to 20 MiB. Compressed request bodies (
Content-Encoding) get415, andTRACEgets405. - Headers. Responses carry
Strict-Transport-Security, aContent-Security-Policythat allows only the tracker's own scripts and styles,X-Frame-Options: DENY,X-Content-Type-Options: nosniff,Referrer-Policy: same-originandCache-Control: no-store, and nginx does not advertise its version. The session cookie isSecure,HttpOnlyandSameSite=Lax.
What is logged: nginx's access log (/var/log/nginx/access.log) records the client address, request path and status, without query strings. nginx's error log records only start-up problems, not refused requests, so a password in a rejected request never reaches the disk. Roundup logs warnings and errors, such as refused CSRF checks, to the journal (journalctl -u roundup).
Step 11: Run the self-test
The image ships a self-test that checks the services and listening ports, that anonymous visitors are refused, that a wrong password is refused, that admin reads the REST API over HTTPS with this instance's certificate and that a cross-origin write is refused:
sudo roundup-selftest | tail -n 1 | tee /dev/stderr | grep -q '^ROUNDUP_SELFTEST_OK '
ROUNDUP_SELFTEST_OK anonymous: login required; wrong password refused; admin reads the REST API over https://203.0.113.10/ with this instance's certificate; cross-origin write refused
Step 12: Email
Roundup can email the nosy list on every change and take new issues and replies by email. The image sends no mail: until you configure an SMTP relay, Roundup writes outgoing mail to /dev/null. To send through your relay (Amazon SES, for example), store the SMTP password in a file Roundup reads, point Roundup at the relay and restart it:
sudo install -m 0640 -o root -g roundup /dev/null /etc/roundup/smtp-password
sudo nano /etc/roundup/smtp-password
C=/opt/roundup/tracker/config.ini
sudo roundup-ini-set $C mail host <smtp-relay>
sudo roundup-ini-set $C mail port 587
sudo roundup-ini-set $C mail tls yes
sudo roundup-ini-set $C mail username <username>
sudo roundup-ini-set $C mail password file:///etc/roundup/smtp-password
sudo roundup-ini-set $C mail domain tracker.example.com
sudo roundup-ini-set $C mail debug ""
sudo roundup-ini-set $C tracker email issue_tracker@tracker.example.com
sudo systemctl restart roundup
Roundup reads file:// values from the file's first line, so the password never sits in config.ini. The mail gateway (new issues by email) is described in Roundup's installation guide.
Step 13: Back up and restore
Issues, messages, users and history are in the PostgreSQL database roundup; attached files are in /var/lib/roundup/db. Back up both:
sudo -u postgres pg_dump -Fc roundup > roundup-$(date +%F).dump
sudo tar -C /var/lib/roundup -czf roundup-files-$(date +%F).tar.gz db
To restore on a new instance of this image, stop Roundup, restore the database with sudo -u postgres pg_restore --clean --if-exists -d roundup roundup-<date>.dump, unpack the files archive into /var/lib/roundup (owned by roundup) and start Roundup again.
Step 14: Security notes and the build record
The certificate generated at first boot is a server certificate only: it cannot sign other certificates.
openssl x509 -in /etc/ssl/roundup/roundup.crt -noout -ext basicConstraints,keyUsage,extendedKeyUsage | tee /dev/stderr | grep -q 'CA:FALSE'
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Key Usage: critical
Digital Signature, Key Encipherment
X509v3 Extended Key Usage:
TLS Web Server Authentication
Roundup is the 2.6.0 release, checked against its published SHA-256 at build time, and the database role has no password:
/opt/roundup/venv/bin/python -I -c 'import roundup, gunicorn; print("Roundup", roundup.__version__, "/ gunicorn", gunicorn.__version__)' | tee /dev/stderr | grep -qx 'Roundup 2.6.0 / gunicorn 26.2.0'
sed -n '1,2p' /usr/share/doc/cloudimg-roundup/BUILD-INFO.txt
sudo -u postgres psql -X -tAc "SELECT 'roles with a password: ' || count(*) FROM pg_authid WHERE rolpassword IS NOT NULL" \
| tee /dev/stderr | grep -qx 'roles with a password: 0'
Roundup 2.6.0 / gunicorn 26.2.0
roundup 2.6.0 (PyPI sdist roundup-2.6.0.tar.gz sha256 12fd8fb806047415f22131f965c6ab4026a28dc63f7054e055c8f891950591d4), MIT AND ZPL-2.0 AND PSF-2.0
(roundup-COPYING.txt); the 'classic' tracker template, anonymous grants removed (login required)
roles with a password: 0
Roundup runs as its own unprivileged user roundup with no login shell and no capabilities. It sees a read-only system, writes only to its file store, and cannot read the credentials file or the TLS key:
systemctl show -p User,ProtectSystem,ReadWritePaths,CapabilityBoundingSet roundup | tee /dev/stderr | grep -qx 'User=roundup'
CapabilityBoundingSet=
User=roundup
ReadWritePaths=/var/lib/roundup/db
ProtectSystem=strict
The Roundup code and the tracker are owned by root, so the service cannot change them. Roundup, nginx and PostgreSQL cannot reach the EC2 instance metadata service, and Roundup makes no outbound connections of its own: its only outbound feature is the email you configure in Step 12. Roundup's secret_key and jwt_secret are generated on your instance and read from /etc/roundup/. Roundup is licensed MIT, ZPL 2.0 and PSF 2.0, and gunicorn MIT; the licences are in /usr/share/doc/cloudimg-roundup/. PostgreSQL 16, nginx and psycopg2 come from Ubuntu's main archive.
Step 15: Keep it up to date
Ubuntu security updates, including PostgreSQL and nginx, install automatically. To apply everything now:
sudo apt-get update && sudo apt-get upgrade -y
New Roundup releases arrive as new versions of the AWS Marketplace listing: launch the new version, restore your backup into it (Step 13) and run sudo roundup-set-site with your domain if you use one. Roundup's upgrade notes for the database are in its upgrading guide.
Support
- Email: support@cloudimg.co.uk
- Website: https://www.cloudimg.co.uk
The other versions on this listing are on the developer and API tools guide.