Deployment guide

Web Check 2.3.7

Overview

Web Check is an open-source OSINT and security analysis dashboard for websites. You enter a domain or URL, and it gathers what is publicly observable about that site into one report of around 35 cards: DNS and TXT records, the SSL certificate and TLS connection, HTTP security headers, HSTS, open ports, DNSSEC, redirects, cookies, the detected technology stack, WHOIS data, mail configuration, crawl rules, a trace route, a page screenshot and more. Every card is also a JSON API endpoint, so you can drive it from scripts.

This guide covers launching and using the Web Check 2.3.7 on Ubuntu 24.04 AMI from SecureAnyCloud on Amazon EC2. It is one version of the SecureAnyCloud OWASP ZAP and Vulnerability Scanners listing on AWS Marketplace. You pick it as the software version when you subscribe and launch.

What you get:

  • HTTPS from the first boot. nginx serves Web Check on port 443 with a certificate generated on the first boot of your instance. Port 80 redirects to HTTPS.
  • A login unique to your instance, for the dashboard and every API route. Web Check has no login of its own, so nginx asks for the user admin and a password generated on first boot. The password is written to /var/lib/sac/data/web-check-credentials.txt (root-only) and stored for nginx as a bcrypt hash. It is never put on a command line. No password is baked into the image, and nginx refuses to start until first boot has created one.
  • An egress wall around the analysis server. By design, Web Check fetches whatever URL it is given. The web-check service, and the headless Chrome it starts for screenshots, cannot connect to the EC2 instance metadata service, to any link-local address (169.254.0.0/16) or to the private ranges 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 and 100.64.0.0/10 (and IPv6 fc00::/7 and fe80::/10), whatever host name or redirect leads there. The kernel enforces this through systemd, not the application. In addition, Web Check refuses to start an analysis of a literal private address, loopback or localhost.
  • Loopback only. The Web Check Node.js server listens on 127.0.0.1:3000 behind nginx and runs as its own unprivileged user in a systemd sandbox. The only open ports are 22, 80 and 443.
  • Hardened defaults. The AMI requires IMDSv2. nginx is also blocked from the metadata service, and requests for a host name the instance does not serve are dropped. Another web site cannot call the analysis API with the saved login of your browser. nginx passes Web Check the real client address, not one a client claims.
  • Bounded scans. Every check has a 60-second deadline, including the outbound request that the status and screenshot checks make. The kernel caps the memory of the service, so a hostile or broken site cannot hold your instance up.
  • A fully patched Ubuntu 24.04 LTS base at build time, with unattended security upgrades switched on.

This version includes a security fix. Web Check 2.3.7 includes the upstream fix for GHSA-7fc3-pm8h-hh96 (fixed in 2.3.2). In earlier releases, the status and HSTS checks fetched the scanned site with no time limit. A site that answered slowly or streamed data forever could hold the request open and exhaust the memory of the server.

SecureAnyCloud tests this on every build. Six parallel scans of a site that streams forever, and an HSTS scan of a site that never answers, are all cut off within the deadline while the service stays up. The 2.3 releases also update the bundled dependencies: the production dependency tree goes from 58 known npm advisories in 2.2.2 to 3. They add new checks: Trackers and Third Parties, Mixed Content and SRI, CAA records, data breaches (Have I Been Pwned) and social presence.

What is on the image: Web Check 2.3.7 built from the official source (tag 2.3.7) at /opt/web-check/src, Node.js 22 from the NodeSource repository, Chrome for Testing 148 (the build that Puppeteer in Web Check pins) at /opt/web-check/puppeteer, nginx, Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent. No third-party API keys are included.

Default user: ubuntu. Architecture: x86_64. Recommended instance type: m5.large.

Prerequisites

Before you launch, make sure you have:

  • An AWS account subscribed to the listing, an EC2 key pair and a VPC subnet with a route to the internet. Web Check needs outbound HTTP, HTTPS and DNS to analyse sites.
  • A security group that allows TCP 22 (SSH), TCP 443 (the dashboard) and TCP 80 (redirects to HTTPS) from your own networks only.
