Deployment guide

GoAccess 1.12

Overview

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). It 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.

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

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

What you get:

  • 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 the password 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. It 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). It 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. nginx's access log records failed and limited requests by path only. A failed sign-in appears as 401, a refused WebSocket as 401 or 403, and a rate-limited request as 429. The log never contains 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 websites cannot use your session. nginx refuses a WebSocket from another site's page, and anything other than reading (GET and HEAD). It also 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. On every boot, the kernel refuses the goaccess and www-data accounts every connection except to localhost. This includes the EC2 instance metadata service, your VPC and the internet.
  • Built from the pinned upstream release. GoAccess 1.12 is compiled unmodified from the upstream release tarball, checked against its SHA-256, 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.
  • Image contents: GoAccess 1.12 (/usr/local/bin/goaccess), nginx, the options file /etc/goaccess/sac.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).
  • Defaults: the default user is ubuntu, the architecture is x86_64 and the recommended instance type is m5.large.
  • 24/7 SecureAnyCloud support

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.

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. Also allow TCP 80 (redirects to HTTPS) if you want plain http:// links to work.
  • Open port 443 only to the people who should read your traffic statistics.
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, choose your Region, then choose Launch through EC2.

Choose m5.large, your key pair, a subnet and a security group as described in Prerequisites. A smaller instance such as t3.small handles logs of a few million lines well. An Elastic IP keeps the address, and so the certificate's name, stable across a stop and start.

To launch with the AWS CLI instead, 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}]'
2

Connect to your instance

Connect over SSH as the ubuntu user, which is the SSH user for GoAccess 1.12 on Ubuntu 24.04:

ssh -i <your-key.pem> ubuntu@<public-ip>
3

Check the services

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

systemctl is-active sac-goaccess-egress goaccess nginx goaccess-refresh.timer
sudo test -f /var/lib/sac/data/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
4

Get your URL and password

sudo cat /root/goaccess-credentials.txt

Only root can read the file. It holds the URL, the user name, the password and the certificate's fingerprint:

# GoAccess 1.12 (sac) 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/sac.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://secureanycloud.com/stacks/goaccess-1-12/

GOACCESS_ADDRESS_SOURCE says where the address came from. It is either the public IPv4 address from instance metadata or, when the instance has no public address, the private address.

The image includes a self test. It runs through nginx, pinned to this instance's certificate, and checks the following:

  • The report and other paths refuse a request with no password and one with a wrong password.
  • The password opens the report.
  • The WebSocket refuses a request with no password, a wrong password, no token, a forged token or another site's origin.
  • The WebSocket works with the report's own token.

Run the self test:

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
5

Sign in

Open GOACCESS_URL in your browser. The certificate is self-signed for your instance, so the browser warns once. To check that you are talking to your instance, open the certificate details and compare its SHA-256 fingerprint with GOACCESS_TLS_SHA256. Then continue.

The browser 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. At first, these are your own visits.

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 the overview, 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:

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

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

HTTP status codes groups the responses by class. Expand a row to see the individual codes:

The other panels are visit time (hits per hour), virtual hosts, referring sites and URLs, static files, remote user and cache status. When you add a GeoIP database (Step 8), you also get 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.

7

Add your logs

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

sudo cat /etc/goaccess/sac.conf
# sac 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. A log file must therefore 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, get them onto the instance:

  • Put the logs under /var/log, for example in /var/log/web/, owned by root and readable by adm.
  • Logs that keep arriving (from 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

Edit the options file (sudo nano /etc/goaccess/sac.conf) as follows:

  • Add a LOG_FILE=/var/log/web/access.log line. Use 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 is CLOUDFRONT and Amazon S3 is AWSS3.
  • Remove the LOG_FILE=/var/log/nginx/access.log line if the formats differ.

Restart GoAccess. It checks the file first, and it 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.

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 database needs a free account and has its own licence terms. Copy it to the instance, then run:

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/sac.conf and run sudo systemctl restart goaccess.

The databases change monthly. To update one, replace the file and restart GoAccess. GoAccess cannot download the database itself, because it has no outbound access (Step 12).

9

Change the password

To generate a new random password, run the command below. It writes the password to the credentials file as GOACCESS_PASSWORD:

sudo goaccess-set-password

To choose your own password, use 16 characters or more. The command reads it from standard input and does not store it on the instance:

sudo goaccess-set-password --stdin

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

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 run:

sudo goaccess-set-domain <your-domain>

This command sets the name nginx accepts and generates a new self-signed certificate for it. It points the report's WebSocket at wss://<your-domain>:443/ws and restarts GoAccess and nginx. It also updates GOACCESS_URL, GOACCESS_HOST and GOACCESS_TLS_SHA256 in the credentials file.

To serve more names or addresses, add them after the first one (sudo goaccess-set-domain <your-domain> <public-ip>). Open the report at the first name.

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

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, run:

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

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

12

Outbound connections

GoAccess, nginx and the first-boot service are refused any connection to the instance metadata service. An iptables backstop, loaded on every boot, also refuses the goaccess and www-data accounts every connection except to localhost. This includes your VPC, private networks and the internet.

GoAccess needs no outbound connection, because it reads only the log files on the instance. Only root, goaccess and nginx (www-data) may connect to GoAccess's port on localhost.

13

Security checks

These checks show that the password guards the report and its WebSocket, and that anything other than reading is refused. They also show that a host name the instance does not serve gets no answer, that plain HTTP redirects, that other local accounts cannot reach GoAccess directly, and that 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://secureanycloud.com/ || 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. 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 SAC-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 SAC-GOACCESS-EGRESS -A SAC-GOACCESS-EGRESS -d 127.0.0.0/8 -o lo -j RETURN -A SAC-GOACCESS-EGRESS -p tcp -j REJECT --reject-with tcp-reset -A SAC-GOACCESS-EGRESS -j REJECT --reject-with icmp-admin-prohibited

GoAccess runs as goaccess on localhost, with no Linux capabilities, and it cannot gain any. It 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 (sac) - <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 only by root and the one service that needs each:

sudo stat -c '%a %U:%G %n' /root/goaccess-credentials.txt /etc/nginx/sac/goaccess.htpasswd /etc/ssl/goaccess/goaccess.key /etc/goaccess/ws-auth.secret /etc/goaccess/sac.conf
600 root:root /root/goaccess-credentials.txt 640 root:www-data /etc/nginx/sac/goaccess.htpasswd 600 root:root /etc/ssl/goaccess/goaccess.key 640 root:goaccess /etc/goaccess/ws-auth.secret 644 root:root /etc/goaccess/sac.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.
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, run:

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

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

15

Keep it up to date

To apply Ubuntu updates now, run:

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 a new version, launch it, copy your options file and logs to it, and restore the database from your backup. Stop GoAccess before you restore. Read the GoAccess release notes before you move.

Support and licensing

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

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

The source code is at github.com/allinurl/goaccess. GoAccess is not endorsed by or affiliated with SecureAnyCloud. There is no per-user licence fee.

The AMI price covers SecureAnyCloud's build, hardening, first-boot setup and support.

  • Email: sales@secureanycloud.com
  • Website: https://secureanycloud.com
  • Support: 24/7, with a 24 hour response SLA

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

Deploy on AWS

Launch GoAccess 1.12 on Ubuntu 24.04 on AWS with 24/7 support from SecureAnyCloud.

View on Marketplace

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on AWS