Analytics AWS

Medama 0.6.2 on AWS User Guide

| Product: Medama 0.6.2 on Ubuntu 24.04 on AWS

Overview

This guide covers launching and using the Medama 0.6.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 launch.

Medama is open source, privacy-first website analytics. Your websites load a small tracker script (under 1 KB) from the instance; Medama counts page views, visitors, time on page, referrers, campaigns (UTM), browsers, operating systems, devices, countries (from the visitor's time zone) and languages, and shows them in its dashboard. It sets no cookies, stores no IP address and no personal data, so it needs no cookie banner. Medama is a single program with its dashboard built in, keeping its account in SQLite and its statistics in DuckDB on the instance.

The Medama dashboard for a fictional bakery's website: 31 visitors, 60 page views, 1m59s time spent and a 0% bounce rate today, above the visitors chart by hour

Medama ships with a built-in account, admin with the password CHANGE_ME_ON_FIRST_LOGIN, and listens on every network interface. The cloudimg image makes it safe to run on EC2:

  • A password unique to your instance, from the first boot. First boot starts Medama on localhost only, replaces the built-in password with a random one through Medama's own API (stored as an argon2id hash), proves the built-in password is refused, and only then lets nginx serve anything. The password is in a root only file; no password, hash, session key, certificate or key is baked into the image. Change it at any time with medama-set-password.
  • Medama has no network of its own. It runs in a private network namespace with only its own loopback interface, as its own medama account with no Linux capabilities. nginx reaches it through a socket on 127.0.0.1:8080 that only root and nginx may connect to.
  • HTTPS from the first boot. nginx serves the dashboard, the API, the tracker script and the event endpoints 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.
  • The dashboard and API need a session. Everything but the tracker script and the two event endpoints your websites call (/api/event/hit and /api/event/ping) refuses a request without a signed-in session (401). The session cookie is HttpOnly and Secure.
  • Rate limited per client. nginx limits sign-in attempts (10 a minute), the API, the dashboard and tracking events per client address, and answers 429 beyond that. Normal use of the dashboard, and busy websites, stay well inside the limits.
  • The visitor address is the real one. nginx passes Medama the address each request came from and replaces any X-Forwarded-For, X-Real-IP, Forwarded or CF-Connecting-IP a client sends, so a visitor cannot pick the address Medama's blocking rules and rate limits see.
  • Other web sites cannot use your session. nginx refuses sign-in and changes coming from another site's page, and sends a content security policy that allows the dashboard's own scripts only (each inline script by its hash).
  • No outbound connections. Medama and nginx need no network access beyond the instance: the medama 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.
  • Logged, without secrets. nginx records requests by path only, never with a query string; its error log is kept at crit, so passwords and session cookies are not written to disk.
  • Built from the pinned upstream release. Medama 0.6.2 is compiled on the build machine from the upstream release source, checked against its SHA-256, with its Go and JavaScript dependencies raised to fixed versions inside the ranges Medama itself allows. The build fails unless govulncheck finds no reachable vulnerability. No source file is changed.
  • Fully patched Ubuntu 24.04 LTS at build time.

What is on the image: Medama 0.6.2 (/usr/local/bin/medama), nginx, the tools medama-set-password, medama-set-domain, medama-set-certificate and medama-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 from everywhere your websites' visitors are (the tracker and its events are loaded by their browsers), plus TCP 80 if you want plain http:// links to redirect. Medama's dashboard is behind its password; the rate limits apply to everyone.
  • Visitors should reach the instance directly. Behind a load balancer every visit would arrive from the balancer's private address, which Medama's Block abusive IPs setting (on by default) drops, and nginx does not trust a balancer's forwarded address headers.

Step 1: Launch the instance

In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick Medama 0.6.2 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large (a t3.small handles a few small websites), your key pair, a subnet and a security group as above. Give the instance an Elastic IP: your websites will load the tracker from its address or domain, which must not change.

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

Step 2: Connect to your instance

Version SSH user
Medama 0.6.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, loads the egress backstop, creates Medama's data directory, starts Medama on localhost only and replaces the built-in password with this instance's own, generates the certificate, writes the credentials file and restarts Medama so that no session from the setup survives. Only then do nginx and Medama's local socket start:

systemctl is-active cloudimg-medama-egress medama-proxy.socket medama nginx
sudo test -f /var/lib/cloudimg/medama-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. Medama's socket listens on localhost only (systemd holds it and passes connections into Medama's own network namespace):

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:8080 systemd-socket-
[::]:22 sshd
[::]:443 nginx
[::]:80 nginx

