Ask about HTTPS on a fresh install

A fresh install asks whether to serve HTTPS, with the reasons, before
touching anything. --yes keeps the default, --no-https answers it, and an
update keeps the saved mode. The prompt reads /dev/tty so `curl | bash`
still works; without a terminal the default is explained instead.

Also state why a production process is serving plain HTTP (no TLS vars),
instead of only giving that hint in development.
This commit is contained in:
2026-10-20 10:24:38 +01:00
parent e859bab294
commit 8c20c9a7ed
4 changed files with 86 additions and 10 deletions
+1 -1
View File
@@ -115,7 +115,7 @@ Inspect live data with `bun llm-code-assistant/query-db.ts "<sql>"` - a read-onl
`install.sh` is the only supported install and update path: `curl -fsSL https://url.moduletrace.com/cobble | sudo bash`. It is idempotent, so the same script is the updater, and `--uninstall [--purge]` is the way back out. It installs Docker when missing (Linux), creates the `cobble` network, writes `$COBBLE_HOME/install.conf` and a generated `.env` (mode 600), and starts one `cobble` container. Existing secrets are never regenerated; the database is backed up to `$COBBLE_HOME/backups` before a container is replaced. The default home is `/opt/cobble`, configurable with `--home`, and everything that must survive an update lives in its `data/` directory (`db`, `media`, `computers`).
HTTPS is the default. `src/scripts/generate-local-certs.ts` (run inside the app image, since a container's interfaces are not the host's; the host's IPs and name are passed with `--host`) writes a local CA and a 397-day leaf into `$COBBLE_HOME/certs`; the certificate, key and CA are mounted read only into `/app/certs` (never the CA's key) and pointed at by `TLS_CERT_FILE`, `TLS_KEY_FILE` and `TLS_CA_FILE` (process-only envs). `src/utils/resolve-tls-options.ts` turns them into `Bun.serve`'s `tls`; a half-set or unreadable pair stops the process instead of falling back to HTTP, and `isSecureRequest` then sees an `https:` URL so cookies get `Secure`. `GET /ca.crt` (`serve-local-ca.ts`) serves the local CA for devices to trust, and `GET /ca.mobileconfig` (`build-ca-profile.ts`) serves the same CA as an iOS/macOS configuration profile for one-tap install (the trust switch after it is Apple's, not ours). The CA is kept across updates; the leaf is re-issued when the `sans.txt` names change or it is within 30 days of expiring, which a rerun does. `--no-https` removes the mount and envs (for a proxy or tunnel), `--https` turns it back on, `--https-cert`/`--https-key` copy in a real certificate, `--https-host` adds names or addresses, `--https-regen` replaces the CA. The installer and the image's `HEALTHCHECK` use `curl -k` when TLS is on. `bun run gen:certs` generates the same files into `certs/` (gitignored), `bun run dev` uses them when `TLS_*` are unset (development only), and `bun run dev:phone` generates and starts in one command. A TLS port cannot answer plain HTTP, so development also starts a redirect listener one port above (`DEV_HTTP_PORT`) that 308s to the HTTPS page (`startHttpRedirect`, tested by `build-https-redirect-url`); production publishes only the HTTPS port.
HTTPS is the default. `src/scripts/generate-local-certs.ts` (run inside the app image, since a container's interfaces are not the host's; the host's IPs and name are passed with `--host`) writes a local CA and a 397-day leaf into `$COBBLE_HOME/certs`; the certificate, key and CA are mounted read only into `/app/certs` (never the CA's key) and pointed at by `TLS_CERT_FILE`, `TLS_KEY_FILE` and `TLS_CA_FILE` (process-only envs). `src/utils/resolve-tls-options.ts` turns them into `Bun.serve`'s `tls`; a half-set or unreadable pair stops the process instead of falling back to HTTP, and `isSecureRequest` then sees an `https:` URL so cookies get `Secure`. `GET /ca.crt` (`serve-local-ca.ts`) serves the local CA for devices to trust, and `GET /ca.mobileconfig` (`build-ca-profile.ts`) serves the same CA as an iOS/macOS configuration profile for one-tap install (the trust switch after it is Apple's, not ours). The CA is kept across updates; the leaf is re-issued when the `sans.txt` names change or it is within 30 days of expiring, which a rerun does. `--no-https` removes the mount and envs (for a proxy or tunnel), `--https` turns it back on, `--https-cert`/`--https-key` copy in a real certificate, `--https-host` adds names or addresses, `--https-regen` replaces the CA. A fresh install asks about HTTPS once, with the reasons, before doing any work (`ask_https`: updates keep the saved mode, `--yes` skips it, any `--https*` flag answers it; it reads `/dev/tty`, never stdin, so `curl | bash` still works, and without a terminal it prints the notice and keeps the default). The installer and the image's `HEALTHCHECK` use `curl -k` when TLS is on. `bun run gen:certs` generates the same files into `certs/` (gitignored), `bun run dev` uses them when `TLS_*` are unset (development only), and `bun run dev:phone` generates and starts in one command. A TLS port cannot answer plain HTTP, so development also starts a redirect listener one port above (`DEV_HTTP_PORT`) that 308s to the HTTPS page (`startHttpRedirect`, tested by `build-https-redirect-url`); production publishes only the HTTPS port.
The container runs with `--network cobble`, `-p BIND:PORT`, `.env` at `/app/.env` read only, the host data directory at `/data`, the docker socket at `/var/run/docker.sock`, `MAIN_SERVER=1` and `PORT`, `--init`, a `unless-stopped` restart policy and bounded json-file logs. `/data` must stay a bind mount: `resolveHostPath` turns paths under the app's own mounts into host paths for computer containers, and a named volume would break them. `DATA_DIR=/data` and `DB_DIR=/data/db` come from the Dockerfile; `entrypoint.sh` keeps bun-sqlite's schema record on the same volume.
+15 -5
View File
@@ -26,6 +26,13 @@ missing):
curl -fsSL https://url.moduletrace.com/cobble | sudo bash
```
HTTPS is on by default, and a fresh install asks before continuing: it
encrypts the connection, and phones need it to install the app and receive
notifications. The installer creates a local CA for the machine, so each
device trusts it once. Answer no, or pass `--no-https`, if a proxy or tunnel
already handles HTTPS; `--yes` accepts the default without asking. Updates
keep the mode already chosen.
Open the address it prints, usually `https://<your-ip>:7227`. The
certificate is signed by a local CA the installer made, so the browser warns
until you trust it: open `https://<your-ip>:7227/ca.crt` on the device and
@@ -56,11 +63,13 @@ thing that should reach it (with `--no-https`).
## HTTPS
The installer serves HTTPS by default. It generates a local CA and a
certificate for this machine into `$COBBLE_HOME/certs` and mounts them into
the container read only. The CA is kept across updates; the certificate is
re-issued when the machine's addresses change or it is within 30 days of
expiring, and a rerun of the updater does both.
The installer serves HTTPS by default, and a fresh install asks before
continuing (`--yes` skips the question, `--no-https` answers it, an update
keeps the saved mode). It generates a local CA and a certificate for this
machine into `$COBBLE_HOME/certs` and mounts them into the container read
only. The CA is kept across updates; the certificate is re-issued when the
machine's addresses change or it is within 30 days of expiring, and a rerun
of the updater does both.
Trust the CA once on each device. Until it is trusted the browser warns, and
the installed app, its service worker and Web Push stay off:
@@ -127,6 +136,7 @@ curl -fsSL https://url.moduletrace.com/cobble | sudo bash -s -- --uninstall --pu
| `--no-pull` | Use an image that is already on the machine |
| `--skip-backup` | Update without a database backup |
| `--no-docker-install` | Fail instead of installing Docker |
| `--yes`, `-y` | Accept the defaults without asking (HTTPS on) |
| `--uninstall` | Stop and remove the app container |
| `--purge` | With `--uninstall`, also delete the data directory |
+66 -2
View File
@@ -77,13 +77,17 @@ Options:
--force Recreate the container even when it is current
--skip-backup Do not back up the database before an update
--no-docker-install Fail instead of installing Docker when it is missing
--yes, -y Accept the defaults without asking (HTTPS on)
--uninstall Stop and remove the app container
--purge With --uninstall, also delete the data directory
--help Show this help
HTTPS:
It is on by default. A local CA and a certificate for this machine are
generated into HOME/certs. Trust the CA on each device first:
It is on by default, and a fresh install asks before continuing. It
encrypts the connection, and phones need it to install the app and
receive notifications; a proxy or tunnel that already handles HTTPS is
the reason to answer no. A local CA and a certificate for this machine
are generated into HOME/certs, so each device trusts the CA once:
https://<this machine>:<port>/ca.crt, or HOME/certs/ca.crt.
iPhone and iPad install it in one tap from
https://<this machine>:<port>/ca.mobileconfig.
@@ -144,7 +148,9 @@ esac
ENV_FILE="$HOME_DIR/.env"
STATE_FILE="$HOME_DIR/install.conf"
HAD_STATE=0
if [ -f "$STATE_FILE" ]; then
HAD_STATE=1
# shellcheck disable=SC1090
. "$STATE_FILE"
fi
@@ -170,9 +176,11 @@ SKIP_BACKUP=0
PURGE=0
HTTPS_DISABLED=0
HTTPS_FORCE=0
HTTPS_EXPLICIT=0
HTTPS_HOSTS_GIVEN=0
HTTPS_CERT_GIVEN=0
HTTPS_REGEN=0
ASSUME_YES=0
HTTPS_MANAGED=1
CERTS_CHANGED=0
CERT_EXPIRY=""
@@ -226,10 +234,12 @@ while [ $# -gt 0 ]; do
--https)
HTTPS_ENABLED=1
HTTPS_FORCE=1
HTTPS_EXPLICIT=1
shift
;;
--no-https)
HTTPS_DISABLED=1
HTTPS_EXPLICIT=1
shift
;;
--https-host)
@@ -239,20 +249,28 @@ while [ $# -gt 0 ]; do
HTTPS_HOSTS="${2:-}"
HTTPS_HOSTS_GIVEN=1
fi
HTTPS_EXPLICIT=1
shift 2
;;
--https-cert)
HTTPS_CERT_SOURCE="${2:-}"
HTTPS_CERT_GIVEN=1
HTTPS_EXPLICIT=1
shift 2
;;
--https-key)
HTTPS_KEY_SOURCE="${2:-}"
HTTPS_CERT_GIVEN=1
HTTPS_EXPLICIT=1
shift 2
;;
--https-regen)
HTTPS_REGEN=1
HTTPS_EXPLICIT=1
shift
;;
--yes | -y)
ASSUME_YES=1
shift
;;
--force)
@@ -460,6 +478,50 @@ health_url() {
printf '%s://%s:%s/api/health' "$(health_scheme)" "$(health_host)" "$PORT"
}
# A fresh install asks about HTTPS and states why; updates keep the saved
# mode, and any --https* flag (or --yes) skips the question. The prompt
# reads /dev/tty, never stdin: with `curl | bash` stdin is the script.
ask_https() {
local answer=""
if [ "$HAD_STATE" = 1 ] || [ "$HTTPS_EXPLICIT" = 1 ] || [ "$ACTION" = "uninstall" ] || [ "$ASSUME_YES" = 1 ]; then
return 0
fi
if ! { : </dev/tty; } 2>/dev/null; then
printf '\nHTTPS is on by default: it encrypts the connection, and phones need it to install the app and receive notifications. Each device trusts the local certificate once. Pass --no-https if a proxy or tunnel already handles HTTPS.\n\n' >&2
return 0
fi
printf '\n' >&2
printf 'Cobble can serve HTTPS, and that is the default.\n' >&2
printf 'It encrypts the connection, and phones need it to install the app\n' >&2
printf 'and receive notifications. The installer creates a local CA for\n' >&2
printf 'this machine, so each device trusts it once (on an iPhone: install\n' >&2
printf 'the profile, then turn it on under Certificate Trust Settings).\n' >&2
printf '\n' >&2
printf 'Answer no if a proxy or tunnel already handles HTTPS for Cobble.\n' >&2
printf 'Serve HTTPS? [Y/n] ' >&2
exec 3</dev/tty
read -r answer <&3 || answer=""
exec 3<&-
case "$answer" in
[nN]*)
HTTPS_ENABLED=0
HTTPS_HOSTS=""
HTTPS_CERT_SOURCE=""
HTTPS_KEY_SOURCE=""
HTTPS_MANAGED=1
printf '\nPlain HTTP. Put HTTPS in front before using it outside this network.\n' >&2
;;
*)
printf '\nHTTPS on. Trust the certificate on each device before using the app.\n' >&2
;;
esac
}
set_env() {
local key="$1" value="$2" escaped
escaped="$(printf '%s' "$value" | sed -e 's/[\/&]/\\&/g')"
@@ -862,6 +924,8 @@ uninstall() {
command -v curl >/dev/null 2>&1 || fail "curl is required"
ask_https
ensure_docker
if [ "$ACTION" = "uninstall" ]; then
+4 -2
View File
@@ -86,9 +86,11 @@ if (
bunext.bunextLog.info(`Using the development certificate in certs/`);
}
if (!cert_file && !key_file && process.env.NODE_ENV != "production") {
if (!cert_file && !key_file) {
bunext.bunextLog.info(
"Serving plain HTTP (run `bun run dev:phone`, or `bun run gen:certs` then restart, for HTTPS)",
process.env.NODE_ENV == "production"
? "Serving plain HTTP (set TLS_CERT_FILE and TLS_KEY_FILE for HTTPS)"
: "Serving plain HTTP (run `bun run dev:phone`, or `bun run gen:certs` then restart, for HTTPS)",
);
}