Analytics AWS

GoAccess 1.12 on AWS User Guide

| Product: GoAccess 1.12 on Ubuntu 24.04 on AWS

Overview

This guide covers launching and using the GoAccess 1.12 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg BI and web analytics listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.

GoAccess is an open source web log analyser. It reads web server access logs (nginx, Apache, Amazon CloudFront, Elastic Load Balancing, Amazon S3, Caddy, Traefik, Squid and W3C formats) and turns them into a report of visitors, requested pages, static files, 404s, referrers, browsers, operating systems, status codes and time of day. On this image GoAccess runs in real-time mode: the report is a single HTML page whose panels update live over a WebSocket as new lines reach the log.

GoAccess's report overview for a fictional web shop's access log: the dashboard totals of requests, unique visitors, requested files, 404s and bandwidth for seven days above the unique visitors per day chart

GoAccess's report has no login of its own, and its WebSocket server would answer anyone who can reach it. The cloudimg image makes it safe to run on EC2:

  • A password on the report and its live updates, unique to your instance. nginx asks for a user name and password (HTTP basic authentication) for the report page and for its WebSocket. The password is generated on your instance's first boot into a root only file; no password, certificate, key or token secret is baked into the image. Change it at any time with goaccess-set-password.
  • A token on every WebSocket connection. GoAccess listens for WebSocket connections on 127.0.0.1:7890 only, behind nginx, and checks a signed token (a JWT, --ws-auth) on every connection. The signing secret is generated on first boot; the token in the report is valid for 24 hours and a fresh one is issued every 8 hours. If the report stops updating, reload the page.
  • Rate limited per client. Every request carries the password, so nginx limits requests per client (5 a second, bursts of 20) and open WebSocket connections per client (8), and answers 429 beyond that. The limit is per IPv4 address or per IPv6 /64 network, not per account. Forged X-Forwarded-For headers do not move it.
  • HTTPS from the first boot. nginx serves the report on port 443 with a certificate generated on your instance's first boot for its public address; port 80 redirects to HTTPS. Requests for a host name the instance does not serve get no answer.
  • GoAccess runs unprivileged. It runs as its own goaccess account with no Linux capabilities, reads logs through the adm group and can write only its report and its database. Other local accounts (including ubuntu) cannot connect to its WebSocket port.
  • Logged, without secrets. Failed and limited requests are recorded in nginx's access log by path only (a failed sign-in as 401, a refused WebSocket as 401 or 403, a rate limited request as 429), never with a query string (the WebSocket token travels in one) or a user name; nginx's error log for the report is kept at crit, so no credentials or tokens are written to disk. A working WebSocket connection is not logged, so the report does not count itself.
  • Other web sites cannot use your session. nginx refuses a WebSocket from another site's page and anything other than reading (GET and HEAD), and sends a content security policy that allows scripts and connections to the instance only.
  • No outbound connections. GoAccess and nginx need no network access beyond the instance itself: the goaccess and www-data accounts are refused, at the kernel level and on every boot, every connection except to localhost, including the EC2 instance metadata service, your VPC and the internet.
  • Built from the pinned upstream release. GoAccess 1.12 is compiled from the upstream release tarball, checked against its SHA-256, unmodified, with hardening flags (PIE, full RELRO, stack protector, FORTIFY_SOURCE). Ubuntu's own older package is not used.
  • Fully patched Ubuntu 24.04 LTS at build time.

Out of the box the report analyses nginx's own access log on the instance. Step 7 shows how to add your web servers' logs.

What is on the image: GoAccess 1.12 (/usr/local/bin/goaccess), nginx, the options file /etc/goaccess/cloudimg.conf, the tools goaccess-set-password, goaccess-set-domain, goaccess-rotate-secret and goaccess-selftest, Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent. No GeoIP database is included (Step 8).

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 with internet access.
  • A security group that allows TCP 22 (SSH) and TCP 443 (the report) from your own network, and TCP 80 (redirects to HTTPS) if you want plain http:// links to work. Open 443 only to the people who should read your traffic statistics.