Step 4: Get your URL and password

sudo cat /root/medama-credentials.txt

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

# Medama 0.6.2 (cloudimg) on this instance. Open MEDAMA_URL in a browser and sign in as MEDAMA_USERNAME / MEDAMA_PASSWORD.
# The password is unique to this instance. Change it: sudo medama-set-password (or --stdin to choose one).
# The certificate is self-signed for this instance: compare its SHA-256 fingerprint with MEDAMA_TLS_SHA256.
# Add a website in the dashboard, then put the tracker on its pages: <script defer src="https://<public-ip>/script.js"></script>
MEDAMA_URL=https://<public-ip>/
MEDAMA_HOST=<public-ip>
MEDAMA_USERNAME=admin
MEDAMA_PASSWORD=<MEDAMA_PASSWORD>
MEDAMA_TLS_SHA256=<64 hex digits, colon separated>
MEDAMA_ADDRESS_SOURCE=imdsv2-public-ipv4
GUIDE=https://www.cloudimg.co.uk/guides/medama-0-6-2-on-ubuntu-24-04-aws/

MEDAMA_ADDRESS_SOURCE says where the address came from: the public IPv4 address from instance metadata, or private-fallback when the instance has no public one. The login message (MOTD) repeats the URL and the tracker tag each time you connect.

The image includes a self test. Through nginx and pinned to this instance's certificate it checks that the health check and the tracker are public, that the API refuses a request with no session, a wrong password and the built-in default password, that this instance's password signs in with an HttpOnly, Secure cookie and reads the account, that the content security policy is sent, and that signing out expires the cookie:

sudo medama-selftest
medama-selftest: /healthz + tracker public, openapi hidden; anonymous API -> 401; wrong + default password -> 401; this instance's password signs in (HttpOnly Secure cookie), reads the account, CSP present, sign-out expires the cookie
MEDAMA_SELFTEST_OK

Step 5: Sign in

Open MEDAMA_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 MEDAMA_TLS_SHA256, then continue. Sign in as admin with MEDAMA_PASSWORD:

The Medama sign-in page: Log in to your dashboard, with Username and Password fields and a Log In button

A new instance has no websites yet; My Websites is empty until you add one (Step 6).

Step 6: Add a website and its tracker

  1. On My Websites choose Add Website and enter your website's host name exactly as visitors see it, for example www.example.com. Medama records visits only for the host names you add, so add each one you use (example.com and www.example.com are different websites).
  2. Add the tracker to every page of the website, inside <head>. The tag is in the credentials file, in the login message and under Settings, Tracker:
<script defer src="https://<public-ip>/script.js"></script>

Use the address or domain in MEDAMA_HOST. Visitors' browsers load the script and send their page views to /api/event/hit and /api/event/ping on your instance, so port 443 must be open to them. With the self signed certificate, browsers refuse the script on your website's pages: before you add the tracker to a live website, give the instance a domain and a certificate from a public certificate authority (Step 9).

Settings, Tracker also switches on the tracker's optional Click Events and Page View Events; the tag is then a different script, shown on that page.

Medama's My Websites page listing bakery.cloudimg-demo.example with 31 visitors in the last 24 hours, and the Add Website button

Step 7: Read the dashboard

The pictures in this guide show a fictional bakery's website, bakery.cloudimg-demo.example, with a few dozen visits sent through the tracker's own endpoints, so that every panel has something in it. Your dashboard fills as your visitors arrive.

Choose a website on My Websites to open its dashboard. The top row gives the visitors, page views, time spent and bounce rate for the period chosen at the top right (today, the last 24 hours, the last 7 or 30 days and more), each compared with the period before; choose one to chart it below (the picture at the start of this guide). Add filter narrows every panel to, for example, one page, referrer, country or browser.