1

Launch the instance

In the AWS Marketplace listing, choose Continue to Subscribe, then Continue to Configuration. Pick Web Check 2.3.7 on Ubuntu 24.04 as the software version and select your Region. Then choose Launch through EC2.

Choose m5.large, your key pair and a security group as described above. Web Check keeps no data, so the root volume is all it needs.

To launch with the AWS CLI instead, 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=web-check}]'
2

Connect to your instance

Version SSH user

Web Check 2.3.7 on Ubuntu 24.04 ubuntu

ssh -i <your-key.pem> ubuntu@<public-ip>
3

Check the services

First boot takes a few seconds. When it finishes, the three units are active:

systemctl is-active web-check nginx web-check-firstboot
active active active

Web Check, a Node.js process, listens on loopback only. nginx serves ports 80 and 443:

sudo ss -tlnp | awk 'NR==1 || /node|nginx|sshd/' | awk '{print $1, $4, $6}' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g'
State Local Peer LISTEN 0.0.0.0:22 users:(("sshd"),("systemd")) LISTEN 0.0.0.0:80 users:(("nginx"),("nginx"),("nginx")) LISTEN 0.0.0.0:443 users:(("nginx"),("nginx"),("nginx")) LISTEN 127.0.0.1:3000 users:(("node")) LISTEN [::]:22 users:(("sshd"),("systemd")) LISTEN [::]:80 users:(("nginx"),("nginx"),("nginx")) LISTEN [::]:443 users:(("nginx"),("nginx"),("nginx"))
jq -r '.name + " " + .version' /opt/web-check/src/package.json node --version ls /opt/web-check/puppeteer/chrome/
web-check 2.3.7 v22.23.3 linux-148.0.7778.97
4

Get your URL and password

sudo cat /var/lib/sac/data/web-check-credentials.txt

The file holds the address that serves Web Check (the public DNS name of your instance by default), the user and the password generated on this instance:

WEB_CHECK_URL=https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/ WEB_CHECK_USERNAME=admin WEB_CHECK_PASSWORD=<24 letters and digits>
5

Sign in from your browser

Open WEB_CHECK_URL in your browser. The certificate is self-signed and generated on your instance, so accept the browser warning once, or install your own certificate (Step 11).

When your browser asks for a user name and password, enter admin and the password from Step 4. Web Check opens on its analyse screen, with one box for the site to check and the list of every check it runs underneath. The arrow beside Analyze lets you run one category of checks only: Security, SEO, Server, Domain, Email, Performance or Privacy.

6

Run your first analysis

Type a domain into the box and choose Analyze. The domain example.com is a good first target. Web Check runs over 40 lookups in parallel and fills in the report as each one answers, usually within ten to thirty seconds.

The report opens with an Advisory panel that groups the findings by severity: Issues, Warnings, Informational and Passes. It is the quickest way to see what a site gets wrong. Here it lists missing security headers and old TLS versions that the server still accepts.

7

Read the report cards

Scroll down for the detail behind the advisory. Each card grades what it found with a green tick, a warning or a cross. These cards are the main ones to read:

  • SSL Certificate shows the subject, issuer, key, expiry and renewal.
  • TLS Connection reports the negotiated protocol, the cipher suite and which TLS versions the server still accepts.
  • Mixed Content and SRI flags insecure resources on an HTTPS page.
  • CAA Records shows which certificate authorities may issue for the domain.
  • Data Breaches checks Have I Been Pwned.
  • Trace Route shows the network path from your instance to the site.
  • Firewall identifies a web application firewall in front of the site.

Further down, Archive History summarises the history of the site in the Internet Archive. Server Status splits the response time into DNS, connect, TLS, server and download. Block Detection shows whether common filtering DNS services block the domain. Trackers and Third Parties loads the page in the headless Chrome and lists the trackers it contacts.