Step 1: Launch the instance

In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick GoAccess 1.12 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large (a smaller instance such as t3.small handles logs of a few million lines well), your key pair, a subnet and a security group as above. An Elastic IP keeps the address, and so the certificate's name, stable across a stop and start.

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=goaccess}]'

Step 2: Connect to your instance

Version SSH user
GoAccess 1.12 on Ubuntu 24.04 ubuntu
ssh -i <your-key.pem> ubuntu@<public-ip>

Step 3: Check the services

First boot reads the instance's public address from EC2 instance metadata, generates the password, the WebSocket secret and the certificate and writes the credentials file; the egress backstop loads; then GoAccess and nginx start, and the timer that refreshes the report's token is armed:

systemctl is-active cloudimg-goaccess-egress goaccess nginx goaccess-refresh.timer
sudo test -f /var/lib/cloudimg/goaccess-firstboot.done && echo FIRSTBOOT_DONE

All four units are active and the first boot marker is present:

active
active
active
active
FIRSTBOOT_DONE

Only SSH and nginx listen on the network. GoAccess's WebSocket server listens on localhost only:

sudo ss -lntpH | awk '{print $4, $6}' | sed -E 's/users:\(\("([^"]+)".*/\1/' | grep -v '127.0.0.5' | sort -u
0.0.0.0:22 sshd
0.0.0.0:443 nginx
0.0.0.0:80 nginx
127.0.0.1:7890 goaccess
[::]:22 sshd
[::]:443 nginx
[::]:80 nginx

Step 4: Get your URL and password

sudo cat /root/goaccess-credentials.txt

The file is readable by root only and holds the URL, the user name, the password and the certificate's fingerprint:

# GoAccess 1.12 (cloudimg) on this instance. Open GOACCESS_URL in a browser and sign in as GOACCESS_USERNAME / GOACCESS_PASSWORD.
# The password is unique to this instance. Change it: sudo goaccess-set-password (or --stdin to choose one).
# The certificate is self-signed for this instance: compare its SHA-256 fingerprint with GOACCESS_TLS_SHA256.
# The report analyses /var/log/nginx/access.log in real time; add your own logs in /etc/goaccess/cloudimg.conf.
GOACCESS_URL=https://<public-ip>/
GOACCESS_HOST=<public-ip>
GOACCESS_USERNAME=admin
GOACCESS_PASSWORD=<GOACCESS_PASSWORD>
GOACCESS_TLS_SHA256=<64 hex digits, colon separated>
GOACCESS_ADDRESS_SOURCE=imdsv2-public-ipv4
GUIDE=https://www.cloudimg.co.uk/guides/goaccess-1-12-on-ubuntu-24-04-aws/

GOACCESS_ADDRESS_SOURCE says where the address came from: the public IPv4 address from instance metadata, or the private address when the instance has no public one.

The image includes a self test. Through nginx and pinned to this instance's certificate it checks that the report and other paths refuse a request with no password and one with a wrong password, that the password opens the report, and that the WebSocket refuses a request with no password, a wrong password, no token, a forged token or another site's origin, and works with the report's own token:

sudo goaccess-selftest
goaccess-selftest: 5 paths x (no credential, wrong password) -> 401; the password opens the report; WebSocket: no credential / wrong password / no JWT / forged JWT -> 401, cross-origin -> 403, the report's JWT -> 101 + data
GOACCESS_SELFTEST_OK

Step 5: Sign in

Open GOACCESS_URL in your browser. The certificate is self signed for your instance, so the browser warns once. To check you are talking to your instance, open the certificate details and compare its SHA-256 fingerprint with GOACCESS_TLS_SHA256, then continue. The browser then asks for a user name and password: enter admin and GOACCESS_PASSWORD. The browser remembers them until you close it.

On a new instance the report shows only the requests nginx itself has served, which at first are your own visits.

Step 6: Read the report

The pictures in this guide show the report for a week of a fictional web shop's access log (shop.example.com, with documentation addresses only), so that every panel has something in it. The panels on your instance fill as your own logs are analysed (Step 7).

