diff --git a/archivebox/tests/test_server_security_browser.py b/archivebox/tests/test_server_security_browser.py index 70c9cce8..a4d1e3b2 100644 --- a/archivebox/tests/test_server_security_browser.py +++ b/archivebox/tests/test_server_security_browser.py @@ -735,18 +735,7 @@ def test_unconfigured_public_host_superuser_can_reach_setup_wizard(tmp_path: Pat assert "SERVER_SECURITY_MODE" in archivebox_environment assert all("ARCHIVEBOX_INGRESS_BASE_URL" not in value for value in archivebox_environment) assert compose["services"]["archivebox"]["ports"] == ["${ARCHIVEBOX_PORT:-8000}:8000"] - tunnel_environment = compose["services"]["tunnel-init"]["environment"] - traefik_environment = compose["services"]["traefik"]["environment"] - assert "ARCHIVEBOX_INGRESS_BASE_URL=${ARCHIVEBOX_INGRESS_BASE_URL:-}" in tunnel_environment - assert "ARCHIVEBOX_INGRESS_BASE_URL=${ARCHIVEBOX_INGRESS_BASE_URL:-}" in traefik_environment - traefik_entrypoint = compose["services"]["traefik"]["entrypoint"][-1] - assert "ARCHIVEBOX_INGRESS_BASE_URL" in traefik_entrypoint - assert "mkdir -p /etc/traefik" in traefik_entrypoint - assert traefik_entrypoint.index("mkdir -p /etc/traefik") < traefik_entrypoint.index("> /etc/traefik/dynamic.yml") - assert "--entrypoints.websecure.http.tls=true" in traefik_entrypoint - assert '--entrypoints.websecure.http.tls.domains[0].sans="*.$$DOMAIN"' in traefik_entrypoint - assert "on-demand" not in traefik_entrypoint.lower() - assert "ondemand" not in traefik_entrypoint.lower() + assert set(compose["services"]) == {"archivebox"} def _run_wacz_preview_probe( diff --git a/docker-compose.yml b/docker-compose.yml index a71809ca..52ce3f37 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -75,167 +75,6 @@ services: # - 127.0.0.1:8080:8080 - ### TLS / HTTPS ingress (opt-in, everything below is driven by env vars only). - # - # ArchiveBox serves the admin/web/api control plane AND every archived - # snapshot on its own subdomain for security isolation, so a public deployment - # needs wildcard DNS + TLS for *.your.domain. Pick ONE of the two ingress options - # below by activating its profile (e.g. put COMPOSE_PROFILES=https or =tunnel in a - # .env file next to this one, then `docker compose up -d`). Both want: - # ARCHIVEBOX_INGRESS_BASE_URL=https://archive.example.com - # ARCHIVEBOX_PORT=127.0.0.1:8000 # keep direct HTTP local; expose only the ingress publicly - # The first-run wizard saves BASE_URL and SERVER_SECURITY_MODE after it verifies the public URLs. - - ### Option A — Cloudflare Tunnel (no public IP / behind NAT, e.g. home/NAS). - # Cloudflare's edge terminates TLS and resolves *.your.domain to a SINGLE tunnel; - # every snapshot/control subdomain rides one connection to archivebox:8000, which - # routes by Host header — so the tunnel itself needs no wildcard cert or per-host - # config. ZERO manual setup: the one-shot tunnel-init below uses your - # CLOUDFLARE_API_KEY (give it Account:Cloudflare Tunnel:Edit + Zone:DNS:Edit - # + Zone:Read) to create/reuse the tunnel, point *.your.domain and your.domain at - # it, and write its connector token — then cloudflared just runs it. - tunnel-init: - image: python:3-alpine # tiny stdlib-only provisioner; runs as root so it can chown the token - profiles: ["tunnel"] - restart: "no" - environment: - - BASE_URL=${BASE_URL:-} - - ARCHIVEBOX_INGRESS_BASE_URL=${ARCHIVEBOX_INGRESS_BASE_URL:-} - - CLOUDFLARE_API_KEY=${CLOUDFLARE_API_KEY:-} # a Cloudflare API *Token* (used as a Bearer token), NOT the legacy global API key - - CLOUDFLARE_ACCOUNT_ID=${CLOUDFLARE_ACCOUNT_ID:-} # optional; first account used if unset - - TUNNEL_SERVICE=http://archivebox:8000 - - TUNNEL_TOKEN_OUT=/shared/token - volumes: - - ./data/proxy/tunnel:/shared - entrypoint: - - python3 - - -c - - | - import os, json, base64, secrets, urllib.request, urllib.error - API = "https://api.cloudflare.com/client/v4" - TOKEN = os.environ["CLOUDFLARE_API_KEY"] - INGRESS_BASE_URL = os.environ.get("ARCHIVEBOX_INGRESS_BASE_URL") or os.environ.get("BASE_URL") or "https://archive.example.com" - DOMAIN = INGRESS_BASE_URL.split("://")[-1].split("/")[0].split(":")[0] - SERVICE = os.environ.get("TUNNEL_SERVICE", "http://archivebox:8000") - OUT = os.environ.get("TUNNEL_TOKEN_OUT", "/shared/token") - H = {"Authorization": "Bearer " + TOKEN, "Content-Type": "application/json"} - def call(method, path, data=None): - body = json.dumps(data).encode() if data is not None else None - req = urllib.request.Request(API + path, data=body, headers=H, method=method) - try: - with urllib.request.urlopen(req, timeout=30) as r: return json.load(r) - except urllib.error.HTTPError as e: return json.load(e) - assert DOMAIN and TOKEN, "set ARCHIVEBOX_INGRESS_BASE_URL (https://archive.example.com) + CLOUDFLARE_API_KEY" - acct = os.environ.get("CLOUDFLARE_ACCOUNT_ID", "").strip() or call("GET", "/accounts")["result"][0]["id"] - labels = DOMAIN.split("."); zone = None # DOMAIN may be a subdomain; find its registrable zone - for i in range(len(labels) - 1): - res = call("GET", f"/zones?name={'.'.join(labels[i:])}")["result"] - if res: zone = res[0]["id"]; break - assert zone, f"no Cloudflare zone found for {DOMAIN}" - NAME = "archivebox-" + DOMAIN.replace(".", "-") - ts = call("GET", f"/accounts/{acct}/cfd_tunnel?name={NAME}&is_deleted=false")["result"] - tid = ts[0]["id"] if ts else call("POST", f"/accounts/{acct}/cfd_tunnel", {"name": NAME, - "tunnel_secret": base64.b64encode(secrets.token_bytes(32)).decode(), "config_src": "cloudflare"})["result"]["id"] - target = f"{tid}.cfargotunnel.com" - call("PUT", f"/accounts/{acct}/cfd_tunnel/{tid}/configurations", {"config": {"ingress": [ - {"hostname": f"*.{DOMAIN}", "service": SERVICE}, {"hostname": DOMAIN, "service": SERVICE}, - {"service": "http_status:404"}]}}) - for name in (DOMAIN, f"*.{DOMAIN}"): - recs = call("GET", f"/zones/{zone}/dns_records?name={name}")["result"] - cname = [r for r in recs if r["type"] == "CNAME"] - for r in [r for r in recs if r["type"] in ("A", "AAAA")] + cname[1:]: - call("DELETE", f"/zones/{zone}/dns_records/{r['id']}") - desired = {"type": "CNAME", "name": name, "content": target, "proxied": True, "ttl": 1} - call("PUT", f"/zones/{zone}/dns_records/{cname[0]['id']}", desired) if cname else call("POST", f"/zones/{zone}/dns_records", desired) - tok = call("GET", f"/accounts/{acct}/cfd_tunnel/{tid}/token")["result"] - os.makedirs(os.path.dirname(OUT) or ".", exist_ok=True) - with open(OUT, "w") as f: f.write(tok) - os.chmod(OUT, 0o600) # private: never world-readable on the host bind-mount - try: os.chown(OUT, 65532, 65532) # best-effort: own it by the cloudflared (uid 65532) connector that reads it - except OSError as e: print(f"[tunnel-init] warning: could not chown {OUT} to uid 65532 ({e}); ensure the cloudflared container can read it") - print(f"[tunnel-init] {NAME} ({tid}): *.{DOMAIN} + {DOMAIN} -> {SERVICE}; connector token -> {OUT}") - - cloudflared: - image: cloudflare/cloudflared - profiles: ["tunnel"] - restart: unless-stopped - depends_on: - archivebox: - condition: service_started - tunnel-init: - condition: service_completed_successfully - command: tunnel --no-autoupdate --protocol http2 run --token-file /shared/token - volumes: - - ./data/proxy/tunnel:/shared:ro - - ### Option B — Traefik reverse proxy + automatic wildcard TLS (you have a public IP). - # ONE container terminates TLS for the apex + every snapshot subdomain and proxies - # to archivebox:8000. Traefik is also an ACME client (it embeds go-acme/lego), so it - # fetches a single *.your.domain WILDCARD cert via DNS-01 and auto-renews it — no - # separate cert sidecar. All config is generated inline; no extra files. - # - # WILDCARD DNS — you must do this ONE manual step first (no proxy can do it for you): - # point a wildcard record at this server's public IP, e.g. at your DNS host add - # A *.archive.example.com -> - # A archive.example.com -> - # (AAAA too if you have IPv6). That's what makes snap-*.archive.example.com reach - # this box. Traefik then only needs the DNS *API* to solve the ACME DNS-01 challenge: - # - # set ARCHIVEBOX_ACME_DNS to your provider and put its credentials in a .env next to - # this file (passed straight through to Traefik/lego) — any of ~100 providers: - # cloudflare -> ARCHIVEBOX_ACME_DNS=cloudflare + CLOUDFLARE_DNS_API_TOKEN=... - # route53 -> ARCHIVEBOX_ACME_DNS=route53 + AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY/AWS_REGION - # digitalocean -> ARCHIVEBOX_ACME_DNS=digitalocean + DO_AUTH_TOKEN - # ... full list + exact var names: https://doc.traefik.io/traefik/https/acme/#providers - # Leave ARCHIVEBOX_ACME_DNS unset to skip ACME — Traefik then serves its built-in - # self-signed cert (browser warning), handy for local/testing. - traefik: - image: traefik:v3 - profiles: ["https"] - restart: unless-stopped - depends_on: [archivebox] - ports: - - "80:80" - - "443:443" - environment: - - BASE_URL=${BASE_URL:-} - - ARCHIVEBOX_INGRESS_BASE_URL=${ARCHIVEBOX_INGRESS_BASE_URL:-} - - ARCHIVEBOX_ACME_EMAIL=${ARCHIVEBOX_ACME_EMAIL:-admin@example.com} - - ARCHIVEBOX_ACME_DNS=${ARCHIVEBOX_ACME_DNS:-} - env_file: - - path: .env # passes your DNS provider's creds (CLOUDFLARE_DNS_API_TOKEN, AWS_*, DO_AUTH_TOKEN, ...) to Traefik - required: false - volumes: - - ./data/proxy/traefik:/certs # Traefik stores acme.json (the wildcard cert) here - entrypoint: - - sh - - -c - - | - set -eu - INGRESS_BASE_URL=$${ARCHIVEBOX_INGRESS_BASE_URL:-$${BASE_URL:-https://archive.example.com}} - DOMAIN=$$(printf '%s' "$$INGRESS_BASE_URL" | sed -E 's#^[a-z]+://##; s#[:/].*##') - # catch-all router -> archivebox (Host-routed); domain-free, so no docker socket needed - mkdir -p /etc/traefik - printf 'http:\n routers:\n archivebox:\n rule: "HostRegexp(`^.+$$`)"\n service: archivebox\n services:\n archivebox:\n loadBalancer:\n servers:\n - url: "http://archivebox:8000"\n' > /etc/traefik/dynamic.yml - set -- --entrypoints.web.address=:80 --entrypoints.websecure.address=:443 \ - --entrypoints.web.http.redirections.entrypoint.to=websecure \ - --entrypoints.web.http.redirections.entrypoint.scheme=https \ - --entrypoints.websecure.http.tls=true \ - --providers.file.filename=/etc/traefik/dynamic.yml - if [ -n "$${ARCHIVEBOX_ACME_DNS:-}" ]; then - echo "[traefik] wildcard cert for *.$$DOMAIN via $$ARCHIVEBOX_ACME_DNS DNS-01" - set -- "$$@" --entrypoints.websecure.http.tls.certresolver=le \ - --entrypoints.websecure.http.tls.domains[0].main="$$DOMAIN" \ - --entrypoints.websecure.http.tls.domains[0].sans="*.$$DOMAIN" \ - --certificatesresolvers.le.acme.email="$$ARCHIVEBOX_ACME_EMAIL" \ - --certificatesresolvers.le.acme.storage=/certs/acme.json \ - --certificatesresolvers.le.acme.dnschallenge=true \ - --certificatesresolvers.le.acme.dnschallenge.provider="$$ARCHIVEBOX_ACME_DNS" - else - echo "[traefik] no ARCHIVEBOX_ACME_DNS set -> serving Traefik's default self-signed cert (set a DNS provider for real wildcard TLS)" - fi - exec traefik "$$@" - ### Example: run all your ArchiveBox traffic through a WireGuard VPN tunnel to avoid IP blocks. # You can also use any other VPN that works at the docker/IP level, e.g. Tailscale, OpenVPN, etc. diff --git a/docs/Docker.md b/docs/Docker.md index d426bc49..ee236fae 100644 --- a/docs/Docker.md +++ b/docs/Docker.md @@ -171,8 +171,6 @@ services: ... ``` -You can also specify an env file via CLI when running compose using `docker compose --env-file=/path/to/config.env ...` although you must specify the variables in the `environment:` section that you want to have passed down to the ArchiveBox container from the passed env file. - For public HTTPS, start the default stack with `docker compose up -d`, open the admin UI on port `8000`, and follow the first-run wizard. It gives the DNS, upstream, and certificate settings to enter in Cloudflare, Nginx Proxy Manager, Caddy, Traefik, Tailscale, or your hosting platform's ingress UI, then verifies the public URLs before saving `BASE_URL` and `SERVER_SECURITY_MODE`. Use exactly one of these certificate layouts: @@ -180,7 +178,7 @@ Use exactly one of these certificate layouts: - **Single-domain mode:** one certificate for the `BASE_URL` hostname, proxied to ArchiveBox port `8000`. - **Isolated-subdomain mode:** one certificate covering both the `BASE_URL` hostname and `*.BASE_URL`, normally obtained through DNS-01. -Never enable on-demand TLS or request individual certificates for `snap-*` hostnames. The bundled Compose file also contains opt-in Cloudflare Tunnel and Traefik examples for users who prefer them; they follow the same certificate rules. +Never enable on-demand TLS or request individual certificates for `snap-*` hostnames.
diff --git a/docs/Publishing-Your-Archive.md b/docs/Publishing-Your-Archive.md index 138a9329..f90c95a8 100644 --- a/docs/Publishing-Your-Archive.md +++ b/docs/Publishing-Your-Archive.md @@ -23,7 +23,7 @@ open https://web.archive.example.com ``` This server is enabled out-of-the-box if you're using `docker-compose` to run ArchiveBox. -If hosting publicly, it's essential to place an SSL termination server in front of ArchiveBox. The bundled compose file includes opt-in `https` (Traefik) and `tunnel` (Cloudflare Tunnel) profiles, or you can bring your own reverse proxy such as [`traefik`](https://github.com/traefik/traefik), [`caddy`](https://caddyserver.com/docs/automatic-https#activation), or [`cloudflared`](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/). +If hosting publicly, place an SSL termination server in front of ArchiveBox. Start ArchiveBox normally, then follow the first-run wizard for the settings to enter in Cloudflare, Nginx Proxy Manager, Caddy, Traefik, Tailscale, or your hosting platform's ingress UI. > [!TIP] > Advanced: You can use nginx to serve a static export directly from the filesystem. Do not proxy live replay paths back onto the admin origin; use ArchiveBox's security-mode routing. diff --git a/etc/README.md b/etc/README.md index 6244aa61..f53208e2 100644 --- a/etc/README.md +++ b/etc/README.md @@ -2,20 +2,7 @@ In this folder are some example config files you can use for setting up ArchiveBox on your machine. -E.g. see `nginx.conf` for an example nginx config to serve your archive with SSL, or `fly.toml` for an example deployment to the Fly.io hosting platform. - -For the recommended, batteries-included reverse proxy and TLS, you don't need a file -here at all — it's built into the main `../docker-compose.yml` as two opt-in, env-var -driven profiles (no extra files, Dockerfiles, or scripts) — set the documented env -vars in a `.env` next to `../docker-compose.yml`: - -- `https` — a single Traefik container terminates TLS and fetches/auto-renews one - `*.` wildcard cert via DNS-01 (covering unlimited `snap-*` subdomains, - ~100 DNS providers via its embedded lego, no per-provider code), serving Traefik's - default self-signed cert if no DNS provider is configured. -- `tunnel` — a Cloudflare Tunnel whose tunnel/DNS are auto-provisioned from your API - token, so Cloudflare's edge terminates TLS and routes `*.` through one - tunnel to ArchiveBox (Host-routed) — no public IP or wildcard cert needed locally. +E.g. see `nginx.conf` for an example nginx config to serve your archive with SSL, or `fly.toml` for an example deployment to the Fly.io hosting platform. The first-run setup wizard provides the recommended settings for your existing ingress provider. Please contribute your etc files here! Example contributions