changedetection.io 0.60.10
Overview
This guide shows you how to launch and verify changedetection.io 0.60.10 on Ubuntu 24.04 from SecureAnyCloud on Amazon EC2. It is one version of the SecureAnyCloud 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). It re-checks the page on a schedule, keeps every snapshot, and shows you exactly what was added or removed.
A watch can be narrowed to a CSS, XPath or JSON region, and it can trigger only on text you care about. Alerts go through email, Slack, Discord, Telegram, Matrix, webhooks and the other channels of the Apprise library. You manage everything 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 you get:
- changedetection.io 0.60.10 from the official upstream container image
ghcr.io/dgtlmoon/changedetection.io:0.60.10, pinned by digestsha256: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/changedetectionfor 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 inspectnever 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
/healthzhealth 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 otherHostheader gets its connection closed), uses TLS 1.2 or 1.3, refuses cross-originPOSTrequests and cross-site WebSocket upgrades (403), strips CORS headers, rate-limits sign-in attempts, 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 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-USERfirewall 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 systemdIPAddressDeny. 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
fwupdfirmware updater stays installed, but its LVFS metadata refresh (fwupd-refresh.timerand.service) is masked, so an LVFS outage never marks your instance unhealthy at boot. If you use fwupd, runsudo fwupdmgr refreshyourself. - 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
- 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 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: changedetection.io 0.60.10 on Ubuntu 24.04
- Operating system: Ubuntu 24.04 LTS
- SSH user: ubuntu
Launch the instance
Option A: AWS Marketplace console
- Open the SecureAnyCloud Hardened Actual Budget & Self-Hosted Personal Apps AMIs listing on AWS Marketplace. Choose View purchase options, then Subscribe. One subscription covers every version in the listing.
- When 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 changedetection.io 0.60.10 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. - Set the security group. The listing's default group opens SSH (22), HTTP (80) and HTTPS (443). Restrict SSH to your own IP address, and restrict 80 and 443 to the networks you work from.
- Choose your VPC and subnet, keep the metadata defaults (the image already requires IMDSv2), then choose Launch.
Option B: AWS CLI
Run these commands on your own workstation. They create a security group that allows SSH from your management address and HTTPS from your own network, then launch the instance. Replace the AMI ID with the one shown for your Region on the Marketplace launch page. Replace 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. To keep the address across a stop and start, associate an Elastic IP.
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 that changedetection.io is served as (the instance's public DNS name when it has one) and creates the TLS certificate for it. It then generates the login password, lets changedetection.io create its datastore, and proves that the password and the API key work.
Last, it writes /root/changedetection-credentials.txt (mode 0600, root) and opens 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.txtCHANGEDETECTION_URL=https://ec2-3-83-154-156.compute-1.amazonaws.com/ CHANGEDETECTION_URL= CHANGEDETECTION_PASSWORD= <set-at-first-boot>= 600 root:root- CHANGEDETECTION_URL: the address of this instance's changedetection.io
- CHANGEDETECTION_PASSWORD: the login password, generated on this instance (changedetection.io has no username)
- CHANGEDETECTION_API_KEY: the REST API key, created on this instance (send it as the x-api-key header)
To read the values, run sudo cat /root/changedetection-credentials.txt in your own session, and store them in your password manager.
Confirm changedetection.io is running
Check that 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 sac-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/healthzactive 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 you can safely use it as a load balancer health check.
Prove the login and the web defaults
A signed-out visit to the watch list is redirected to the sign-in page. 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.
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:
A new instance has no watches, no tags and no notification targets.
Add watches
Paste a URL into Web page URL and choose Watch, or use the REST API. These commands add three watches with the default check interval and list 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://secureanycloud.com/ 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)"' | sortwatching https://secureanycloud.com/ watching https://changedetection.io/CHANGELOG.txt watching https://www.debian.org/News/ https://changedetection.io/CHANGELOG.txt checked=true error=false https://secureanycloud.com/ checked=true error=false https://www.debian.org/News/ checked=true error=falseThe watch list shows each page's title, when it was last checked and when it last changed:
Preview shows the text of the latest snapshot. Once a page changes, History highlights what was added and removed between any two snapshots:
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 by design.
Check the security controls
These checks confirm the following:
- 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, and nginx, Docker and the changedetection unit are denied it as well.
- The container runs as an unprivileged user with no capabilities.
- The password file and datastore are closed to other users.
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") "$@"; }
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/changedetectionException: 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 = sac 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/changedetectionThe datastore belongs to uid 10001, the container's user, which has no account on the host (hence UNKNOWN).
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 the name and updates nginx's accepted names and the cross-origin guard. It also records the name in /etc/sac/changedetection-hostname (kept across reboots) and updates CHANGEDETECTION_URL:
sudo changedetection-set-hostname watch.example.comchangedetection-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. Then run sudo systemctl reload nginx.
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.
Click Send test notification to check the target straight away. The full list of services is in the Apprise documentation.
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. After a restart, the service stays down until you run sudo changedetection-reset-password (see Troubleshooting).
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-passwordKeep 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 changedetectionfor changedetection.io,/var/log/nginx/for HTTPS access,sudo journalctl -u changedetection-firstboot.service -u changedetection-postboot.servicefor first boot. - Restart:
sudo systemctl restart changedetection, orsudo 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 upgradein 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, and pull the new release by digest (
sudo docker pull ghcr.io/dgtlmoon/changedetection.io:<new-version>@sha256:<digest>), change theimage:line in/opt/changedetection/docker-compose.ymlto it, and runsudo systemctl restart changedetection. Keep theuser,cap_drop,security_opt,ulimitsand loopbackportssettings. 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. By design, nginx on a new instance waits until first boot has proved the credentials. 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
SecureAnyCloud provides 24/7 support for this image by email and live chat. Support covers 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. SecureAnyCloud is not affiliated with or endorsed by them.
Deploy on AWS
Launch changedetection.io 0.60.10 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