The overview at the top (the picture at the start of this guide) gives the totals for the period: requests, unique visitors, requested files, static files, 404s, bandwidth and the time GoAccess spent. Below it each panel has a chart and a table; Panel Options switches the chart type and the metric, and the arrows page through the rows. The menu on the left jumps to a panel and sets the layout and colour scheme.

Requested files lists the most requested paths with hits, unique visitors, bandwidth, method and protocol:

GoAccess's Requested Files panel for the fictional shop: a bar chart of hits and visitors per path, and a table led by /static/js/app.js with 1,334 hits, /static/img/logo.svg and /static/img/hero.webp, then / with 771 hits and /products/, with 6,907 hits in total across 19 paths

Not found URLs lists the paths that answered 404. Scans for admin pages and leaked files show up here:

GoAccess's Not Found URLs (404s) panel for the fictional shop: a discontinued product page with 40 hits, then probes for /phpmyadmin/, /.git/config, /admin/, /.env and /wp-login.php, 201 hits in total across 14 paths

Browsers and Operating systems count your visitors' software; GoAccess recognises crawlers and lists them separately:

GoAccess's Browsers panel for the fictional shop: Chrome with 3,810 hits and 977 visitors, Safari, Firefox and Edge, then others and crawlers, 9,657 hits in total

HTTP status codes groups the responses by class; expand a row to see the individual codes:

GoAccess's HTTP Status Codes panel for the fictional shop: 6,880 2xx successes, 2,563 3xx redirections, 209 4xx client errors and 5 5xx server errors out of 9,657 hits

The other panels are visit time (hits per hour), virtual hosts, referring sites and URLs, static files, remote user, cache status, and, when you add a GeoIP database (Step 8), geo location and ASN. Click a column heading to sort a table. Requests in the report never carry a query string: GoAccess is started with --no-query-string, and this instance's own nginx log keeps the path only.

GoAccess keeps the data it has parsed in /var/lib/goaccess/db, so the report survives a restart or a reboot; it keeps 30 days of history by default.

Step 7: Add your logs

The report reads the log files listed in /etc/goaccess/cloudimg.conf:

sudo cat /etc/goaccess/cloudimg.conf
# cloudimg GoAccess options. goaccess-serve reads this file as KEY=VALUE lines (it is never run as a shell script) and
# refuses to start GoAccess when a line is not valid. After a change:  sudo systemctl restart goaccess
#
# LOG_FORMAT: one of GoAccess's predefined formats: COMBINED VCOMBINED COMMON VCOMMON W3C CLOUDFRONT CLOUDSTORAGE AWSELB
# SQUID AWSS3 CADDY AWSALB TRAEFIKCLF. This instance's nginx writes COMBINED (the request path without its query string).
LOG_FORMAT=COMBINED
# LOG_FILE: one log file per LOG_FILE line (repeat the key for more files): an absolute path under /var/log/, a regular
# file (never a symbolic link) that the adm group can read. Every file must use LOG_FORMAT.
LOG_FILE=/var/log/nginx/access.log
# KEEP_LAST: days of history kept in GoAccess's database (/var/lib/goaccess/db), 1 to 3650.
KEEP_LAST=30
# ANONYMIZE_IP: yes masks the last octet of IPv4 and the last 80 bits of IPv6 client addresses in the report.
ANONYMIZE_IP=no
# GEOIP_DATABASE: an optional MaxMind / DB-IP .mmdb file you supply, root-owned, in /etc/goaccess/geoip/. Empty = off.
GEOIP_DATABASE=

GoAccess reads each file as the goaccess account through the adm group, so a log file must be under /var/log/, be a regular file (not a symbolic link) and be readable by adm. All files in one report use the same LOG_FORMAT. To analyse your web servers' logs:

  1. Get the logs onto the instance under /var/log, for example in /var/log/web/, owned by root and readable by adm. Logs that keep arriving (a web server on this instance, rsync or a log shipper) are analysed live; GoAccess follows each file as it grows.