Below the chart are the panels: Pages (and the time spent on each), Referrers (with sources, mediums and campaigns from UTM tags), Browsers (with operating systems and devices), Countries (with languages) and Properties for custom events. Load More shows the full list:

Medama's panels for the fictional bakery: pages led by /about, referrers led by Direct/None, Reddit and Bing, browsers Safari, Chrome and Firefox, and countries led by the United States, Australia, France and Germany

Medama works out the country from the visitor's time zone and counts unique visitors without cookies or stored IP addresses, so a visitor counted on one day is counted again on another. Its data is on the instance: the account and websites in SQLite (/var/lib/medama/data/me_app.db) and the statistics in DuckDB (/var/lib/medama/data/me_analytics.db), in a directory only the medama account can open.

Step 8: Change the password

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

sudo medama-set-password

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

sudo medama-set-password --stdin
medama-set-password: the admin password was changed and every signed-in session ended

The tool changes the password through Medama's own API on localhost (Medama stores it as an argon2id hash), checks that the new password signs in and the old one is refused, and then restarts Medama, which signs everyone out. If the credentials file no longer holds the current password (after --stdin, or a change made in the dashboard), add --current-stdin and give the current password on the first line of standard input (with --stdin too, the new one on the second line).

Settings, Account in the dashboard can also change the user name and password, but it neither updates the credentials file nor ends the sessions already signed in. Use medama-set-password.

Medama's Settings page, Account tab: Account Details with the Username admin and a New password field

Signing out. In Medama 0.6.2, Log out expires the session cookie in your browser, but Medama keeps the session itself in memory, valid for up to 30 days, until Medama restarts. A copy of the cookie taken before you signed out would still work until then. To end every session at once, change the password as above or restart Medama:

sudo systemctl restart medama

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

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 medama-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 medama-set-domain <your-domain>

This sets the names nginx accepts, generates a new self signed certificate for them, reloads nginx and updates MEDAMA_URL, MEDAMA_HOST and MEDAMA_TLS_SHA256 in the credentials file. Add more names or addresses after the first to serve them too (sudo medama-set-domain <your-domain> <public-ip>). Then use https://<your-domain>/script.js in the tracker tag.

Websites load the tracker from your instance, so for live websites use a certificate browsers trust. nginx serves /.well-known/acme-challenge/ on port 80 from /var/www/medama-acme, so with port 80 open you can get one from Let's Encrypt (sudo apt-get install -y certbot, then sudo certbot certonly --webroot -w /var/www/medama-acme -d <your-domain>) and install it:

sudo medama-set-certificate /etc/letsencrypt/live/<your-domain>/fullchain.pem /etc/letsencrypt/live/<your-domain>/privkey.pem

medama-set-certificate checks that the certificate is valid now, is not a CA certificate and matches the key, installs it, and keeps the previous pair if nginx rejects the new one; --check runs the checks only. Run it again after each renewal (for example from a certbot deploy hook).

Step 10: Spam filters and your visitors' addresses

Settings, Spam has Medama's own filters. Abusive IPs (the FireHOL level 1 list) and Tor Exit Nodes are on by default; you can also block single addresses:

Medama's Settings page, Spam tab: Preset Filters with Abusive IPs and Tor Exit Nodes both ticked, and Filter IP addresses with an Add IP address field

nginx gives Medama the address each request came from, and replaces any X-Forwarded-For, X-Real-IP, Forwarded or CF-Connecting-IP header a client sends, so a visitor cannot choose the address the filters and the rate limits see. The FireHOL list includes the private address ranges, so visits arriving through a load balancer or proxy on a private address are dropped: let visitors reach the instance directly (see Prerequisites). Medama uses the address only to filter; it does not store it.

Step 11: Rate limits

nginx limits requests per client address and answers 429 beyond the limit:

Requests Rate Burst
Sign in (/api/auth/login) 10 a minute 10
Tracker events (/api/event/hit, /api/event/ping) 10 a second 60
Dashboard API (/api/) 20 a second 100
Dashboard pages and the tracker script 30 a second 200

