Applications AWS

changedetection.io 0.60.10 on AWS User Guide

| Product: changedetection.io 0.60.10 on Ubuntu 24.04 on AWS

Overview

This guide covers launching and verifying changedetection.io 0.60.10 on Ubuntu 24.04 from cloudimg on Amazon EC2. It is one version of the cloudimg self-hosted personal apps listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.

changedetection.io is the open-source (Apache-2.0) website change monitor. Point it at any web page (a price, a stock level, terms and conditions, a job board, a release notes file, a government notice) and it re-checks the page on a schedule, keeps every snapshot, and shows you exactly what was added or removed. It can narrow a watch to a CSS, XPath or JSON region, trigger only on text you care about, and alert you through email, Slack, Discord, Telegram, Matrix, webhooks and the other channels of the Apprise library. Everything is manageable from the web UI or the REST API.

Version 0.60.10 includes the fix for GHSA-gwph-fp79-379w (high): the URL filter now also refuses the carrier-grade NAT range 100.64.0.0/10, which earlier releases let a watch fetch.

What is on the image:

  • changedetection.io 0.60.10 from the official upstream container image ghcr.io/dgtlmoon/changedetection.io:0.60.10, pinned by digest sha256:72f97681f390e4d6ae3e6585e1e0036449910f347144b766fc23910458566a1a
  • Docker CE (official Docker apt repository) running the container under the systemd unit changedetection.service
  • A dedicated 20 GiB EBS data volume mounted at /var/lib/changedetection for the watch list, snapshots and settings, created empty on your instance's first boot
  • 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 login password and REST API key, generated on first boot into a root-only file
  • changedetection-set-hostname, a helper that serves changedetection.io under your own DNS name and re-issues the certificate for it

Default user: ubuntu · Architecture: x86_64 · Root volume: 8 GiB gp3 · Data volume: 20 GiB gp3

Security posture

  • No default login. Upstream changedetection.io ships with no password at all. On this image the login password (28 characters) is generated on each instance's first boot and stored as changedetection.io's own salted PBKDF2 hash. The REST API key, the RSS token and the session-signing secret are created by changedetection.io itself on that first boot. None is in the image, so no two instances share one.
  • Fails closed. changedetection.io will not start unless the datastore holds a password, the API key is switched on and the password file is root-only. nginx stays closed until first boot has proved that the password and the API key really authenticate.
  • No secret in the container environment. The container's environment and command line carry only the listen address and proxy settings, so docker inspect never shows a secret.
  • No example watches. A new upstream datastore adds two example watches. First boot creates the datastore in a container with no network at all and removes them, so your instance fetches nothing until you add a watch.
  • What is public. Everything except the /healthz health check needs the login (web UI) or the API key (REST API). The optional "anonymous access to watch history" setting is off.
  • Only nginx faces the network. changedetection.io listens on 127.0.0.1:5000. nginx answers only for the configured hostname (any other Host header gets its connection closed), uses TLS 1.2 or 1.3, refuses cross-origin POST requests and cross-site WebSocket upgrades (403), strips CORS headers, rate-limits sign-in attempts, forces the session cookie to Secure, HttpOnly and SameSite=Lax, and replaces any client-supplied X-Forwarded-For, X-Real-IP or Forwarded header 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 changedetection.io that fixed name, so links and redirects it builds cannot be steered by a request.
  • Watches cannot read instance metadata. A watch fetches any URL you or your users enter, so three layers stop it reaching the instance metadata service: changedetection.io's own URL filter refuses private, link-local and carrier-grade NAT addresses; a Docker DOCKER-USER firewall rule, in place before any container starts, drops container traffic to 169.254.169.254; and nginx, Docker and the changedetection units are blocked by systemd IPAddressDeny. This was proved at build time with real watches pointed at the metadata service.
  • Least-privilege container. changedetection.io runs as an unprivileged user (uid 10001) with every Linux capability dropped, no-new-privileges, a 1 GiB memory limit and a process limit.
  • No scheduled firmware-metadata refresh. The fwupd firmware updater stays installed, but its LVFS metadata refresh (fwupd-refresh.timer and .service) is masked, so an LVFS outage never marks your instance unhealthy at boot; run sudo fwupdmgr refresh yourself if you use it.
  • No core dumps. changedetection.io, nginx and the first-boot units run with core dumps disabled, so process memory holding your credentials is never written to disk.
  • 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

  1. An AWS account and permission to launch EC2 instances
  2. An EC2 key pair in the Region you launch in
  3. Outbound internet access from the instance to the pages you want to watch and the services you notify