sudo install -d -o root -g adm -m 0750 /var/log/web
sudo install -o root -g adm -m 0640 /tmp/access.log /var/log/web/access.log
  1. Edit the options file (sudo nano /etc/goaccess/cloudimg.conf): add a LOG_FILE=/var/log/web/access.log line (one line per file, up to 32), set LOG_FORMAT to your logs' format (Apache and nginx in their default formats are COMBINED; an Application Load Balancer is AWSALB, CloudFront CLOUDFRONT, Amazon S3 AWSS3), and remove the LOG_FILE=/var/log/nginx/access.log line if the formats differ.

  2. Restart GoAccess. It checks the file first and refuses to start with a message that names the line that is wrong:

sudo systemctl restart goaccess
systemctl is-active goaccess

GoAccess adds what it reads to the history in its database. To start the report again from nothing (for example after switching to different logs), stop GoAccess, empty the database and start it:

sudo systemctl stop goaccess
sudo find /var/lib/goaccess/db -mindepth 1 -delete
sudo systemctl start goaccess

Set ANONYMIZE_IP=yes to mask the client addresses in the report, and KEEP_LAST to keep more or fewer days.

Step 8: Add a GeoIP database (optional)

No GeoIP database ships on the image, so the geo location and ASN panels are off. GoAccess reads MaxMind's GeoLite2 or DB-IP's free databases in .mmdb format. Download one (MaxMind's needs a free account and has its own licence terms), copy it to the instance, then:

sudo install -o root -g root -m 0644 /tmp/GeoLite2-City.mmdb /etc/goaccess/geoip/GeoLite2-City.mmdb

Set GEOIP_DATABASE=/etc/goaccess/geoip/GeoLite2-City.mmdb in /etc/goaccess/cloudimg.conf and run sudo systemctl restart goaccess. The databases change monthly; replace the file and restart GoAccess to update it. GoAccess cannot download the database itself, because it has no outbound access (Step 12).

Step 9: Change the password

To generate a new random password (written to the credentials file as GOACCESS_PASSWORD):

sudo goaccess-set-password

To choose your own (16 characters or more, read from standard input and not stored on the instance):

sudo goaccess-set-password --stdin

The new password takes effect at once; your browser asks for it again. The password is stored for nginx as a bcrypt hash in /etc/nginx/cloudimg/goaccess.htpasswd.

Step 10: Use your own domain, or a new address

The certificate, the host names nginx answers and the report's WebSocket address are set for the address the instance had on first boot. If you stop and start an instance that has no Elastic IP, its public address changes; run goaccess-set-domain with the new address. To use a domain, point an A record (for example stats.example.com) at the instance's public IP address, then:

sudo goaccess-set-domain <your-domain>

This sets the name nginx accepts, generates a new self signed certificate for it, points the report's WebSocket at wss://<your-domain>:443/ws, restarts GoAccess and nginx and updates GOACCESS_URL, GOACCESS_HOST and GOACCESS_TLS_SHA256 in the credentials file. Add more names or addresses after the first to serve them too (sudo goaccess-set-domain <your-domain> <public-ip>); open the report at the first name.

For a certificate your browser trusts, put the certificate chain for <your-domain> in /etc/ssl/goaccess/goaccess.crt and its private key in /etc/ssl/goaccess/goaccess.key (root only, mode 600) and run sudo systemctl reload nginx.

Step 11: The WebSocket token

The report page carries a token (a JWT signed with the instance's own secret) that its WebSocket presents to GoAccess. A token is valid for 24 hours. The goaccess-refresh.timer restarts GoAccess every 8 hours so the report always carries a token with hours to run; the data is kept across the restart. If the report has been open for a long time and stops updating, reload the page.

To make every token issued so far stop working at once (for example after someone who should no longer see the report has had it open):

sudo goaccess-rotate-secret
goaccess-rotate-secret: a new WebSocket secret is in place and GoAccess was restarted; reload the report page

Rotate it together with the password (Step 9): a token alone cannot open the WebSocket, because nginx asks for the password first.

Step 12: Outbound connections

GoAccess, nginx and the first boot service are refused any connection to the instance metadata service, and the goaccess and www-data accounts are refused every connection except to localhost, including your VPC, private networks and the internet, by an iptables backstop loaded on every boot. GoAccess needs no outbound connection: it reads only the log files on the instance. Only root, goaccess and nginx (www-data) may connect to GoAccess's port on localhost.

Step 13: Security checks

The password guards the report and its WebSocket; anything other than reading is refused; a host name the instance does not serve gets no answer; plain HTTP redirects; other local accounts cannot reach GoAccess directly; and the goaccess account cannot reach the metadata service or the internet:

H=$(sudo sed -n 's/^GOACCESS_HOST=//p' /root/goaccess-credentials.txt); WRONG="admin:not-the-password"
curl -sk -o /dev/null -w 'no password: %{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/"
curl -sk -o /dev/null -w 'wrong password: %{http_code}\n' --resolve "$H:443:127.0.0.1" -u "$WRONG" "https://$H/"
curl -sk -o /dev/null -w 'WebSocket without a password: %{http_code}\n' --resolve "$H:443:127.0.0.1" -H 'Connection: Upgrade' -H 'Upgrade: websocket' -H 'Sec-WebSocket-Version: 13' -H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' "https://$H/ws"
curl -sk -o /dev/null -w 'POST: %{http_code}\n' --resolve "$H:443:127.0.0.1" -X POST "https://$H/"
curl -sk -o /dev/null -w 'unknown host name: %{http_code}\n' --resolve evil.example:443:127.0.0.1 https://evil.example/ || true
curl -s -o /dev/null -w 'http: %{http_code}\n' --resolve "$H:80:127.0.0.1" "http://$H/"
curl -s -o /dev/null -w 'GoAccess directly, as ubuntu: %{http_code}\n' -m 5 http://127.0.0.1:7890/ || true
sudo -u goaccess curl -s -m 5 -o /dev/null -w 'metadata from the goaccess account: %{http_code}\n' http://169.254.169.254/latest/meta-data/ || true
sudo -u goaccess curl -s -m 5 -o /dev/null -w 'internet from the goaccess account: %{http_code}\n' https://www.cloudimg.co.uk/ || true
no password: 401
wrong password: 401
WebSocket without a password: 401
POST: 405
unknown host name: 000
http: 301
GoAccess directly, as ubuntu: 000
metadata from the goaccess account: 000
internet from the goaccess account: 000

GoAccess and nginx are blocked from the instance metadata service and reserved addresses, and the backstop refuses everything else that is not localhost:

for u in goaccess nginx; do printf '%s ' "$u"; systemctl show -p IPAddressDeny --value "$u" | tr ' ' '\n' | sort | tr '\n' ' '; echo; done
sudo iptables -S CLOUDIMG-GOACCESS-EGRESS
goaccess 168.63.129.16/32 169.254.169.254/32 192.0.0.0/24 198.18.0.0/15 2001::/32 2002::/16 224.0.0.0/4 240.0.0.0/4 64:ff9b:1::/48 64:ff9b::/96 ::ffff:0.0.0.0/96 fd00:ec2::254/128
nginx 168.63.129.16/32 169.254.169.254/32 192.0.0.0/24 198.18.0.0/15 2001::/32 2002::/16 224.0.0.0/4 240.0.0.0/4 64:ff9b:1::/48 64:ff9b::/96 ::ffff:0.0.0.0/96 fd00:ec2::254/128
-N CLOUDIMG-GOACCESS-EGRESS
-A CLOUDIMG-GOACCESS-EGRESS -d 127.0.0.0/8 -o lo -j RETURN
-A CLOUDIMG-GOACCESS-EGRESS -p tcp -j REJECT --reject-with tcp-reset
-A CLOUDIMG-GOACCESS-EGRESS -j REJECT --reject-with icmp-admin-prohibited

GoAccess runs as goaccess, on localhost, with no Linux capabilities, cannot gain any, and checks a token on every WebSocket connection (--ws-auth):

P=$(systemctl show -p MainPID --value goaccess)
ps -o user:9=,args= -p "$P"
sudo grep -E '^(CapEff|CapBnd|CapAmb|NoNewPrivs)' /proc/"$P"/status
goaccess  /usr/local/bin/goaccess --no-global-config --real-time-html --addr=127.0.0.1 --port=7890 --ws-url=wss://<public-ip>:443/ws --ws-auth=jwt:/etc/goaccess/ws-auth.secret --ws-auth-expire=24h --ping-interval=30 --output=/var/lib/goaccess/html/index.html --persist --restore --db-path=/var/lib/goaccess/db/ --keep-last=30 --log-format=COMBINED --no-query-string --html-report-title=GoAccess (cloudimg) - <public-ip> --log-file=/var/log/nginx/access.log
CapEff: 0000000000000000
CapBnd: 0000000000000000
CapAmb: 0000000000000000
NoNewPrivs: 1

The credentials, the password hash, the TLS key and the WebSocket secret are readable by root and the one service that needs each:

sudo stat -c '%a %U:%G %n' /root/goaccess-credentials.txt /etc/nginx/cloudimg/goaccess.htpasswd /etc/ssl/goaccess/goaccess.key /etc/goaccess/ws-auth.secret /etc/goaccess/cloudimg.conf
600 root:root /root/goaccess-credentials.txt
640 root:www-data /etc/nginx/cloudimg/goaccess.htpasswd
600 root:root /etc/ssl/goaccess/goaccess.key
640 root:goaccess /etc/goaccess/ws-auth.secret
644 root:root /etc/goaccess/cloudimg.conf

Other notes:

  • Rate limits. The burst of 20 covers normal use of the report. A script that requests the report in a tight loop from one address gets 429.
  • Client addresses. nginx replaces any X-Forwarded-For or X-Real-IP a client sends with the address it connected from.
  • Logs. nginx's access log (/var/log/nginx/access.log) records each request by path only, with no query string and no user name; failed sign-ins appear as 401, refused WebSocket connections as 401 or 403 and rate limited requests as 429. nginx's error log for the report is kept at crit. GoAccess logs to the journal (journalctl -u goaccess).
  • Response headers. nginx sends HSTS, a content security policy, X-Content-Type-Options: nosniff, X-Frame-Options: DENY and Referrer-Policy: same-origin, and no version number. The content security policy allows 'unsafe-eval' for scripts because the report's own templates and charts need it; scripts are still loaded from the instance only.
  • NVMe host identity. No NVMe-oF host identity ships in the image. If you install nvme-cli, its host NQN and host ID are your instance's own.
  • Who can reach it. Keep ports 22 and 443 open only to your own networks.

Step 14: Back up

GoAccess's history is in /var/lib/goaccess/db, and your logs stay where you put them. To copy the database and the options to your computer:

ssh -i <your-key.pem> ubuntu@<public-ip> 'sudo tar -C / -czf - var/lib/goaccess/db etc/goaccess/cloudimg.conf' > goaccess-backup.tar.gz

An EBS snapshot of the instance's volume is also a complete backup.

Step 15: Keep it up to date

To apply Ubuntu updates now:

sudo apt-get update && sudo apt-get upgrade -y

New GoAccess releases arrive as new versions of this listing, built and checked the same way. To move to one, launch the new version, copy your options file and logs to it, and restore the database from your backup (stop GoAccess first). Read the GoAccess release notes before you move.

Support and licensing

GoAccess is open source under the MIT licence. It is compiled from the upstream release unmodified; its licence text, its release notes and a record of how it was built, with the licences of the components in its source tree, are in /usr/share/doc/goaccess:

ls /usr/share/doc/goaccess
COPYING
NEWS
README.cloudimg

The source code is at github.com/allinurl/goaccess. GoAccess is not endorsed by or affiliated with cloudimg. There is no per user licence fee. The AMI price covers cloudimg's build, 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

The other versions on this listing are on the BI and web analytics guide.