Kuvasz Uptime Monitoring 4.5.0 on AWS User Guide
Overview
This guide covers launching and verifying Kuvasz Uptime Monitoring 4.5.0 on Ubuntu 24.04 from cloudimg on Amazon EC2. It is one version of the cloudimg Hardened Uptime Kuma, Gatus & Uptime and Status Pages AMIs listing on AWS Marketplace; you pick it as the software version when you subscribe and launch. The other monitoring and status page products in the listing are described on the listing's guide hub.
Kuvasz is an open-source (AGPL-3.0) uptime and SSL monitoring service. It checks websites and APIs over HTTP(S), hosts over ICMP and TCP, DNS records, Docker containers and push heartbeats from your cron jobs, at intervals down to five seconds. It warns before SSL certificates expire, records every outage as an incident with a timeline and notes, suppresses alerts during maintenance windows, and notifies you through email, Slack, Microsoft Teams, Discord, Telegram, PagerDuty, Pushover, Apprise or webhooks. Status pages share the picture with your users. Everything is manageable from the web UI, the REST API or a YAML file.
Version 4.5.0 includes the two redirect security fixes of 4.3.0 and 4.4.0: a relative redirect is now resolved against the hop that returned it (GHSA-jq3w-22qh-gf8w), and a monitor's custom request headers are no longer sent to other origins in a redirect chain.
What is on the image:
- Kuvasz 4.5.0, the official upstream container image
kuvaszmonitoring/kuvasz:4.5.0, pinned by digestsha256:e361bd8eb6b0acec45feeacb50a4ea8e57b88701e509be97b2e8005f789200ae - PostgreSQL 16.15 (
postgres:16-alpine, pinned by digestsha256:721873c34ceb9f8d8fc265984940dc982404c105f19ad51be9fdc5970a6080ea) as its database, created empty on your instance's first boot - Docker CE (official Docker apt repository) running both containers under one systemd unit,
kuvasz.service - nginx as the only network-facing service: HTTPS on port 443 with a self-signed certificate created on first boot, port 80 redirecting to HTTPS
- A per-instance admin password and REST API key, generated on first boot into a root-only file
kuvasz-set-hostname, a helper that serves Kuvasz under your own DNS name and re-issues the certificate for it
Default user: ubuntu · Architecture: x86_64 · Root volume: 8 GiB gp3
Security posture
- No default login. The admin password (28 characters), the REST API key (40) and the PostgreSQL password (32) are generated on each instance's first boot. None is in the image, so no two instances share one. The published example values from the Kuvasz documentation never exist on the instance.
- Fails closed. Kuvasz will not start unless its secrets exist, are long enough, are not a published default and are readable only by the container user. nginx stays closed until first boot has proved that the admin password and the API key really authenticate.
- No secret in a container environment. Kuvasz reads its secrets from a mounted file (
/opt/kuvasz/secrets/kuvasz-secrets.yml, mode 0640, readable only by the container's user) and PostgreSQL fromPOSTGRES_PASSWORD_FILE, sodocker inspectand the process environment never show them. - What is public. The dashboard, monitors, settings and REST API need the admin login or the API key. Status pages you choose to publish are public by design, and so is each push monitor's heartbeat URL (it carries its own secret). The built-in default status page is private until you make it public. The MCP server is off.
- Only nginx faces the network. Kuvasz listens on
127.0.0.1:8080and PostgreSQL has no host port at all. nginx answers only for the configured hostname (any otherHostheader gets its connection closed), uses TLS 1.2 or 1.3, refuses cross-originPOSTrequests and WebSocket upgrades (403), strips CORS headers, rate-limits the sign-in form, forces the session cookie toSecure,HttpOnlyandSameSite=Lax, and replaces any client-suppliedX-Forwarded-For,X-Real-IPorForwardedheader with the real peer address. - The hostname comes from configuration, never from a request. First boot reads it from EC2 instance metadata (IMDSv2). nginx hands Kuvasz that fixed name, so absolute URLs Kuvasz builds (for example an OIDC redirect URI) cannot be steered by a request.
- Monitors cannot read instance metadata. HTTP monitors fetch addresses that you or your users enter, so both containers are blocked from the instance metadata service (169.254.169.254 and fd00:ec2::254) by a Docker
DOCKER-USERfirewall rule that is in place before any container starts. nginx, Docker and the Kuvasz units are blocked by systemdIPAddressDeny. This was proved at build time with real Kuvasz monitors pointed at the metadata service: they time out, while monitors of normal sites are UP. - Least-privilege containers. Kuvasz runs as an unprivileged user with every Linux capability dropped,
no-new-privilegesand a read-only root filesystem (only/tmpis writable). PostgreSQL runs as its own unprivileged user with every capability dropped. - IMDSv2 required. The AMI is registered with
ImdsSupport v2.0, so instances refuse token-less (IMDSv1) metadata requests. - Per-instance identity. SSH host keys, the machine ID, the TLS certificate and Docker's engine ID are generated fresh on every instance.
Prerequisites
- An AWS account and permission to launch EC2 instances
- An EC2 key pair in the Region you launch in
- Outbound internet access from the instance to the sites and services you want to monitor and notify
Recommended instance type: m5.large. Kuvasz (a JVM capped at a 512 MiB heap) and PostgreSQL fit comfortably in 8 GiB of memory and check hundreds of monitors on 2 vCPUs. The image also runs on 4 GiB instances. Increase the root volume at launch if you keep long event and latency history for many monitors.
Connecting to your instance
| Version | Operating system | SSH user |
|---|---|---|
| Kuvasz Uptime Monitoring 4.5.0 on Ubuntu 24.04 | Ubuntu 24.04 LTS | ubuntu |
Step 1: Launch the instance
Option A: AWS Marketplace console
- Open the cloudimg Hardened Uptime Kuma, Gatus & Uptime and Status Pages AMIs listing on AWS Marketplace and choose View purchase options, then Subscribe. One subscription covers every version in the listing.
- Once the subscription is active, choose Launch your software.
- Choose One-click launch from AWS Marketplace (or Launch from EC2 Console for the full launch wizard). In Version, choose Kuvasz Uptime Monitoring 4.5.0 on Ubuntu 24.04, then choose your Region.
- Pick an instance type (
m5.largerecommended; the software charge is $0.04 per vCPU per hour) and your key pair. - Security group: the listing's default group opens SSH (22), HTTP (80) and HTTPS (443). Restrict SSH to your own IP address. Restrict 80 and 443 to the networks your team works from, unless you publish a status page for the whole internet.
- Choose your VPC and subnet, leave the metadata defaults (the image already requires IMDSv2), then choose Launch.
Option B: AWS CLI
Run this on your own workstation. It creates a security group that allows SSH from your management address and HTTPS from your team's network, then launches the instance. Replace the AMI ID with the one shown for your Region on the Marketplace launch page, and the VPC, subnet, key pair and CIDR ranges with your own:
SG=$(aws ec2 create-security-group --group-name kuvasz \
--description "Kuvasz uptime monitoring" --vpc-id vpc-0123456789abcdef0 \
--query GroupId --output text)
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 22 --cidr <your-mgmt-cidr>
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 443 --cidr 203.0.113.0/24
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 80 --cidr 203.0.113.0/24
aws ec2 run-instances \
--image-id ami-0123456789abcdef0 \
--instance-type m5.large \
--key-name my-key-pair \
--security-group-ids "$SG" \
--subnet-id subnet-0123456789abcdef0 \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=kuvasz}]'
You do not need --metadata-options: the AMI already makes IMDSv2 required. If you want the address to survive a stop and start, associate an Elastic IP.
Step 2: Connect and find your login
Connect as ubuntu with the key pair you chose:
ssh -i ~/.ssh/my-key-pair.pem ubuntu@<public-ip>
First boot finishes within a minute or two of launch. It sets the hostname Kuvasz is served as (the instance's public DNS name when it has one), creates the TLS certificate for it, generates the secrets, starts PostgreSQL and Kuvasz, proves the admin login and the API key work, and writes /root/kuvasz-credentials.txt (mode 0600, root). Only then does it open nginx. These are the entries in that file (the secrets are not shown here):
sudo grep -E '^KUVASZ_(URL|USERNAME)=' /root/kuvasz-credentials.txt
sudo grep -oE '^[A-Z_]+=' /root/kuvasz-credentials.txt
sudo stat -c '%a %U:%G' /root/kuvasz-credentials.txt
KUVASZ_URL=https://ec2-3-92-178-244.compute-1.amazonaws.com/
KUVASZ_USERNAME=admin
KUVASZ_URL=
KUVASZ_USERNAME=
KUVASZ_PASSWORD=
KUVASZ_API_KEY=
600 root:root
| Value | What it is for |
|---|---|
KUVASZ_URL |
The address of this instance's Kuvasz |
KUVASZ_USERNAME |
Admin username (always admin) |
KUVASZ_PASSWORD |
Admin password, generated on this instance |
KUVASZ_API_KEY |
REST API key (send it as the X-API-KEY header), generated on this instance |
To read the values, run sudo cat /root/kuvasz-credentials.txt in your own session and store them in your password manager.
Step 3: Confirm Kuvasz is running
Docker, the metadata firewall rule, the first-boot units, Kuvasz and nginx are active. Only sshd and nginx listen beyond loopback: Kuvasz is on 127.0.0.1:8080, and PostgreSQL's port is open only inside the private container network.
systemctl is-active docker.service cloudimg-docker-imds-deny.service kuvasz-firstboot.service kuvasz.service kuvasz-postboot.service nginx.service
sudo docker ps --format '{{.Names}} {{.Ports}}'
sudo ss -Htlnp | awk '{print $4, $6}' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g' | sort -u
curl -sk https://127.0.0.1/api/v2/health | grep '"status":"UP"'
active
active
active
active
active
active
kuvasz 127.0.0.1:8080->8080/tcp
kuvasz-db 5432/tcp
0.0.0.0:22 users:(("sshd"),("systemd"))
0.0.0.0:443 users:(("nginx"),("nginx"),("nginx"))
0.0.0.0:80 users:(("nginx"),("nginx"),("nginx"))
127.0.0.1:8080 users:(("docker-proxy"))
127.0.0.53%lo:53 users:(("systemd-resolve"))
127.0.0.54:53 users:(("systemd-resolve"))
[::]:22 users:(("sshd"),("systemd"))
[::]:443 users:(("nginx"),("nginx"),("nginx"))
[::]:80 users:(("nginx"),("nginx"),("nginx"))
{"name":null,"status":"UP","details":null}
/api/v2/health is Kuvasz's anonymous health endpoint and reveals nothing else, so it is safe to use as a load balancer health check.
Step 4: Prove the login and the web defaults
A signed-out visit to the dashboard is sent to the sign-in page, and the REST API refuses requests without the key or with a wrong one. A cross-origin POST is refused, an unknown Host header gets no answer, and plain HTTP redirects to the configured HTTPS name. The last line uses this instance's own API key and checks the version and that authentication is on:
H=$(sudo sed -n 's/^set \$app_host "\(.*\)";$/\1/p' /etc/nginx/kuvasz-host.conf)
K=$(sudo grep '^KUVASZ_API_KEY=' /root/kuvasz-credentials.txt | cut -d= -f2-)
R=(--resolve "$H:443:127.0.0.1")
curl -sk "${R[@]}" -o /dev/null -w 'dashboard, signed out -> %{http_code} %{redirect_url}\n' "https://$H/"
curl -sk "${R[@]}" -o /dev/null -w 'REST API, no key -> %{http_code}\n' "https://$H/api/v2/http-monitors"
curl -sk "${R[@]}" -o /dev/null -w 'REST API, wrong key -> %{http_code}\n' -H 'X-API-KEY: not-the-real-key' "https://$H/api/v2/http-monitors"
curl -sk "${R[@]}" -o /dev/null -w 'cross-origin POST -> %{http_code}\n' -X POST -H 'Origin: https://evil.example' \
-H 'Content-Type: application/json' -d '{"username":"admin","password":"x"}' "https://$H/auth/login"
curl -sk -o /dev/null -w 'unknown Host -> %{http_code}\n' -H 'Host: evil.example' https://127.0.0.1/ || true
curl -s -o /dev/null -w 'http -> %{http_code} %{redirect_url}\n' -H "Host: $H" http://127.0.0.1/
curl -sk "${R[@]}" -H @<(printf 'X-API-KEY: %s\n' "$K") "https://$H/api/v2/settings" | jq -e '.app.version == "4.5.0" and .authentication.enabled'
dashboard, signed out -> 303 https://ec2-3-92-178-244.compute-1.amazonaws.com/login
REST API, no key -> 401
REST API, wrong key -> 401
cross-origin POST -> 403
unknown Host -> 000
http -> 301 https://ec2-3-92-178-244.compute-1.amazonaws.com/
true
The API key reaches curl through a header file on a file descriptor (-H @<(...)), so it never appears on a command line or in your shell history.
Step 5: Sign in to Kuvasz
Browse to KUVASZ_URL (https://<public-dns-name>/). The certificate is self-signed, so your browser warns once; accept it for this host, or install your own certificate (Step 9). Sign in with admin and KUVASZ_PASSWORD:

A new instance has no monitors, no status pages and no notification integrations. The dashboard summarises uptime, incidents, downtime, certificates and maintenance across all your monitors once you add some:

Step 6: Add monitors
Choose New monitor in the web UI and pick a type (HTTP & SSL, ICMP ping, TCP, DNS, push heartbeat or Docker). Or use the REST API. This adds four HTTP monitors that check every 60 seconds and watch each site's certificate:
H=$(sudo sed -n 's/^set \$app_host "\(.*\)";$/\1/p' /etc/nginx/kuvasz-host.conf)
K=$(sudo grep '^KUVASZ_API_KEY=' /root/kuvasz-credentials.txt | cut -d= -f2-)
R=(--resolve "$H:443:127.0.0.1")
for site in https://www.cloudimg.co.uk/ https://aws.amazon.com/ https://github.com/ https://kuvasz-uptime.dev/; do
name=$(printf '%s' "$site" | sed -E 's#^https://(www\.)?##; s#/$##')
jq -nc --arg n "$name" --arg u "$site" '{name:$n,url:$u,uptimeCheckInterval:60,enabled:true,sslCheckEnabled:true}' |
curl -sk "${R[@]}" -H @<(printf 'X-API-KEY: %s\n' "$K") -H 'Content-Type: application/json' \
--data-binary @- "https://$H/api/v2/http-monitors" | jq -c '{id,name,url}'
done
curl -sk "${R[@]}" -H @<(printf 'X-API-KEY: %s\n' "$K") "https://$H/api/v2/http-monitors" | jq -e 'length >= 4' >/dev/null && echo "monitors created"
{"id":1,"name":"cloudimg.co.uk","url":"https://www.cloudimg.co.uk/"}
{"id":2,"name":"aws.amazon.com","url":"https://aws.amazon.com/"}
{"id":3,"name":"github.com","url":"https://github.com/"}
{"id":4,"name":"kuvasz-uptime.dev","url":"https://kuvasz-uptime.dev/"}
monitors created
The first checks run straight away. Monitors > HTTP & SSL lists them with their status, certificate state and next check:

Kuvasz checks from this instance, so a monitor reaches whatever the instance can reach: public sites, and private addresses in your VPC that its security groups allow. Instance metadata addresses are blocked on purpose.
Step 7: Publish a status page
A status page shows the state of the monitors you choose. It is private until you make it public, and a public page needs no login. This creates a public page for the four monitors and checks that it is served signed out, while the built-in default page stays private:
H=$(sudo sed -n 's/^set \$app_host "\(.*\)";$/\1/p' /etc/nginx/kuvasz-host.conf)
K=$(sudo grep '^KUVASZ_API_KEY=' /root/kuvasz-credentials.txt | cut -d= -f2-)
R=(--resolve "$H:443:127.0.0.1")
jq -nc '{title:"Service status",slug:"service-status",public:true,monitors:["http:cloudimg.co.uk","http:aws.amazon.com","http:github.com","http:kuvasz-uptime.dev"]}' |
curl -sk "${R[@]}" -H @<(printf 'X-API-KEY: %s\n' "$K") -H 'Content-Type: application/json' \
--data-binary @- "https://$H/api/v2/status-pages" | jq -c '{id,slug,public}'
curl -sk "${R[@]}" -o /dev/null -w 'published status page, signed out -> %{http_code}\n' "https://$H/status/service-status"
curl -sk "${R[@]}" -o /dev/null -w 'default status page, signed out -> %{http_code}\n' "https://$H/status"
curl -sk "${R[@]}" "https://$H/status/service-status" | grep -c 'cloudimg.co.uk'
{"id":1,"slug":"service-status","public":true}
published status page, signed out -> 200
default status page, signed out -> 404
1
Anyone who can reach the instance can open https://<your-host>/status/service-status, so publish only monitor names you are happy to show. Pages refresh themselves and are cached for up to five minutes:

Step 8: Check the security controls
The certificate was made for this instance on first boot, both containers' traffic to the instance metadata service is dropped, and nginx, Docker and Kuvasz's own unit are denied it as well. The containers run as unprivileged users with no capabilities, the secrets files are readable only by the container users, and the last line sends a request to the metadata service from inside the Kuvasz container:
H=$(sudo sed -n 's/^set \$app_host "\(.*\)";$/\1/p' /etc/nginx/kuvasz-host.conf)
openssl s_client -connect 127.0.0.1:443 -servername "$H" </dev/null 2>/dev/null | openssl x509 -noout -subject -enddate
sudo iptables -S DOCKER-USER | grep DROP
systemctl show -p IPAddressDeny --value nginx.service docker.service kuvasz.service
sudo docker inspect --format '{{.Name}} user={{.Config.User}} cap_drop={{.HostConfig.CapDrop}} read_only={{.HostConfig.ReadonlyRootfs}}' kuvasz kuvasz-db
sudo ls -ln /opt/kuvasz/secrets/
out=$(sudo docker exec kuvasz wget -q -T 5 -O - http://169.254.169.254/latest/meta-data/ 2>&1); echo "$out"; [[ "$out" == *"timed out"* ]]
subject=CN = ec2-3-92-178-244.compute-1.amazonaws.com, O = cloudimg
notAfter=Jan 22 01:36:58 2029 GMT
-A DOCKER-USER -d 168.63.129.16/32 -j DROP
-A DOCKER-USER -d 169.254.169.254/32 -j DROP
169.254.169.254/32 168.63.129.16/32
169.254.169.254/32 168.63.129.16/32
169.254.169.254/32 168.63.129.16/32
/kuvasz user=10001:10001 cap_drop=[ALL] read_only=true
/kuvasz-db user=70:70 cap_drop=[ALL] read_only=false
total 8
-rw-r----- 1 0 70 32 Oct 20 01:36 db_password
-rw-r----- 1 0 10001 381 Oct 20 01:36 kuvasz-secrets.yml
wget: download timed out
Step 9: Serve Kuvasz under your own DNS name
Point a DNS record at the instance (an Elastic IP keeps the address stable), then tell Kuvasz the name. The helper re-issues the self-signed certificate for it, updates nginx's accepted names and the cross-origin guard, records the name in /etc/cloudimg/kuvasz-hostname (kept across reboots) and updates KUVASZ_URL:
sudo kuvasz-set-hostname status.example.com
kuvasz-set-hostname: Kuvasz is served at https://status.example.com/
Until you set a name, the instance follows its own public DNS name: if it comes back from a stop and start with a new address, the certificate and nginx names are updated at boot.
To use a certificate from your own CA or from Let's Encrypt instead, replace /etc/ssl/kuvasz/kuvasz.crt and /etc/ssl/kuvasz/kuvasz.key (keep the key mode 0600, owner root), set the name with kuvasz-set-hostname first so the automatic refresh leaves your certificate alone, and run sudo systemctl reload nginx.
Step 10: Notifications, monitors as code and SMTP
Notification integrations, SMTP, monitors and status pages can also be defined in /opt/kuvasz/kuvasz.yml, which ships empty. Edit it with sudo, then restart Kuvasz. For example, a global Slack channel and SMTP for email alerts:
integrations:
slack:
- name: ops_channel
webhook-url: 'https://hooks.slack.com/services/XXX/YYY/ZZZ'
global: true
smtp-config:
host: 'smtp.example.com'
port: 465
transport-strategy: SMTPS
username: 'your-smtp-user'
password: 'your-smtp-password'
sudo systemctl restart kuvasz
The file is mounted read-only into the container and is readable by root and the container user only. The full reference is in the Kuvasz configuration documentation and the integrations documentation. Use Integrations > Test in the web UI to send a test notification.
Step 11: Change the admin password or API key
The admin password, the API key and the database password are in /opt/kuvasz/secrets/kuvasz-secrets.yml. Edit the password: (under admin-auth) or api-key: value with sudo, using at least 20 characters for the password and 32 for the key, then restart Kuvasz. The start-up check refuses a short or published default value, and the file must keep mode 0640 with group 10001:
sudo nano /opt/kuvasz/secrets/kuvasz-secrets.yml
sudo systemctl restart kuvasz
Then put the new values in /root/kuvasz-credentials.txt (or delete that file) so it does not hold stale secrets. Do not change the database password in this file: PostgreSQL keeps the one it was created with.
Maintenance
- Logs:
sudo docker logs kuvaszfor Kuvasz,sudo docker logs kuvasz-dbfor PostgreSQL,/var/log/nginx/for HTTPS access,sudo journalctl -u kuvasz-firstboot.service -u kuvasz-postboot.servicefor first boot. - Restart:
sudo systemctl restart kuvasz(both containers), orsudo systemctl restart nginx. - Backups: Kuvasz's own Settings > Backup & Restore exports and imports your monitors, status pages and maintenance windows as YAML. For a full database backup, run
sudo docker exec kuvasz-db pg_dump -U kuvasz kuvasz > kuvasz.sql, and take EBS snapshots of the volume. - Operating system updates: Ubuntu's unattended security upgrades stay on, so nginx, Docker and the rest of the OS keep receiving patches. Apply the rest with
sudo apt update && sudo apt upgradein a maintenance window. - Upgrading Kuvasz: the images are pinned by digest and first boot never pulls. To upgrade deliberately, read the changelog, take a database backup, pull the new release by digest (
sudo docker pull kuvaszmonitoring/kuvasz:<new-version>@sha256:<digest>), change theimage:line of thekuvaszservice in/opt/kuvasz/docker-compose.ymlto it and runsudo systemctl restart kuvasz. Kuvasz migrates its database schema on start. Keep theuser,cap_drop,read_onlyand loopbackportssettings: never publish port 8080 on all interfaces and never use host networking, which would bypass both nginx and the metadata firewall rule.
Troubleshooting
The sign-in page says the credentials are wrong. Use admin and KUVASZ_PASSWORD from /root/kuvasz-credentials.txt exactly, over https://. After several quick failures nginx answers 429 for a short while; wait a minute and try again.
nginx is not running. On a new instance it waits until first boot has proved the credentials, by design. Check with sudo journalctl -u kuvasz-postboot.service --no-pager | tail -20 and sudo systemctl status kuvasz.
Kuvasz does not start after an edit. sudo journalctl -u kuvasz --no-pager | tail -20 shows the start-up check's reason (for example a short password, or the secrets file mode). sudo docker logs kuvasz shows YAML errors in kuvasz.yml.
A monitor is DOWN with a read timeout. The target is not reachable from the instance: check the target's security group or firewall, and whether the address is in your VPC. Instance metadata addresses are blocked on purpose.
Support
cloudimg provides 24/7 support for this image by email and live chat: launching and connecting, monitors and notifications, TLS and hostnames, and upgrades.
Kuvasz is developed by Adam Kobor and contributors and is released under the GNU Affero General Public License v3.0. The licence text and a written offer of the corresponding source ship on the instance in /usr/share/doc/kuvasz/. PostgreSQL is released under the PostgreSQL Licence. Ubuntu is a registered trademark of Canonical Ltd. Docker is a trademark of Docker, Inc. All other product and company names are trademarks or registered trademarks of their respective holders. cloudimg is not affiliated with or endorsed by them.