Recommended instance type: m5.large. changedetection.io's plain HTTP fetcher checks hundreds of watches comfortably on 2 vCPUs and 8 GiB of memory. The image also runs on 4 GiB instances. Grow the data volume if you keep long snapshot histories for many watches.

Connecting to your instance

Version Operating system SSH user
changedetection.io 0.60.10 on Ubuntu 24.04 Ubuntu 24.04 LTS ubuntu

Step 1: Launch the instance

Option A: AWS Marketplace console

  1. Open the cloudimg Hardened Actual Budget & Self-Hosted Personal Apps AMIs listing on AWS Marketplace and choose View purchase options, then Subscribe. One subscription covers every version in the listing.
  2. Once the subscription is active, choose Launch your software.
  3. Choose One-click launch from AWS Marketplace (or Launch from EC2 Console for the full launch wizard). In Version, choose changedetection.io 0.60.10 on Ubuntu 24.04, then choose your Region.
  4. Pick an instance type (m5.large recommended; the software charge is $0.04 per vCPU per hour) and your key pair.
  5. Security group: the listing's default group opens SSH (22), HTTP (80) and HTTPS (443). Restrict SSH to your own IP address, and 80 and 443 to the networks you work from.
  6. 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 own 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 changedetection \
  --description "changedetection.io" --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=changedetection}]'

You do not need --metadata-options or --block-device-mappings: the AMI already requires IMDSv2 and brings its 20 GiB data volume. 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 changedetection.io is served as (the instance's public DNS name when it has one), creates the TLS certificate for it, generates the login password, lets changedetection.io create its datastore, proves the password and the API key work, and writes /root/changedetection-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 '^CHANGEDETECTION_URL=' /root/changedetection-credentials.txt
sudo grep -oE '^[A-Z_]+=' /root/changedetection-credentials.txt
sudo stat -c '%a %U:%G' /root/changedetection-credentials.txt
CHANGEDETECTION_URL=https://ec2-3-83-154-156.compute-1.amazonaws.com/
CHANGEDETECTION_URL=
CHANGEDETECTION_PASSWORD=
CHANGEDETECTION_API_KEY=
600 root:root
Value What it is for
CHANGEDETECTION_URL The address of this instance's changedetection.io
CHANGEDETECTION_PASSWORD Login password, generated on this instance (changedetection.io has no username)
CHANGEDETECTION_API_KEY REST API key (send it as the x-api-key header), created on this instance

To read the values, run sudo cat /root/changedetection-credentials.txt in your own session and store them in your password manager.

Step 3: Confirm changedetection.io is running

Docker, the metadata firewall rule, changedetection.io and nginx are active. Only sshd and nginx listen beyond loopback: changedetection.io is on 127.0.0.1:5000, and its data is on the separate EBS volume.

systemctl is-active docker.service cloudimg-docker-imds-deny.service changedetection.service nginx.service
sudo docker ps --format '{{.Names}} {{.Image}} {{.Ports}}' | sed -E 's/@sha256:[0-9a-f]+//'
sudo ss -Htlnp | awk '{print $4, $6}' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g' | sort -u
findmnt -no SOURCE,FSTYPE,SIZE /var/lib/changedetection
curl -sk https://127.0.0.1/healthz
active
active
active
active
changedetection ghcr.io/dgtlmoon/changedetection.io:0.60.10 127.0.0.1:5000->5000/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:5000 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"))
/dev/nvme1n1 ext4   19.5G
ok

/healthz is answered by nginx itself and reveals nothing, 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 watch list is redirected 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 HTTPS. The last line uses this instance's own API key and shows the version and an empty watch list:

H=$(sudo sed -n 's/^set \$app_host "\(.*\)";$/\1/p' /etc/nginx/changedetection-host.conf)
R=(--resolve "$H:443:127.0.0.1")
K=$(sudo grep '^CHANGEDETECTION_API_KEY=' /root/changedetection-credentials.txt | cut -d= -f2-)
api() { curl -sk "${R[@]}" -H @<(printf 'x-api-key: %s\n' "$K") "$@"; }
curl -sk "${R[@]}" -o /dev/null -w 'watch list, signed out -> %{http_code}\n' "https://$H/"
curl -sk "${R[@]}" -o /dev/null -w 'API, no key -> %{http_code}\n' "https://$H/api/v1/watch"
curl -sk "${R[@]}" -o /dev/null -w 'API, wrong key -> %{http_code}\n' -H 'x-api-key: not-the-real-key' "https://$H/api/v1/watch"
curl -sk "${R[@]}" -o /dev/null -w 'cross-origin POST -> %{http_code}\n' -X POST -H 'Origin: https://evil.example' -d 'password=x' "https://$H/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}\n' -H "Host: $H" http://127.0.0.1/
api "https://$H/api/v1/systeminfo" | jq -c '{version, watch_count, tag_count}'
watch list, signed out -> 302
API, no key -> 403
API, wrong key -> 403
cross-origin POST -> 403
unknown Host -> 000
http -> 301
{"version":"0.60.10","watch_count":0,"tag_count":0}

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 changedetection.io

Browse to CHANGEDETECTION_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 8). changedetection.io has a single password and no username. Enter CHANGEDETECTION_PASSWORD:

The changedetection.io sign-in page with a single password field and a Login button

A new instance has no watches, no tags and no notification targets.

Step 6: Add watches

Paste a URL into Web page URL and choose Watch, or use the REST API. This adds three watches with the default check interval and lists them once their first check has run:

H=$(sudo sed -n 's/^set \$app_host "\(.*\)";$/\1/p' /etc/nginx/changedetection-host.conf)
R=(--resolve "$H:443:127.0.0.1")
K=$(sudo grep '^CHANGEDETECTION_API_KEY=' /root/changedetection-credentials.txt | cut -d= -f2-)
api() { curl -sk "${R[@]}" -H @<(printf 'x-api-key: %s\n' "$K") "$@"; }
for site in https://www.cloudimg.co.uk/ https://changedetection.io/CHANGELOG.txt https://www.debian.org/News/; do
  jq -nc --arg u "$site" '{url:$u}' |
    api -H 'Content-Type: application/json' \
      --data-binary @- "https://$H/api/v1/watch" | jq -e '.uuid' >/dev/null && echo "watching $site"
done
sleep 60
api "https://$H/api/v1/watch" |
  jq -r 'to_entries[] | "\(.value.url) checked=\(.value.last_checked > 0) error=\(.value.last_error)"' | sort
watching https://www.cloudimg.co.uk/
watching https://changedetection.io/CHANGELOG.txt
watching https://www.debian.org/News/
https://changedetection.io/CHANGELOG.txt checked=true error=false
https://www.cloudimg.co.uk/ checked=true error=false
https://www.debian.org/News/ checked=true error=false

The watch list shows each page's title, when it was last checked and when it last changed:

The changedetection.io watch list with three watches, cloudimg, the changedetection.io changelog and Debian Latest News, each checked two minutes ago and not yet changed

Preview shows the text of the latest snapshot. Once a page changes, History highlights what was added and removed between any two snapshots:

The changedetection.io preview of the cloudimg home page watch showing the extracted page text of the latest snapshot

changedetection.io fetches from this instance, so a watch reaches whatever the instance can reach on the public internet. Private, link-local and carrier-grade NAT addresses, including the instance metadata service, are refused on purpose.

Step 7: Check the security controls

A watch pointed at the instance metadata service is refused by changedetection.io's URL filter (and is then deleted). The certificate was made for this instance on first boot, container traffic to the metadata service is dropped, nginx, Docker and the changedetection unit are denied it as well, the container runs as an unprivileged user with no capabilities, and the password file and datastore are closed to other users:

H=$(sudo sed -n 's/^set \$app_host "\(.*\)";$/\1/p' /etc/nginx/changedetection-host.conf)
R=(--resolve "$H:443:127.0.0.1")
K=$(sudo grep '^CHANGEDETECTION_API_KEY=' /root/changedetection-credentials.txt | cut -d= -f2-)
api() { curl -sk "${R[@]}" -H @<(printf 'x-api-key: %s\n' "$K") "$@"; }
U=$(jq -nc '{url:"http://169.254.169.254/latest/meta-data/"}' | api -H 'Content-Type: application/json' --data-binary @- "https://$H/api/v1/watch" | jq -r .uuid)
api -o /dev/null "https://$H/api/v1/watch/$U?recheck=1"; sleep 20
api "https://$H/api/v1/watch/$U" | jq -r '.last_error' | cut -c1-110
api -o /dev/null -w 'metadata watch deleted -> %{http_code}\n' -X DELETE "https://$H/api/v1/watch/$U"
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 changedetection.service
sudo docker inspect --format '{{.Name}} user={{.Config.User}} cap_drop={{.HostConfig.CapDrop}} no_new_privs={{.HostConfig.SecurityOpt}}' changedetection
sudo stat -c '%a %U:%G %n' /opt/changedetection/secrets/admin_password /var/lib/changedetection
Exception: Fetch blocked: 'http://169.254.169.254/latest/meta-data/' resolves to a private/reserved IP address
metadata watch deleted -> 204
subject=CN = ec2-3-83-154-156.compute-1.amazonaws.com, O = cloudimg
notAfter=Feb  9 09:30:43 2029 GMT
-A DOCKER-USER -d 168.63.129.16/32 -j DROP
-A DOCKER-USER -d 169.254.169.254/32 -j DROP
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 169.254.169.254/32
/changedetection user=10001:10001 cap_drop=[ALL] no_new_privs=[no-new-privileges:true]
600 root:root /opt/changedetection/secrets/admin_password
700 UNKNOWN:UNKNOWN /var/lib/changedetection

