infra: edge-control install.sh overwrites /etc/caddy/Caddyfile with no carve-out for apex/static sites — landing page lost on install #788

Closed
opened 2026-04-15 16:20:57 +00:00 by dev-bot · 0 comments
Collaborator

Symptom

tools/edge-control/install.sh line 227-240 writes /etc/caddy/Caddyfile unconditionally with a minimal template:

{
  admin localhost:2019
}

:80, :443 {
  tls {
    dns gandi {env.GANDI_API_KEY}
  }
}

No backup of the pre-existing file. No import directive. No site block for the apex domain or static content. All actual serving is then expected to happen through dynamic routes injected by lib/caddy.sh add_route, and those routes only handle <project>.<DOMAIN_SUFFIX> hosts via reverse_proxy to 127.0.0.1:<port>.

Consequence on a real deployment (harb-staging as observed 2026-04-15): the current Caddyfile serves three distinct things — disinto.ai (static landing page from /home/debian/disinto-site), www.disinto.ai (redirect to apex), and <project>.disinto.ai reverse-proxies. Running install.sh as-is would lose the apex + www blocks and leave visitors with an empty catch-all.

The installer is presented in the README as idempotent (curl … | bash -s -- --gandi-token …) but cannot safely be re-run on a box that already serves non-tunnel content. This matches the class of drift we just eliminated for docker-compose.yml via #770.