Each visitor's browser sends a few events per page it opens, so even a very busy website stays well inside the event limit, which applies to each visitor separately. The limits are in /etc/nginx/conf.d/cloudimg-medama.conf and /etc/nginx/sites-available/medama; after a change run sudo nginx -t && sudo systemctl reload nginx. medama-set-domain re-renders the site file from /usr/local/lib/cloudimg/medama/nginx-site.tmpl, so make lasting changes there too.

Step 12: Outbound connections

Medama runs in a private network namespace whose only interface is its own loopback, so it cannot open a connection to anything. In addition, an iptables backstop loaded on every boot refuses the medama and www-data (nginx) accounts every new connection except to localhost, including the instance metadata service, your VPC and the internet, and only root and nginx may connect to Medama's socket on 127.0.0.1:8080. Medama needs no outbound connection: its spam lists ship with the program.

Step 13: Security checks

The API needs a session; wrong and built-in passwords are refused; a sign-in from another site's page is refused; the tracker is public; a host name the instance does not serve gets no answer; plain HTTP redirects; other local accounts cannot reach Medama directly; and the medama account cannot reach the metadata service or the internet:

H=$(sudo sed -n 's/^MEDAMA_HOST=//p' /root/medama-credentials.txt)
curl -sk -o /dev/null -w 'API without a session: %{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/api/user"
curl -sk -o /dev/null -w 'wrong password: %{http_code}\n' --resolve "$H:443:127.0.0.1" -H 'Content-Type: application/json' -H "Origin: https://$H" --data '{"username":"admin","password":"not-the-password"}' "https://$H/api/auth/login"
curl -sk -o /dev/null -w 'built-in default password: %{http_code}\n' --resolve "$H:443:127.0.0.1" -H 'Content-Type: application/json' -H "Origin: https://$H" --data '{"username":"admin","password":"CHANGE_ME_ON_FIRST_LOGIN"}' "https://$H/api/auth/login"
curl -sk -o /dev/null -w 'sign-in from another site: %{http_code}\n' --resolve "$H:443:127.0.0.1" -H 'Content-Type: application/json' -H 'Origin: https://evil.example' --data '{"username":"admin","password":"x"}' "https://$H/api/auth/login"
curl -sk -o /dev/null -w 'tracker 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 'Medama directly, as ubuntu: %{http_code}\n' -m 5 http://127.0.0.1:8080/ || true
sudo -u medama curl -s -m 5 -o /dev/null -w 'metadata from the medama account: %{http_code}\n' http://169.254.169.254/latest/meta-data/ || true
sudo -u medama curl -s -m 5 -o /dev/null -w 'internet from the medama account: %{http_code}\n' https://www.cloudimg.co.uk/ || true
API without a session: 401
wrong password: 401
built-in default password: 401
sign-in from another site: 403
tracker script: 200
unknown host name: 000
http: 301
Medama directly, as ubuntu: 000
metadata from the medama account: 000
internet from the medama account: 000

Medama and nginx are blocked from the instance metadata service and reserved addresses, the backstop refuses everything else that is not localhost, and the loopback guard admits only root and nginx to Medama's socket:

for u in medama nginx; do printf '%s ' "$u"; systemctl show -p IPAddressDeny --value "$u" | tr ' ' '\n' | sort | tr '\n' ' '; echo; done
sudo iptables -S CLOUDIMG-MEDAMA-EGRESS; sudo iptables -S CLOUDIMG-MEDAMA-LOOPBACK
medama 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-MEDAMA-EGRESS
-A CLOUDIMG-MEDAMA-EGRESS -d 127.0.0.0/8 -o lo -j RETURN
-A CLOUDIMG-MEDAMA-EGRESS -p tcp -j REJECT --reject-with tcp-reset
-A CLOUDIMG-MEDAMA-EGRESS -j REJECT --reject-with icmp-admin-prohibited
-N CLOUDIMG-MEDAMA-LOOPBACK
-A CLOUDIMG-MEDAMA-LOOPBACK -m owner --uid-owner 0 -j RETURN
-A CLOUDIMG-MEDAMA-LOOPBACK -m owner --uid-owner 33 -j RETURN
-A CLOUDIMG-MEDAMA-LOOPBACK -p tcp -j REJECT --reject-with tcp-reset

Medama runs as medama, with no Linux capabilities, cannot gain any, and its network namespace has only a loopback interface:

