Analytics AWS

Liwan Analytics 1.8.2 on AWS User Guide

| Product: Liwan Analytics 1.8.2 on Ubuntu 24.04 on AWS

Overview

This guide covers launching and using the Liwan Analytics 1.8.2 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.

Liwan is a lightweight web analytics server. You add a small script to your web pages; Liwan counts page views, unique visitors, time on site and bounce rate, and breaks them down by page, referrer, country, browser, operating system and device. It sets no cookies on your visitors and stores no IP address: a visitor is counted with a hash that changes every day. One program serves the dashboard, its API, the tracking script and the event endpoint, and keeps its data in an embedded DuckDB database (events) and a SQLite database (users and projects).

The Liwan dashboard's Northwind Coffee (demo) project for the last 30 days: 1.8k total views, 420 unique visitors, an average time on site of 12:43, a line chart of daily views rising from mid October to early November, then tables of the top pages (/ 514, /menu 271, /locations 179) and referrers (Unknown 811, Google 479, DuckDuckGo 166)

The cloudimg image makes Liwan ready and safe to run on EC2:

  • An admin password unique to your instance. The admin account and its random 24 character password are created on your instance's first boot, into a root only file. No account, password, session, certificate or key is baked into the image, and Liwan's one-time setup link is never issued.
  • Sign-in on the dashboard and its API. Every dashboard page and every /api/dashboard call needs a Liwan session (an HttpOnly, Secure, SameSite=Strict cookie). Projects are private unless you make one public.
  • Rate limited per client. nginx limits sign-ins to 10 a minute and events to 20 a second per client address and answers 429 beyond that. The tracking script and the event endpoint are public by design, as your visitors' browsers must reach them.
  • Real client addresses only. nginx replaces any X-Forwarded-For, X-Real-IP or Forwarded header a client sends with the address it connected from, and Liwan trusts that header from localhost only, so nobody can choose the address Liwan counts or rate limits.
  • HTTPS from the first boot. nginx serves Liwan 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.
  • Liwan listens on localhost only (127.0.0.1:9042), runs as its own unprivileged liwan user with no Linux capabilities and can write only its data directory. Other local accounts (including ubuntu) cannot connect to it directly.
  • Other web sites cannot use your session. nginx refuses a sign-in or any change to the dashboard that comes from another site's page.
  • No outbound reach. Liwan and nginx are refused, at the kernel level and on every boot, any connection to the EC2 instance metadata service, link-local, private (RFC 1918), carrier grade NAT and reserved addresses, your VPC's own IPv4 and IPv6 ranges and the public internet. Liwan needs none of them to count visits.
  • Built from source and checked. Liwan 1.8.2 is built by cloudimg from the upstream liwan-v1.8.2 tag with no change to the source; every bundled component's licence is listed on the instance. No compiler or build tool is on the image.
  • Fully patched Ubuntu 24.04 LTS at build time.

The image ships with a fictional demo site, Northwind Coffee (demo) for demo.example, holding about 1,800 synthetic page views over 30 days, so you can explore the dashboard straight away. Step 6 shows how to track your own site.

What is on the image: Liwan 1.8.2 (/usr/local/bin/liwan), its configuration /etc/liwan/liwan.config.toml, nginx, the sqlite3 command line shell, the tools liwan-reset-password, liwan-set-domain and liwan-selftest, Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent.

Default user: ubuntu · Architecture: x86_64 · Recommended instance type: m5.large

Prerequisites

  • An AWS account subscribed to the listing, an EC2 key pair, and a VPC subnet with internet access.
  • A security group that allows TCP 22 (SSH) from your own network and TCP 443 (Liwan) from everywhere your visitors are (the tracking script and events come from their browsers), and TCP 80 (redirects to HTTPS) if you want plain http:// links to work.
  • To track a live web site: a domain name for the instance and a certificate your visitors' browsers trust (Step 8).

Step 1: Launch the instance

In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick Liwan Analytics 1.8.2 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 serves a few small sites 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=liwan}]'

Step 2: Connect to your instance

Version SSH user
Liwan Analytics 1.8.2 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, creates the data directory, the configuration, the certificate, the admin account and its password and the demo site, and writes the credentials file; the egress backstop loads; then Liwan and nginx start:

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

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

active
active
active
FIRSTBOOT_DONE

Only SSH and nginx listen on the network. Liwan listens on localhost only:

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

Step 4: Get your URL and password

