Medama 0.6.2
Overview
This guide covers launching and using the Medama 0.6.2 on Ubuntu 24.04 AMI from SecureAnyCloud on Amazon EC2. It is one version of the SecureAnyCloud 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 and stores no IP address or personal data, so it needs no cookie banner.
Medama is a single program with its dashboard built in. It keeps its account in SQLite and its statistics in DuckDB on the instance.
Medama ships with a built-in account, admin with the password CHANGE_ME_ON_FIRST_LOGIN, and listens on every network interface. The SecureAnyCloud image makes it safe to run on EC2.
What you get:
- A password unique to your instance, from the first boot. First boot starts Medama on localhost only and replaces the built-in password with a random one through Medama's own API (stored as an argon2id hash). It 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 the password 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
medamaaccount with no Linux capabilities. nginx reaches it through a socket on127.0.0.1:8080that onlyrootand 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 except the tracker script and the two event endpoints your websites call (
/api/event/hitand/api/event/ping) refuses a request without a signed-in session (401). The session cookie isHttpOnlyandSecure. - Rate limited per client. nginx limits sign-in attempts (10 a minute), the API, the dashboard and tracking events per client address, and answers
429beyond 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. It replaces any
X-Forwarded-For,X-Real-IP,ForwardedorCF-Connecting-IPa client sends, so a visitor cannot pick the address that Medama's blocking rules and rate limits see. - Other websites cannot use your session. nginx refuses sign-in and changes that come from another site's page. It also 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. On every boot, the kernel refuses the
medamaandwww-dataaccounts every connection except to localhost. That includes 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 and checked against its SHA-256. Its Go and JavaScript dependencies are raised to fixed versions inside the ranges Medama itself allows. The build fails unless
govulncheckfinds 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, because their browsers load the tracker and send its events. Also allow TCP 80 if you want plain
http://links to redirect. Medama's dashboard is behind its password, and the rate limits apply to everyone. - Direct access from visitors to the instance. Behind a load balancer, every visit would arrive from the balancer's private address. Medama's Block abusive IPs setting (on by default) drops that address, and nginx does not trust a balancer's forwarded address headers.
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 select your Region. Then choose 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 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}]'Connect to your instance
For Medama 0.6.2 on Ubuntu 24.04, the SSH user is ubuntu.
ssh -i <your-key.pem> ubuntu@<public-ip>Check the services
First boot does the following in order:
- Reads the instance's public address from EC2 instance metadata.
- Loads the egress backstop and creates Medama's data directory.
- Starts Medama on localhost only and replaces the built-in password with this instance's own.
- Generates the certificate and writes the credentials file.
- Restarts Medama so that no session from the setup survives.
Only then do nginx and Medama's local socket start:
systemctl is-active sac-medama-egress medama-proxy.socket medama nginx
sudo test -f /var/lib/sac/data/medama-firstboot.done && echo FIRSTBOOT_DONEAll four units are active and the first boot marker is present:
active active active active FIRSTBOOT_DONEOnly 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 -u0.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 nginxGet your URL and password
sudo cat /root/medama-credentials.txtOnly root can read the file. It holds the URL, the user name, the password and the certificate's fingerprint:
# Medama 0.6.2 (sac) 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://secureanycloud.com/stacks/medama-0-6-2/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. It runs through nginx, pinned to this instance's certificate, and checks that:
- The health check and the tracker are public.
- The API refuses a request with no session, a wrong password and the built-in default password.
- This instance's password signs in with an
HttpOnly,Securecookie and reads the account. - The content security policy is sent.
- Signing out expires the cookie.
Run it:
sudo medama-selftestmedama-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_OKSign in
Open MEDAMA_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 MEDAMA_TLS_SHA256. Then continue. Sign in as admin with MEDAMA_PASSWORD:
A new instance has no websites yet. My Websites is empty until you add one (Step 6).
Add a website and its tracker
- 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.comandwww.example.comare different websites. - 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.
Read the dashboard
The pictures in this guide show a fictional bakery's website, bakery.sac-demo.example. A few dozen visits were 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 is 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 these panels:
- Pages, with the time spent on each
- Referrers, with sources, mediums and campaigns from UTM tags
- Browsers, with operating systems and devices
- Countries, with languages
- Properties, for custom events
Load More shows the full list:
Medama works out the country from the visitor's time zone. It 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, in a directory that only the medama account can open. The account and websites are in SQLite (/var/lib/medama/data/me_app.db), and the statistics are in DuckDB (/var/lib/medama/data/me_analytics.db).
Change the password
To generate a new random password, which is written to the credentials file as MEDAMA_PASSWORD, run:
sudo medama-set-passwordTo choose your own password, run the command below. The password is 16 to 128 characters, read from standard input and not stored on the instance:
sudo medama-set-password --stdinmedama-set-password: the admin password was changed and every signed-in session endedThe tool changes the password through Medama's own API on localhost (Medama stores it as an argon2id hash). It checks that the new password signs in and the old one is refused, 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. Give the current password on the first line of standard input. With --stdin too, give the new password on the second line.
Settings, Account in the dashboard can also change the user name and password. It neither updates the credentials file nor ends the sessions already signed in, so use medama-set-password instead.
Signing out. In Medama 0.6.2, Log out expires the session cookie in your browser. 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 medamaUse 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 run:
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 that browsers trust. nginx serves /.well-known/acme-challenge/ on port 80 from /var/www/medama-acme. With port 80 open, you can get a certificate 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.pemmedama-set-certificate checks that the certificate is valid now, is not a CA certificate and matches the key. It then installs the certificate, 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.
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:
nginx gives Medama the address each request came from. It replaces any X-Forwarded-For, X-Real-IP, Forwarded or CF-Connecting-IP header a client sends, so a visitor cannot choose the address that the filters and the rate limits see.
The FireHOL list includes the private address ranges, so visits that arrive 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 and does not store it.
Rate limits
nginx limits requests per client address and answers 429 beyond the limit:
- Sign in (/api/auth/login): 10 a minute, burst 10
- Tracker events (/api/event/hit, /api/event/ping): 10 a second, burst 60
- Dashboard API (/api/): 20 a second, burst 100
- Dashboard pages and the tracker script: 30 a second, burst 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/sac-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/sac/medama/nginx-site.tmpl, so make lasting changes there too.
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. That includes the instance metadata service, your VPC and the internet. Only root and nginx may connect to Medama's socket on 127.0.0.1:8080. Medama needs no outbound connection, because its spam lists ship with the program.
Security checks
These checks confirm the following:
- 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.
- The
medamaaccount 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://secureanycloud.com/ || trueAPI 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: 000Medama 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 SAC-MEDAMA-EGRESS;
sudo iptables -S SAC-MEDAMA-LOOPBACKmedama 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-MEDAMA-EGRESS -A SAC-MEDAMA-EGRESS -d 127.0.0.0/8 -o lo -j RETURN -A SAC-MEDAMA-EGRESS -p tcp -j REJECT --reject-with tcp-reset -A SAC-MEDAMA-EGRESS -j REJECT --reject-with icmp-admin-prohibited -N SAC-MEDAMA-LOOPBACK -A SAC-MEDAMA-LOOPBACK -m owner --uid-owner 0 -j RETURN -A SAC-MEDAMA-LOOPBACK -m owner --uid-owner 33 -j RETURN -A SAC-MEDAMA-LOOPBACK -p tcp -j REJECT --reject-with tcp-resetMedama runs as medama with no Linux capabilities and cannot gain any. 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 addrmedama /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/128Only root can read the credentials and the TLS key. Only the medama account can read Medama's data directory and databases:
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.db600 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.dbOther notes:
- Service hardening.
medama.serviceruns withNoNewPrivileges,ProtectSystem=strict(only its data directory is writable),PrivateTmp,PrivateNetworkand 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. This check means 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 as401, refused cross-site requests as403and rate-limited requests as429. nginx's error log is kept atcrit. Medama logs warnings to the journal (journalctl -u medama), and first boot logs tojournalctl -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, aPermissions-Policythat turns off the camera, microphone, location, payment and USB, andCache-Control: no-storefor 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 answer404. The dashboard answers onlyGETandHEAD. - 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.
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 the files to your computer, run:
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.gzTo restore onto an instance of this version, stop Medama and 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.
Keep it up to date
To apply Ubuntu updates now:
sudo apt-get update && sudo apt-get upgrade -yNew 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 is 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/medamaLICENSE LICENSE.tracker README.sac dashboard-third-party go-modulesThe source code is at github.com/medama-io/medama. Medama 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 Medama 0.6.2 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