Fix

  1. Back up the existing Caddyfile before overwriting. At the top of the "Create Caddyfile with admin API and wildcard cert" step:

    if [ -f "$CADDYFILE" ] && [ ! -f "${CADDYFILE}.pre-disinto" ]; then
      cp "$CADDYFILE" "${CADDYFILE}.pre-disinto"
      log_info "Backed up existing Caddyfile to ${CADDYFILE}.pre-disinto"
    fi
    

    So an operator who runs install.sh on a non-empty box loses nothing.

  2. Support an --extra-caddyfile <path> flag that causes the emitted Caddyfile to end with import <path>. Operators write their apex/www/static blocks once into a site-owned file and edge-control leaves them alone:

    {
      admin localhost:2019
    }
    
    :80, :443 {
      tls {
        dns gandi {env.GANDI_API_KEY}
      }
    }
    
    import /etc/caddy/extra.d/*.caddy
    

    Default the path to /etc/caddy/extra.d/ (glob import) so operators can drop in landing.caddy, www-redirect.caddy, etc., and re-running install.sh preserves them.

  3. Document the contract in tools/edge-control/README.md: edge-control owns <project>.<DOMAIN_SUFFIX> routing; the operator owns apex, www, and any non-tunnel content via /etc/caddy/extra.d/.

Out of scope for this issue

Extending the register protocol with a first-class "static site" verb (e.g. ssh disinto-register@edge "static <name> <path>") would be nice — you could serve the landing page as just another registered entry — but it changes the control-plane surface. Treat as a separate follow-up if ever needed. The import-directive approach above is small enough to ship now.

Affected files

  • tools/edge-control/install.sh — backup + extra-caddyfile support + extra.d directory scaffolding
  • tools/edge-control/README.md — document the operator/edge-control boundary

Acceptance criteria

  • Running install.sh on a box with an existing /etc/caddy/Caddyfile creates /etc/caddy/Caddyfile.pre-disinto as a backup before overwriting
  • Emitted Caddyfile ends with import /etc/caddy/extra.d/*.caddy (or equivalent) so operator-owned blocks survive across re-installs
  • /etc/caddy/extra.d/ directory is created during install with 0755 perms, root:caddy ownership
  • README documents: operators put apex/www/static site config under /etc/caddy/extra.d/, edge-control only touches the top-level Caddyfile + dynamic routes via admin API
  • Re-running install.sh on an already-installed box does not clobber /etc/caddy/extra.d/ contents
  • CI green
## Symptom `tools/edge-control/install.sh` line 227-240 writes `/etc/caddy/Caddyfile` unconditionally with a minimal template: ```caddyfile { admin localhost:2019 } :80, :443 { tls { dns gandi {env.GANDI_API_KEY} } } ``` No backup of the pre-existing file. No import directive. No site block for the apex domain or static content. All actual serving is then expected to happen through dynamic routes injected by `lib/caddy.sh add_route`, and those routes only handle `<project>.<DOMAIN_SUFFIX>` hosts via reverse_proxy to `127.0.0.1:<port>`. Consequence on a real deployment (harb-staging as observed 2026-04-15): the current Caddyfile serves three distinct things — `disinto.ai` (static landing page from `/home/debian/disinto-site`), `www.disinto.ai` (redirect to apex), and `<project>.disinto.ai` reverse-proxies. Running `install.sh` as-is would lose the apex + www blocks and leave visitors with an empty catch-all. The installer is presented in the README as idempotent (`curl … | bash -s -- --gandi-token …`) but cannot safely be re-run on a box that already serves non-tunnel content. This matches the class of drift we just eliminated for `docker-compose.yml` via #770. ## Fix 1. **Back up the existing Caddyfile before overwriting.** At the top of the "Create Caddyfile with admin API and wildcard cert" step: ```bash if [ -f "$CADDYFILE" ] && [ ! -f "${CADDYFILE}.pre-disinto" ]; then cp "$CADDYFILE" "${CADDYFILE}.pre-disinto" log_info "Backed up existing Caddyfile to ${CADDYFILE}.pre-disinto" fi ``` So an operator who runs install.sh on a non-empty box loses nothing. 2. **Support an `--extra-caddyfile <path>` flag** that causes the emitted Caddyfile to end with `import <path>`. Operators write their apex/www/static blocks once into a site-owned file and edge-control leaves them alone: ```caddyfile { admin localhost:2019 } :80, :443 { tls { dns gandi {env.GANDI_API_KEY} } } import /etc/caddy/extra.d/*.caddy ``` Default the path to `/etc/caddy/extra.d/` (glob import) so operators can drop in `landing.caddy`, `www-redirect.caddy`, etc., and re-running install.sh preserves them. 3. **Document the contract** in `tools/edge-control/README.md`: edge-control owns `<project>.<DOMAIN_SUFFIX>` routing; the operator owns apex, www, and any non-tunnel content via `/etc/caddy/extra.d/`. ## Out of scope for this issue Extending the register protocol with a first-class "static site" verb (e.g. `ssh disinto-register@edge "static <name> <path>"`) would be nice — you could serve the landing page as just another registered entry — but it changes the control-plane surface. Treat as a separate follow-up if ever needed. The import-directive approach above is small enough to ship now. ## Affected files - `tools/edge-control/install.sh` — backup + extra-caddyfile support + extra.d directory scaffolding - `tools/edge-control/README.md` — document the operator/edge-control boundary ## Acceptance criteria - [ ] Running `install.sh` on a box with an existing `/etc/caddy/Caddyfile` creates `/etc/caddy/Caddyfile.pre-disinto` as a backup before overwriting - [ ] Emitted Caddyfile ends with `import /etc/caddy/extra.d/*.caddy` (or equivalent) so operator-owned blocks survive across re-installs - [ ] `/etc/caddy/extra.d/` directory is created during install with 0755 perms, root:caddy ownership - [ ] README documents: operators put apex/www/static site config under `/etc/caddy/extra.d/`, edge-control only touches the top-level Caddyfile + dynamic routes via admin API - [ ] Re-running install.sh on an already-installed box does not clobber `/etc/caddy/extra.d/` contents - [ ] CI green
dev-bot added the
backlog
bug-report
labels 2026-04-15 16:20:57 +00:00
dev-bot self-assigned this 2026-04-15 16:41:22 +00:00
dev-bot added
in-progress
and removed
backlog
labels 2026-04-15 16:41:23 +00:00
dev-bot removed their assignment 2026-04-15 16:48:47 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: disinto-admin/disinto#788
No description provided.