sudo cat /root/liwan-credentials.txt

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

# Liwan 1.8.2 (cloudimg) on this instance. Open LIWAN_URL in a browser and sign in as LIWAN_USERNAME / LIWAN_PASSWORD.
# The password is unique to this instance. Change it in the dashboard (Settings > Account), or: sudo liwan-reset-password
# The certificate is self-signed for this instance: compare its SHA-256 fingerprint with LIWAN_TLS_SHA256.
# The "Northwind Coffee (demo)" project is fictional sample data for demo.example; delete it in Settings when you like.
LIWAN_URL=https://<public-ip>/
LIWAN_HOST=<public-ip>
LIWAN_USERNAME=admin
LIWAN_PASSWORD=<LIWAN_PASSWORD>
LIWAN_TLS_SHA256=<64 hex digits, colon separated>
LIWAN_ADDRESS_SOURCE=imdsv2-public-ipv4
GUIDE=https://www.cloudimg.co.uk/guides/liwan-analytics-1-8-2-on-ubuntu-24-04-aws/

LIWAN_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 dashboard API refuses every request without a session, that a wrong password is refused, that your password signs in and sees the demo site's page views, that signing out ends the session, and that the tracking script and the event endpoint work:

sudo liwan-selftest
liwan-selftest: 7 dashboard API paths + setup refused without a session (401), no project listed and the demo project + its stats not found (404); a wrong password refused; this instance's password signs in (HttpOnly Secure SameSite=Strict), sees <n> demo page views, and its session ends at sign-out; script.js served; a page view accepted
LIWAN_SELFTEST_OK

Each run records one page view on the demo site.

Step 5: Sign in and explore

Open LIWAN_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 LIWAN_TLS_SHA256, then continue. Sign in as admin with LIWAN_PASSWORD:

The Liwan sign-in form with the user name admin entered, an empty password field and the Login button

The home page shows each project you can see, with its views, unique visitors, average time on site, bounce rate and a chart for the chosen period (the date picker at the top right):

The Liwan home page signed in as admin: the Northwind Coffee (demo) project card for the last 30 days with 1.8k total views, 420 unique visitors, 12:43 average time on site, a 99.6% bounce rate and a chart of daily views

Click the project's name to open it. Below the chart, tables break the views down by page, referrer, country and city, platform and browser, and device type. Click any row to filter the whole project by it, and Show details for the full list. The demo data has no country, as the image ships no GeoIP database (Step 9).

Step 6: Track your website

Liwan groups what it tracks in two levels: an entity is one web site (one tracking ID), and a project is a dashboard showing one or more entities.

  1. Open Settings from the admin menu at the top right, then Entities, and create an entity, for example ID my-site and name www.example.com.
  2. Open Projects, create a project (for example My site) and add the entity to it. Leave it Private to need a sign-in to see it.
  3. Add the tracking script to every page of your site, inside <head>, with your instance's domain and your entity ID:
<script type="module" data-entity="my-site" src="https://<your-domain>/script.js"></script>

The script sends a page view to https://<your-domain>/api/event for each page. It sets no cookie and stores nothing in the visitor's browser.

Your visitors' browsers load the script and send events straight to the instance, so the instance needs a domain name and a certificate they trust; a browser will not load a script from a self signed certificate (Step 8). Open port 443 in the security group to the internet. Do not put the instance behind a load balancer or proxy: nginx records the address it is connected from, so every visit would appear to come from the load balancer.

The projects page lists your projects, their visibility and entities:

Liwan's Settings, Projects page listing the Northwind Coffee (demo) project, Private, with the entity demo.example (fictional), next to the General and Admin menus: My Account, Projects, Entities, Collection, Users, Teams, Authentication and API Keys

When you no longer need the demo site, delete the Northwind Coffee (demo) project and the demo.example (fictional) entity in Settings.

Other people can have their own accounts: Settings, Users creates a user, and Teams gives users access to chosen projects.

Step 7: Change the password

Change it in the dashboard under Settings, My Account. If the password is lost, generate a new random one (written to the credentials file as LIWAN_PASSWORD) over SSH:

sudo liwan-reset-password

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

sudo liwan-reset-password --stdin

A reset ends every session of that user. The password is stored by Liwan as an Argon2id hash.

Step 8: Use your own domain and certificate

The certificate and the host names nginx answers 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 liwan-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 liwan-set-domain <your-domain>