A few cards use third-party services that need a free API key of your own: Shodan, Google Safe Browsing threat detail, global ranking, subdomain search and page quality. Without a key, they say so and the rest of the report is unaffected (Step 12).

8

Use the analysis API

Every check is a JSON endpoint under /api/<check>?url=<site>, protected by the same login. This helper reads the password from the credentials file and hands it to curl on stdin, so the password never appears on a command line:

H=$(sudo grep '^WEB_CHECK_URL=' /var/lib/sac/data/web-check-credentials.txt | sed -E 's#^WEB_CHECK_URL=https://([^/]+)/.*#\1#') wc_api() {
sudo sed -n 's/^WEB_CHECK_PASSWORD=\(.*\)$/user = "admin:\1"/p' /var/lib/sac/data/web-check-credentials.txt | curl -sk -K - --resolve "$H:443:127.0.0.1" "https://$H/api/$1"; } wc_api 'dns?url=https://example.com' | jq '{A, NS}' wc_api 'ssl?url=https://example.com' | jq '{subject, issuer, valid_to}' wc_api 'status?url=https://example.com' | jq '{isUp, responseCode}'
{ "A": [ "104.20.23.154", "172.66.147.243" ], "NS": [ "hera.ns.cloudflare.com", "elliott.ns.cloudflare.com" ] } { "subject": { "CN": "example.com" }, "issuer": { "C": "US", "O": "SSL Corporation", "CN": "Cloudflare TLS Issuing ECC CA 3" }, "valid_to": "Dec 25 22:56:35 2026 GMT" } { "isUp": true, "responseCode": 200 }

Request /api?url=<site> with no check name to run every check at once and get the whole report as one JSON document. From another machine, use https://<your host>/api/... with curl -u admin. curl then prompts for the password.

The request rate limiter of the upstream project is off on this image (API_ENABLE_RATE_LIMIT=false in /etc/web-check/web-check.env). It was written for the free public instance of the author and allows 100 requests in ten minutes. One full analysis makes about 38 requests, so the limiter would lock you out of your own instance after two analyses. The login controls access instead.

If you share the instance with a wider group, set the option to true and run sudo systemctl restart web-check.

9

Security checks

Port 80 sends browsers to HTTPS. The nginx health endpoint /health answers without a password, for load balancer checks. The dashboard, the API and the /healthz status page of Web Check do not:

H=$(sudo grep '^WEB_CHECK_URL=' /var/lib/sac/data/web-check-credentials.txt | sed -E 's#^WEB_CHECK_URL=https://([^/]+)/.*#\1#')
curl -s -o /dev/null -w 'port 80: %{http_code} -> %{redirect_url}\n' --resolve "$H:80:127.0.0.1" "http://$H/"
curl -sk --resolve "$H:443:127.0.0.1" "https://$H/health"
curl -sk -o /dev/null -w 'dashboard without a password: %{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/"
curl -sk -o /dev/null -w 'API without a password: %{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/api/dns?url=https://example.com"
curl -sk -o /dev/null -w 'healthz without a password: %{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/healthz"
port 80: 301 -> https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/ ok dashboard without a password: 401 API without a password: 401 healthz without a password: 401

Only root and nginx can read the password hash, and only root can read the credentials file. The certificate was generated on this instance and names its own addresses:

sudo stat -c '%a %U:%G %n' /etc/nginx/web-check.htpasswd /var/lib/sac/data/web-check-credentials.txt
H=$(sudo grep '^WEB_CHECK_URL=' /var/lib/sac/data/web-check-credentials.txt | sed -E 's#^WEB_CHECK_URL=https://([^/]+)/.*#\1#')
echo | openssl s_client -connect 127.0.0.1:443 -servername "$H" 2>/dev/null | openssl x509 -noout -subject -ext subjectAltName
640 root:www-data /etc/nginx/web-check.htpasswd 600 root:root /var/lib/sac/data/web-check-credentials.txt subject=CN = ec2-<public-ip-dashed>.<region>.compute.amazonaws.com, O = sac X509v3 Subject Alternative Name: DNS:ec2-<public-ip-dashed>.<region>.compute.amazonaws.com, IP Address:<public-ip>, IP Address:<private-ip>, IP Address:127.0.0.1, DNS:localhost