P=$(systemctl show -p MainPID --value medama)
ps -o user:9=,args= -p "$P"
sudo grep -E '^(CapEff|CapBnd|CapAmb|NoNewPrivs)' /proc/"$P"/status
sudo nsenter -t "$P" -n ip -brief addr
medama    /usr/local/bin/medama start -port 8081 -appdb /var/lib/medama/data/me_app.db -analyticsdb /var/lib/medama/data/me_analytics.db -logger json -level warn
CapEff: 0000000000000000
CapBnd: 0000000000000000
CapAmb: 0000000000000000
NoNewPrivs: 1
lo               UNKNOWN        127.0.0.1/8 ::1/128

The credentials and the TLS key are readable by root only; Medama's data directory and databases by the medama account only:

sudo stat -c '%a %U:%G %n' /root/medama-credentials.txt /etc/ssl/medama/medama.key /var/lib/medama/data /var/lib/medama/data/me_app.db /var/lib/medama/data/me_analytics.db
600 root:root /root/medama-credentials.txt
600 root:root /etc/ssl/medama/medama.key
700 medama:medama /var/lib/medama/data
600 medama:medama /var/lib/medama/data/me_app.db
600 medama:medama /var/lib/medama/data/me_analytics.db

Other notes:

  • Service hardening. medama.service runs with NoNewPrivileges, ProtectSystem=strict (only its data directory is writable), PrivateTmp, PrivateNetwork and an empty capability set, and restarts on a crash. Before it starts, a check refuses to run Medama unless first boot has finished, the egress backstop and the loopback guard are loaded, the program and data directory have their owners and modes, and the database holds an account (on an empty database Medama would re-create its built-in account), so Medama never comes up with its built-in password.
  • Logs. nginx's access log (/var/log/nginx/access.log) records each request by path only, with no query string and no cookie; failed sign-ins appear as 401, refused cross-site requests as 403 and rate limited requests as 429. nginx's error log is kept at crit. Medama logs warnings to the journal (journalctl -u medama); first boot logs to journalctl -u medama-firstboot.
  • Response headers. nginx sends HSTS, a content security policy, X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: same-origin, cross-origin isolation headers, a Permissions-Policy that turns off the camera, microphone, location, payment and USB, and Cache-Control: no-store for the dashboard and API, with no version number. The content security policy allows scripts from the instance only (and the dashboard's inline scripts by their hash); it allows inline styles because the dashboard's component library sets them. The tracker script and event endpoints are served to any website, as they must be.
  • Not served. Medama's API description (/openapi.yaml) and profiler paths answer 404; the dashboard answers only GET and HEAD.
  • Who can reach it. Keep port 22 open only to your own networks. Port 443 has to be open to your websites' visitors for the tracker; the dashboard is behind its password and the rate limits.

Step 14: Back up

Medama's account, websites and statistics are the two database files in /var/lib/medama/data. Stopping Medama for a consistent copy also signs everyone out. To copy them to your computer:

ssh -i <your-key.pem> ubuntu@<public-ip> 'sudo systemctl stop medama && sudo tar -C / -czf - var/lib/medama/data; sudo systemctl start medama' > medama-backup.tar.gz

To restore onto an instance of this version, stop Medama, replace the two files (owned by medama:medama, mode 600), start Medama, and sign in with the password of the instance the backup came from; then run sudo medama-set-password --current-stdin with that password to set a new one and update the credentials file. 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 Medama releases arrive as new versions of this listing, built and checked the same way. To move to one, launch the new version and restore your backup onto it (Step 14). Read the Medama release notes before you move.

Support and licensing

Medama's server and dashboard are open source under the Apache License 2.0 and its tracker script under the MIT licence. Medama is compiled from the upstream release unmodified. Its licence texts, the licences of the Go modules and dashboard packages compiled into it (MIT, BSD, Apache-2.0, ISC, MPL-2.0 and others, with DuckDB under MIT), and a record of how it was built are in /usr/share/doc/medama:

ls /usr/share/doc/medama
LICENSE
LICENSE.tracker
README.cloudimg
dashboard-third-party
go-modules

The source code is at github.com/medama-io/medama. Medama 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.