This sets the name nginx accepts and the address Liwan shows in its tracking snippets, generates a new self signed certificate for it and updates LIWAN_URL, LIWAN_HOST and LIWAN_TLS_SHA256 in the credentials file. Add more names or addresses after the first to serve them too (sudo liwan-set-domain <your-domain> <public-ip>).

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

Step 9: Outbound connections

Liwan, nginx and the first boot service are refused any connection to the instance metadata service, and the liwan and www-data accounts are refused every address except localhost: link-local, private, carrier grade NAT, reserved and multicast addresses, your VPC's own IPv4 and IPv6 ranges (read from instance metadata on every boot) and the public internet. Liwan needs no outbound connection to count visits; site icons are not fetched and the clock is kept by the instance.

Two optional Liwan features do need one: sign-in through your own OpenID Connect identity provider (Settings, Authentication), and the MaxMind GeoLite2 database for countries and cities (the [geoip] settings in /etc/liwan/liwan.config.toml, with your MaxMind account ID and licence key). To use them, list the networks they need in the allow list, which is empty by default:

cat /etc/cloudimg/liwan-egress-allow
# cloudimg Liwan egress allow-list (empty by default: Liwan and nginx reach nothing but loopback).
# Liwan needs no network for analytics. Its OPTIONAL outbound features need one:
#   * OIDC sign-in (configured in the dashboard: Settings > Authentication): your identity provider's addresses;
#   * MaxMind GeoIP downloads ([geoip] maxmind_account_id / maxmind_license_key): download.maxmind.com's addresses.
# List one CIDR per line. A PUBLIC range (or 0.0.0.0/0 for the whole internet, ::/0 for IPv6) is opened for the liwan
# account only, and never includes metadata, link-local, RFC 1918, CGNAT or this VPC's own ranges. A PRIVATE range (inside
# 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 100.64.0.0/10 or fc00::/7) is opened for liwan, e.g. an internal IdP.
# Metadata and link-local addresses can never be allowed. Then run:   sudo systemctl reload cloudimg-liwan-egress

A public range is opened only after your VPC's own ranges are refused, so it never reaches into your VPC. That needs the VPC's ranges from instance metadata for the same address family: a public IPv6 range is honoured only when your VPC has an IPv6 block, and stays closed otherwise (use an IPv4 range instead). nginx is never opened, and the instance metadata service can never be allowed.

Step 10: Security checks

The dashboard API needs a session; a wrong password and Liwan's one-time setup are refused; a sign-in from another site's page is refused; the tracking script is public; a host name the instance does not serve gets no answer; plain HTTP redirects; and other local accounts cannot reach Liwan directly:

H=$(sudo sed -n 's/^LIWAN_HOST=//p' /root/liwan-credentials.txt)
curl -sk -o /dev/null -w 'dashboard API without a session: %{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/api/dashboard/auth/me"
curl -sk -o /dev/null -w 'wrong password: %{http_code}\n' --resolve "$H:443:127.0.0.1" -H "Origin: https://$H" -H 'Content-Type: application/json' --data '{"username":"admin","password":"not-the-password"}' "https://$H/api/dashboard/auth/login"
curl -sk -o /dev/null -w 'one-time setup: %{http_code}\n' --resolve "$H:443:127.0.0.1" -H "Origin: https://$H" -H 'Content-Type: application/json' --data '{"token":"x","username":"x","password":"xxxxxxxxxxxx"}' "https://$H/api/dashboard/auth/setup"
curl -sk -o /dev/null -w 'sign-in from another site: %{http_code}\n' --resolve "$H:443:127.0.0.1" -H 'Origin: https://evil.example' -H 'Content-Type: application/json' --data '{"username":"admin","password":"x"}' "https://$H/api/dashboard/auth/login"
curl -sk -o /dev/null -w 'tracking script: %{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/script.js"
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 'Liwan directly, as ubuntu: %{http_code}\n' http://127.0.0.1:9042/ || true
dashboard API without a session: 401
wrong password: 401
one-time setup: 401
sign-in from another site: 403
tracking script: 200
unknown host name: 000
http: 301
Liwan directly, as ubuntu: 000

Liwan and nginx are blocked from the instance metadata service and reserved addresses:

for u in liwan nginx; do printf '%s ' "$u"; systemctl show -p IPAddressDeny --value "$u" | tr ' ' '\n' | sort | tr '\n' ' '; echo; done
sudo -u liwan curl -s -m 5 -o /dev/null -w 'metadata from the liwan account: %{http_code}\n' http://169.254.169.254/latest/meta-data/ || true
liwan 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
metadata from the liwan account: 000