nginx passes Web Check a fixed host name and the real client address from the connection, never values that a client sends. It closes the connection for any host name the instance does not serve:

grep -E 'proxy_set_header (Host|X-Forwarded-For|X-Real-IP)' /etc/nginx/web-check-proxy.conf curl -s -o /dev/null -w 'unknown host name: %{http_code}\n' -H 'Host: example.com' http://127.0.0.1/ || true
proxy_set_header Host 127.0.0.1:3000; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; unknown host name: 000

The code 000 means nginx closed the connection without an answer.

Browsers resend a saved login on requests that other sites trigger. For that reason, nginx refuses API calls that a browser marks as coming from another site. It also refuses any change request or WebSocket whose Origin is another site (HTTP 403), and it removes the Access-Control-Allow-Origin: * header of the API, so another site cannot read your results.

10

What the analysis server can reach

Web Check runs as the unprivileged webcheck user in a systemd sandbox. The kernel refuses its connections to the addresses in IPAddressDeny, and so it refuses those of the headless Chrome that Web Check starts for the screenshot and trackers checks. nginx is blocked from the metadata service too. The memory of the service is capped at 75% of the RAM of the instance:

systemctl show web-check -p User -p ProtectSystem -p NoNewPrivileges -p IPAddressDeny
systemctl show nginx -p IPAddressDeny grep -E '^MemoryMax=' /etc/systemd/system/web-check.service
IPAddressDeny=169.254.0.0/16 10.0.0.0/8 fe80::/10 fc00::/7 100.64.0.0/10 192.168.0.0/16 172.16.0.0/12 168.63.129.16/32 User=webcheck ProtectSystem=strict NoNewPrivileges=yes IPAddressDeny=169.254.169.254/32 fd00:ec2::254/128 168.63.129.16/32 MemoryMax=75%

The range 169.254.0.0/16 includes the EC2 instance metadata service (169.254.169.254), and fc00::/7 includes its IPv6 address. Because the block is at the network layer, it also stops a public host name that resolves to a private address, and a site that redirects to one.

The list of hosts that Web Check refuses to scan is a second, simpler layer. It matches only the address or name you type:

H=$(sudo grep '^WEB_CHECK_URL=' /var/lib/sac/data/web-check-credentials.txt | sed -E 's#^WEB_CHECK_URL=https://([^/]+)/.*#\1#') wc_api() {
sudo sed -n 's/^WEB_CHECK_PASSWORD=\(.*\)$/user = "admin:\1"/p' /var/lib/sac/data/web-check-credentials.txt | curl -sk -K - --resolve "$H:443:127.0.0.1" "https://$H/api/$1"; } grep '^API_BLOCKED_HOSTS=' /etc/web-check/web-check.env wc_api 'headers?url=http://169.254.169.254/latest/meta-data/' | jq .
API_BLOCKED_HOSTS=169.254.0.0/16,127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,100.64.0.0/10,localhost { "skipped": "Scanning this host is not permitted on this instance." }

Only that list refuses loopback (127.0.0.0/8). The network block does not, because nginx reaches Web Check over loopback.

Each check stops after PUBLIC_API_TIMEOUT_LIMIT milliseconds (60 seconds). The status and screenshot checks apply the same deadline to their own request to the site, so a site that never finishes answering is cut off instead of holding the server:

grep '^PUBLIC_API_TIMEOUT_LIMIT=' /etc/web-check/web-check.env
PUBLIC_API_TIMEOUT_LIMIT=60000

Scanning private addresses. To let Web Check analyse hosts inside your own VPC, remove those ranges deliberately, and only on an instance that you keep private. Override the network block with a drop-in that resets the list to the metadata addresses only. Then remove the private ranges from API_BLOCKED_HOSTS in /etc/web-check/web-check.env:

sudo mkdir -p /etc/systemd/system/web-check.service.d printf '[Service]\nIPAddressDeny=\nIPAddressDeny=169.254.0.0/16 168.63.129.16/32 fd00:ec2::254/128\n' \
  | sudo tee /etc/systemd/system/web-check.service.d/60-allow-private.conf
sudo systemctl daemon-reload && sudo systemctl restart web-check
11

Your own DNS name and certificate

Point a DNS record at the instance, then set the name that serves Web Check. This creates a new self-signed certificate for the name, updates nginx and the URL in the credentials file, and keeps the name across reboots:

sudo web-check-set-hostname <your-domain>

To use a certificate from your own certificate authority instead, copy it over the generated pair and reload nginx. Running web-check-set-hostname again replaces it with a self-signed one:

sudo install -m 0644 <your-domain>.crt /etc/nginx/ssl/web-check.crt
sudo install -m 0600 <your-domain>.key /etc/nginx/ssl/web-check.key
sudo nginx -t && sudo systemctl reload nginx
12

Password, API keys and upgrades

web-check-set-password sets a new password for admin, records it in the credentials file and reloads nginx. Run it without arguments for a new random password, or pass your own password (16 to 64 letters and digits) on stdin:

sudo web-check-set-password printf '%s\n' '<your-new-password>' | sudo web-check-set-password --stdin

The cards that use third-party services read their keys from /etc/web-check/web-check.env. The key names are listed there, commented out:

grep -E '^#\s+[A-Z_]+=$' /etc/web-check/web-check.env
# GOOGLE_CLOUD_API_KEY=
# SHODAN_API_KEY=
# SECURITY_TRAILS_API_KEY=
# URL_SCAN_API_KEY=
# TRANCO_USERNAME=
# TRANCO_API_KEY=
# CLOUDMERSIVE_API_KEY=
# WHO_API_KEY=
# TORRENT_IP_API_KEY=

Uncomment the one you want and add your key. Then run sudo systemctl restart web-check.

Web Check keeps no database and no report history, so there is nothing to back up. The data breaches and social presence checks send the domain you scan to Have I Been Pwned and to public social sites. The other third-party checks run only when you add their key.

The image ships the version named in the listing. To move to a newer release, launch the newer version from the listing when SecureAnyCloud publishes it.

13

Server components

Component Path

Web Check source and build /opt/web-check/src

Headless Chrome /opt/web-check/puppeteer

Runtime settings /etc/web-check/web-check.env

Service unit and egress drop-ins /etc/systemd/system/web-check.service, /etc/systemd/system/web-check.service.d/

nginx site /etc/nginx/sites-available/sac-web-check

Password hash (bcrypt) /etc/nginx/web-check.htpasswd (root:www-data, 0640)

Certificate and key /etc/nginx/ssl/

First boot script /usr/local/sbin/web-check-firstboot.sh

Host name and password helpers /usr/local/sbin/web-check-set-hostname, /usr/local/sbin/web-check-set-password

Credentials /var/lib/sac/data/web-check-credentials.txt (mode 0600)

14

Security recommendations

  • Allow ports 22, 80 and 443 in the security group from your own networks only.
  • Keep the password secret, and rotate it if it may have been shared (Step 12).
  • Use your own DNS name and a certificate that your browsers trust (Step 11).
  • Keep the egress block unless you need to scan private hosts, and then lift it only on a private instance (Step 10).
  • Keep the operating system patched. Unattended security upgrades are on, and sudo apt-get update && sudo apt-get upgrade applies everything else, including Node.js updates from the NodeSource repository.

Support and licensing

Web Check is open source under the MIT licence. A copy is at /usr/share/doc/web-check/LICENSE. There is no per-user or per-CPU licence fee. The AMI price covers the hardening, first-boot setup and support from SecureAnyCloud.

  • Email: sales@secureanycloud.com
  • Website: https://secureanycloud.com
  • Support: 24/7, with a 24 hour response SLA

Deploy on AWS

Launch Web Check 2.3.7 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

Deploy on AWS