The datastore belongs to uid 10001, the container's user, which has no account on the host (hence UNKNOWN).

Step 8: Serve changedetection.io under your own DNS name

Point a DNS record at the instance (an Elastic IP keeps the address stable), then tell changedetection.io 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/changedetection-hostname (kept across reboots) and updates CHANGEDETECTION_URL:

sudo changedetection-set-hostname watch.example.com
changedetection-set-hostname: changedetection.io is served at https://watch.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/changedetection/changedetection.crt and /etc/ssl/changedetection/changedetection.key (keep the key mode 0600, owner root), set the name with changedetection-set-hostname first so the automatic refresh leaves your certificate alone, and run sudo systemctl reload nginx.

Step 9: Notifications and settings

Open Settings to change the default check interval, global filters, fetching options and time zone, and to add notification targets. Settings > Notifications (and each watch's Edit > Notifications) take Apprise URLs, for example mailtos://user:password@smtp.example.com, slack://TokenA/TokenB/TokenC/#channel or json://hooks.example.com/changes; Send test notification checks the target straight away. The full list of services is in the Apprise documentation.

The changedetection.io Settings page on the General tab with the default time between checks, history limits, the Remove password button and the Save button

Keep the password. The Remove password button would leave the instance open to anyone who can reach it. This image refuses to start changedetection.io without a password, so after a restart the service stays down until you run sudo changedetection-reset-password (see Troubleshooting).

Step 10: Change the password or API key

changedetection.io's Settings page only offers Remove password while a password is set, so change the password from the shell instead, without ever opening the instance. Put the new password (at least 20 characters) into the root-only first-boot password file and re-seed it; changedetection-reset-password stores it as changedetection.io's salted hash and restarts the service:

sudo nano /opt/changedetection/secrets/admin_password
sudo changedetection-reset-password

Keep the file at mode 0600, owner root. To issue a new API key, use Settings > API > Regenerate API key and keep API access switched on. Then update /root/changedetection-credentials.txt with sudo nano (or delete that file) so it does not hold stale secrets.

Maintenance

  • Logs: sudo docker logs changedetection for changedetection.io, /var/log/nginx/ for HTTPS access, sudo journalctl -u changedetection-firstboot.service -u changedetection-postboot.service for first boot.
  • Restart: sudo systemctl restart changedetection, or sudo systemctl restart nginx.
  • Backups: Settings > Backups creates a zip of your watches, snapshots and settings for download. For the whole datastore, take EBS snapshots of the data volume mounted at /var/lib/changedetection.
  • 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 upgrade in a maintenance window.
  • Upgrading changedetection.io: the image is pinned by digest and first boot never pulls. To upgrade deliberately, read the release notes, take a backup, pull the new release by digest (sudo docker pull ghcr.io/dgtlmoon/changedetection.io:<new-version>@sha256:<digest>), change the image: line in /opt/changedetection/docker-compose.yml to it and run sudo systemctl restart changedetection. Keep the user, cap_drop, security_opt, ulimits and loopback ports settings: never publish port 5000 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 password is wrong. Use CHANGEDETECTION_PASSWORD from /root/changedetection-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 changedetection-postboot.service --no-pager | tail -20 and sudo systemctl status changedetection.

changedetection.io does not start. sudo journalctl -u changedetection --no-pager | tail -20 shows the start-up check's reason, for example no password in the datastore after Remove password was pressed in Settings, or API access switched off. sudo changedetection-reset-password restores the password held in /opt/changedetection/secrets/admin_password (the first-boot password, unless you changed it in Step 10), switches API access back on and starts changedetection.io again.

A watch shows "Fetch blocked". The URL resolves to a private, link-local or carrier-grade NAT address, which changedetection.io refuses by design. Watches of addresses inside your VPC are not supported on this image.

Support

cloudimg provides 24/7 support for this image by email and live chat: launching and connecting, watches and notifications, TLS and hostnames, and upgrades.

changedetection.io is developed by Leigh Morresi (dgtlmoon) and contributors and is released under the Apache License 2.0. The licence text and a pointer to the source ship on the instance in /usr/share/doc/changedetection/. 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.