The private ranges and your VPC are refused by the egress backstop, which reads the VPC's ranges on every boot:

sudo iptables -S CLOUDIMG-LIWAN-EGRESS | sed -n '1,4p'
cat /run/cloudimg/liwan-vpc-cidrs
-N CLOUDIMG-LIWAN-EGRESS
-A CLOUDIMG-LIWAN-EGRESS -d 127.0.0.0/8 -o lo -j RETURN
-A CLOUDIMG-LIWAN-EGRESS -d 169.254.0.0/16 -p tcp -j REJECT --reject-with tcp-reset
-A CLOUDIMG-LIWAN-EGRESS -d 169.254.0.0/16 -j REJECT --reject-with icmp-admin-prohibited
<your VPC CIDR, for example 172.31.0.0/16>

When your VPC has IPv6 blocks, they are in /run/cloudimg/liwan-vpc6-cidrs and refused the same way in ip6tables.

Liwan runs as liwan, on localhost, with no Linux capabilities and cannot gain any:

P=$(systemctl show -p MainPID --value liwan)
ps -o user:6=,args= -p "$P"
sudo grep -E '^(CapEff|CapBnd|CapAmb|NoNewPrivs)' /proc/"$P"/status
liwan  /usr/local/bin/liwan --config /etc/liwan/liwan.config.toml
CapEff: 0000000000000000
CapBnd: 0000000000000000
CapAmb: 0000000000000000
NoNewPrivs: 1

The credentials and the TLS key are readable by root only, and Liwan's data by the liwan user only:

sudo stat -c '%a %U:%G %n' /root/liwan-credentials.txt /etc/ssl/liwan/liwan.key /etc/liwan/liwan.config.toml /var/lib/liwan/data
600 root:root /root/liwan-credentials.txt
600 root:root /etc/ssl/liwan/liwan.key
640 root:liwan /etc/liwan/liwan.config.toml
700 liwan:liwan /var/lib/liwan/data

Other notes:

  • Rate limits. Sign-ins: 10 a minute per client address (bursts of 10). Events: 20 a second (bursts of 40). Everything else: 20 a second (bursts of 200), which covers normal use of the dashboard.
  • Logs. nginx's access log (/var/log/nginx/access.log) holds the method, the path without any query string, the status and the size; its error log records only critical errors. Liwan logs to the journal (journalctl -u liwan) without request details.
  • Response headers. nginx sends HSTS, a content security policy, X-Content-Type-Options: nosniff and X-Frame-Options: DENY, and no version number.
  • 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 port 22 open only to your own networks.

Step 11: Back up

Liwan keeps everything in /var/lib/liwan/data: liwan-app.sqlite (users, projects, entities, settings) and liwan-events.duckdb (events). Stop Liwan for a consistent copy, then copy the directory to your computer:

sudo systemctl stop liwan
sudo tar -C /var/lib/liwan -czf /home/ubuntu/liwan-data.tar.gz data
sudo chown ubuntu: /home/ubuntu/liwan-data.tar.gz && sudo chmod 600 /home/ubuntu/liwan-data.tar.gz
sudo systemctl start liwan

Then, on your computer:

scp -i <your-key.pem> ubuntu@<public-ip>:liwan-data.tar.gz .

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

Step 12: Keep it up to date

To apply Ubuntu updates now:

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

New Liwan releases arrive as new versions of this listing, built and checked the same way. To move to one, launch the new version, stop Liwan on both instances and copy /var/lib/liwan/data across (owned by liwan, mode 700). Read the Liwan changelog before you move.

Support and licensing

Liwan is open source under the Apache License 2.0. The image ships a cloudimg build of the liwan-v1.8.2 tag with no change to the source. Its licence and notice, the build record and the licence of every bundled Rust crate and JavaScript package (all permissive: MIT, Apache-2.0, BSD, ISC and similar; DuckDB is MIT, SQLite is public domain and the dashboard's Google Sans font is under the SIL Open Font License) are in /usr/share/doc/liwan:

ls /usr/share/doc/liwan
BUILD-INFO
Cargo.lock
LICENSE.md
NOTICE.md
THIRD-PARTY-CRATES.txt
THIRD-PARTY-NPM.txt

The source code is at github.com/explodingcamera/liwan. Liwan 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.