install.sh: enrollment token never on the command line (node-26#18)
CI / lint (push) Successful in 6s
CI / test-op25 (push) Successful in 14s
CI / lint (pull_request) Successful in 8s
CI / test (push) Successful in 48s
CI / test-op25 (pull_request) Successful in 15s
CI / test (pull_request) Successful in 49s

The token is now prompted for on the TTY (hidden input), or read from
--token-file PATH / an exported DRB_ENROLLMENT_TOKEN for unattended installs.
--token still works for old copied commands but warns that it has landed in
shell history and ps. Every "re-run with --token DRB-..." hint now points at
the prompt; stale-pickup (401/404) re-runs prompt for a token instead of
dying. DRB_ENROLLMENT_TOKEN is unset after reading so children don't inherit
it. README install example drops --token.

Server half (mint panel still prints --token) is filed separately.

Refs #18

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Logan Cusano
2026-10-04 18:09:19 -04:00
co-authored by Claude Opus 5.5
parent 80a0e88fc4
commit 4dff9cbab6
2 changed files with 60 additions and 23 deletions
+7 -4
View File
@@ -142,13 +142,16 @@ this repo at the `v1` tag, enrols with C2, pulls the prebuilt images and starts:
```bash ```bash
curl -fsSL https://git.vpn.cusano.net/logan/node-26/raw/tag/v1/install.sh \ curl -fsSL https://git.vpn.cusano.net/logan/node-26/raw/tag/v1/install.sh \
| sudo bash -s -- --token DRB-xxxx --node-id node-003 \ | sudo bash -s -- --node-id node-003 \
--c2-url https://api.<domain> --mqtt-broker mqtt.<domain> --c2-url https://api.<domain> --mqtt-broker mqtt.<domain>
``` ```
Mint the `--token` at **Settings → Nodes** in the web app (the panel prints the It prompts (hidden input) for an enrollment token, minted at **Settings → Nodes**
whole command). Run `install.sh --help` for every flag; each also has a in the web app. The token never goes on the command line — argv lands in shell
`DRB_*` env var. `--build` compiles op25 on the Pi (~1h) instead of pulling. history and `ps` (node-26#18). Unattended installs use `--token-file PATH` (mode
0600) or an exported `DRB_ENROLLMENT_TOKEN`; the old `--token` flag still works
but warns. Run `install.sh --help` for every flag; each also has a `DRB_*` env
var. `--build` compiles op25 on the Pi (~1h) instead of pulling.
### Local dev / manual ### Local dev / manual
+53 -19
View File
@@ -3,9 +3,14 @@
# DRB edge node — one-shot bootstrap for a clean Raspberry Pi OS (arm64). # DRB edge node — one-shot bootstrap for a clean Raspberry Pi OS (arm64).
# #
# curl -fsSL https://git.vpn.cusano.net/logan/node-26/raw/tag/v1/install.sh \ # curl -fsSL https://git.vpn.cusano.net/logan/node-26/raw/tag/v1/install.sh \
# | sudo bash -s -- --token DRB-xxxx --node-id node-003 \ # | sudo bash -s -- --node-id node-003 \
# --c2-url https://api.<domain> --mqtt-broker mqtt.<domain> # --c2-url https://api.<domain> --mqtt-broker mqtt.<domain>
# #
# It prompts (hidden input) for the enrollment token. The token is never
# taken from the command line by design (node-26#18, CISO condition from
# server-26#61): argv lands in shell history and in `ps`. Unattended installs
# use --token-file PATH (a 0600 file) or an exported DRB_ENROLLMENT_TOKEN.
#
# (fetch install.sh from the same tag it installs; use raw/branch/main with # (fetch install.sh from the same tag it installs; use raw/branch/main with
# --track-main for the owner's own always-latest nodes) # --track-main for the owner's own always-latest nodes)
# #
@@ -78,6 +83,7 @@ MQTT_BROKER="${DRB_MQTT_BROKER:-}"
MQTT_PORT="${DRB_MQTT_PORT:-8883}" MQTT_PORT="${DRB_MQTT_PORT:-8883}"
MQTT_TLS="${DRB_MQTT_TLS:-true}" MQTT_TLS="${DRB_MQTT_TLS:-true}"
ENROLLMENT_TOKEN="${DRB_ENROLLMENT_TOKEN:-}" ENROLLMENT_TOKEN="${DRB_ENROLLMENT_TOKEN:-}"
unset DRB_ENROLLMENT_TOKEN # read once; don't leak it into every child's env
DASHBOARD_USER="${DRB_DASHBOARD_USERNAME:-admin}" DASHBOARD_USER="${DRB_DASHBOARD_USERNAME:-admin}"
DASHBOARD_PASS="${DRB_DASHBOARD_PASSWORD:-}" DASHBOARD_PASS="${DRB_DASHBOARD_PASSWORD:-}"
REGISTRY="${DRB_IMAGE_REGISTRY:-git.vpn.cusano.net}" # D1 (public host) REGISTRY="${DRB_IMAGE_REGISTRY:-git.vpn.cusano.net}" # D1 (public host)
@@ -108,7 +114,10 @@ usage() {
cat <<'USAGE' cat <<'USAGE'
Usage: install.sh [options] Usage: install.sh [options]
--token TOKEN Enrollment token (Settings -> Nodes -> New token) (enrollment token) Prompted for on the terminal, input hidden. Mint one at
Settings -> Nodes -> New token.
--token-file PATH read the enrollment token from PATH (unattended installs;
keep it mode 0600 and delete it afterwards)
--node-id ID Unique node id, e.g. node-003 --node-id ID Unique node id, e.g. node-003
--name NAME Display name (default: node id) --name NAME Display name (default: node id)
--lat N --lon N Decimal degrees for the map --lat N --lon N Decimal degrees for the map
@@ -132,13 +141,17 @@ Usage: install.sh [options]
Every option also has an env var: DRB_NODE_ID, DRB_C2_URL, DRB_ENROLLMENT_TOKEN, Every option also has an env var: DRB_NODE_ID, DRB_C2_URL, DRB_ENROLLMENT_TOKEN,
DRB_MQTT_BROKER, DRB_REF, DRB_INSTALL_DIR, DRB_REGISTRY_USER/PASS, ... DRB_MQTT_BROKER, DRB_REF, DRB_INSTALL_DIR, DRB_REGISTRY_USER/PASS, ...
Secrets are read from the environment or prompted on the TTY, never from a pipe. Secrets are read from a file, the environment or a TTY prompt, never from a
pipe. Never put the token on a command line: `--token X` and
`sudo DRB_ENROLLMENT_TOKEN=X bash` both expose it in shell history and `ps`.
(`--token` still works for old copied commands but is deprecated, see node-26#18.)
USAGE USAGE
} }
while [ $# -gt 0 ]; do while [ $# -gt 0 ]; do
case "$1" in case "$1" in
--token) ENROLLMENT_TOKEN="$2"; shift 2 ;; --token) ENROLLMENT_TOKEN="$2"; TOKEN_ON_ARGV=1; shift 2 ;;
--token-file) TOKEN_FILE="$2"; shift 2 ;;
--node-id) NODE_ID="$2"; shift 2 ;; --node-id) NODE_ID="$2"; shift 2 ;;
--name) NODE_NAME="$2"; shift 2 ;; --name) NODE_NAME="$2"; shift 2 ;;
--lat) NODE_LAT="$2"; shift 2 ;; --lat) NODE_LAT="$2"; shift 2 ;;
@@ -162,6 +175,18 @@ while [ $# -gt 0 ]; do
esac esac
done done
if [ "${TOKEN_ON_ARGV:-0}" = 1 ]; then
warn "--token is deprecated: the token is now in your shell history and visible in"
warn "\`ps\` for this whole install (node-26#18). Omit it to be prompted, or use --token-file."
fi
if [ -n "${TOKEN_FILE:-}" ]; then
[ -r "$TOKEN_FILE" ] || die "--token-file: cannot read $TOKEN_FILE"
_mode="$(stat -c %a "$TOKEN_FILE" 2>/dev/null || echo 600)"
(( 8#$_mode & 8#044 )) && warn "--token-file $TOKEN_FILE is readable by other users — chmod 600 it"
ENROLLMENT_TOKEN="$(tr -d '[:space:]' < "$TOKEN_FILE")"
[ -n "$ENROLLMENT_TOKEN" ] || die "--token-file: $TOKEN_FILE is empty"
fi
[ "$ENROLL_WAIT" = forever ] || [[ "$ENROLL_WAIT" =~ ^[0-9]+$ ]] \ [ "$ENROLL_WAIT" = forever ] || [[ "$ENROLL_WAIT" =~ ^[0-9]+$ ]] \
|| die "--wait-approval takes a number of seconds (or omit it to wait until approved)" || die "--wait-approval takes a number of seconds (or omit it to wait until approved)"
@@ -181,6 +206,14 @@ ask_secret() {
read -rsp "$__p: " __r </dev/tty; echo >/dev/tty read -rsp "$__p: " __r </dev/tty; echo >/dev/tty
printf -v "$__v" '%s' "$__r" printf -v "$__v" '%s' "$__r"
} }
# Make sure ENROLLMENT_TOKEN is set: already given, else prompt on the TTY.
# Returns 1 if there is still no token (non-interactive, or Enter at the prompt).
need_token() {
[ -n "$ENROLLMENT_TOKEN" ] || ask_secret ENROLLMENT_TOKEN "Enrollment token (input hidden)"
[ -n "$ENROLLMENT_TOKEN" ]
}
NO_TOKEN_HELP="mint one at Settings -> Nodes and re-run the install command; it prompts for
the token (unattended: --token-file PATH, or an exported DRB_ENROLLMENT_TOKEN)"
genpw() { head -c 24 /dev/urandom | base64 | tr -d '\n=' ; } genpw() { head -c 24 /dev/urandom | base64 | tr -d '\n=' ; }
# ── 1. Preflight ──────────────────────────────────────────────────────────── # ── 1. Preflight ────────────────────────────────────────────────────────────
@@ -416,8 +449,8 @@ wait_for_key() { # wait_for_key PICKUP_SECRET
while :; do while :; do
case "$CRED_HTTP" in case "$CRED_HTTP" in
200) [ -n "$(cred_field '.api_key')" ] && { trap - INT TERM; return 0; } ;; 200) [ -n "$(cred_field '.api_key')" ] && { trap - INT TERM; return 0; } ;;
401) die "C2 rejected the saved pickup secret (401) while waiting — re-run with --token DRB-… to re-enroll." ;; 401) die "C2 rejected the saved pickup secret (401) while waiting — re-run the installer to re-enroll (it asks for a fresh enrollment token)." ;;
404) die "C2 no longer knows node '$NODE_ID' (404) — it was deleted. Re-run with --token DRB-… to enroll again." ;; 404) die "C2 no longer knows node '$NODE_ID' (404) — it was deleted. Re-run the installer to enroll again (it asks for an enrollment token)." ;;
*) : ;; # 000/5xx: transient, keep waiting *) : ;; # 000/5xx: transient, keep waiting
esac esac
if [ "$_shown" = 0 ]; then if [ "$_shown" = 0 ]; then
@@ -435,8 +468,7 @@ wait_for_key() { # wait_for_key PICKUP_SECRET
do_fresh_enroll() { do_fresh_enroll() {
say "Enrolling '$NODE_ID' with $C2_URL" say "Enrolling '$NODE_ID' with $C2_URL"
[ -n "$ENROLLMENT_TOKEN" ] || ask_secret ENROLLMENT_TOKEN "Enrollment token" need_token || die "no enrollment token — $NO_TOKEN_HELP"
[ -n "$ENROLLMENT_TOKEN" ] || die "no enrollment token — mint one at Settings -> Nodes, then re-run with --token"
local BODY RESP PICKUP STATUS KEY local BODY RESP PICKUP STATUS KEY
BODY="$(jq -nc --arg id "$NODE_ID" --arg n "${NODE_NAME:-$NODE_ID}" \ BODY="$(jq -nc --arg id "$NODE_ID" --arg n "${NODE_NAME:-$NODE_ID}" \
@@ -459,7 +491,7 @@ do_fresh_enroll() {
or have an admin Reissue key (Settings -> Nodes) and write the key into or have an admin Reissue key (Settings -> Nodes) and write the key into
$CREDS by hand (see node-26#6)." ;; $CREDS by hand (see node-26#6)." ;;
*401*) die "enroll refused (401): enrollment token missing/invalid/revoked — *401*) die "enroll refused (401): enrollment token missing/invalid/revoked —
mint a fresh one at Settings -> Nodes and re-run with --token." ;; $NO_TOKEN_HELP." ;;
*429*) die "enroll refused (429): rate limited. Wait ~1 minute, then re-run." ;; *429*) die "enroll refused (429): rate limited. Wait ~1 minute, then re-run." ;;
*) die "enroll failed: $RESP" ;; *) die "enroll failed: $RESP" ;;
esac esac
@@ -522,24 +554,26 @@ elif [ -s "$PICKUP_FILE" ]; then
fi fi
;; ;;
401) 401)
if [ -n "$ENROLLMENT_TOKEN" ]; then warn "saved pickup secret rejected (401) — likely rotated by a re-enroll elsewhere."
warn "saved pickup secret rejected (401) — likely rotated by a re-enroll elsewhere. Re-enrolling with --token." if need_token; then
warn "re-enrolling with the enrollment token."
rm -f "$PICKUP_FILE" rm -f "$PICKUP_FILE"
do_fresh_enroll do_fresh_enroll
else else
die "saved pickup secret is stale (401) and no --token was given. Re-run with die "saved pickup secret is stale (401) and no enrollment token was given.
--token DRB-… to re-enroll (only works while the node is still pending), or Re-enrolling only works while the node is still pending: $NO_TOKEN_HELP.
have an admin Reissue key for an approved node and write $CREDS by hand." For an approved node, have an admin Reissue key and write $CREDS by hand."
fi fi
;; ;;
404) 404)
if [ -n "$ENROLLMENT_TOKEN" ]; then warn "C2 does not know node '$NODE_ID' (404) — deleted server-side or never fully enrolled."
warn "C2 does not know node '$NODE_ID' (404) — deleted server-side or never fully enrolled. Re-enrolling with --token." if need_token; then
warn "re-enrolling with the enrollment token."
rm -f "$PICKUP_FILE" rm -f "$PICKUP_FILE"
do_fresh_enroll do_fresh_enroll
else else
die "C2 does not know node '$NODE_ID' (404) and no --token was given. die "C2 does not know node '$NODE_ID' (404) and no enrollment token was given:
Re-run with --token DRB-… to enroll it again." $NO_TOKEN_HELP."
fi fi
;; ;;
000) 000)
@@ -618,6 +652,6 @@ if [ -z "$(jq -r '.api_key // empty' "$CREDS" 2>/dev/null)" ]; then
if [ -s "$PICKUP_FILE" ]; then if [ -s "$PICKUP_FILE" ]; then
printf "${Y}This node has no api_key yet. Once an admin approves it at %s/settings/nodes\nthe node fetches the key and comes online by itself (no re-run needed).${N}\n\n" "$APP_URL" printf "${Y}This node has no api_key yet. Once an admin approves it at %s/settings/nodes\nthe node fetches the key and comes online by itself (no re-run needed).${N}\n\n" "$APP_URL"
else else
printf "${Y}This node has no api_key and no saved pickup secret, so a plain re-run cannot\nfix it. Re-run with --token DRB-… to enroll; or, if the node is already\napproved, have an admin Reissue key and write it into\n%s by hand.${N}\n\n" "$CREDS" printf "${Y}This node has no api_key and no saved pickup secret. Re-run the installer\nand give it an enrollment token when it asks; or, if the node is already\napproved, have an admin Reissue key and write it into\n%s by hand.${N}\n\n" "$CREDS"
fi fi
fi fi