#!/usr/bin/env bash
# sshking-server: keeps an OpenSSH server in step with SSHKing (sshking.app). Run as root.
# Needs bash, curl, ssh-keygen and OpenSSH 8.2+. People's laptops use sshking-user instead.
#
#   sshking-server enroll URL TOKEN [--no-timer]
#                                    enrol this server (command shown in the web app when you add a server)
#   sshking-server sync                refresh CA trust, revoked keys, who may log in as whom, accounts, sudo, limits
#   sshking-server host                renew this server's host certificate
#   sshking-server install-timer       systemd timers (or cron): sync every 5 min, host certificate daily
#
# Everything (who may log in, accounts, sudo, certificate limits) is set in the web app; this server follows it.
# Built in: certificates never log in as root or to system accounts (uid below 1000).
# Logins are checked by /usr/local/lib/sshking/principals (a short script, installed by enroll and sync).
# legacy token mode: SSHCA_TOKEN / SSHCA_TOKEN_FILE (signer tokens from /admin)

set -euo pipefail
# systemd timers and cron run without HOME; fall back to the account's home directory (root: /root)
HOME="${HOME:-$(getent passwd "$(id -u)" 2>/dev/null | cut -d: -f6)}"; HOME="${HOME:-/root}"; export HOME

die() { echo "sshking-server: $*" >&2; exit 1; }
if [[ -r /etc/sshca.conf ]]; then
  while IFS='=' read -r k v; do
    [[ "$k" =~ ^SSHCA_[A-Z_]+$ ]] || continue
    if [[ -z "${!k:-}" ]]; then v="${v%\"}"; v="${v#\"}"; export "$k=$v"; fi
  done < /etc/sshca.conf
fi
SSHCA_URL="${SSHCA_URL:-https://sshking.app}"; SSHCA_URL="${SSHCA_URL%/}"   # filled in by the service when downloaded
[[ "$SSHCA_URL" == __SSHKING_URL* ]] && SSHCA_URL=""
DEVICE_TOKEN_FILE="${SSHCA_DEVICE_TOKEN_FILE:-/etc/sshca/device.token}"
PRINCIPALS_DIR=/etc/ssh/sshca_principals


need_url() { [[ -n "$SSHCA_URL" ]] || die "set SSHCA_URL (e.g. export SSHCA_URL=https://ssh-ca.example.workers.dev)"; }


# kv FILE KEY -> value from "key value" response lines
kv() { awk -v k="$2" '$1==k { $1=""; sub(/^ /,""); print; exit }' "$1"; }

# req METHOD PATH [JSON] [AUTH] -> body to stdout; dies with the server's message on HTTP errors
req() {
  local method="$1" path="$2" data="${3:-}" auth="${4:-}" tmp code
  tmp="$(mktemp)"
  local args=(-sS -o "$tmp" -w '%{http_code}' -X "$method" "$SSHCA_URL$path")
  [[ -n "$auth" ]] && args+=(-H "authorization: Bearer $auth")
  [[ -n "$data" ]] && args+=(-H 'content-type: application/json' --data "$data")
  code=$(curl "${args[@]}") || { rm -f "$tmp"; die "cannot reach $SSHCA_URL"; }
  [[ -n "${REQ_CODE_FILE:-}" ]] && printf '%s' "$code" > "$REQ_CODE_FILE"
  if [[ "$code" != 2* ]]; then
    local msg; msg="$(sed -n 's/.*"error": *"\(.*\)".*/\1/p' "$tmp" | head -1)"
    rm -f "$tmp"; echo "sshking-server: ${msg:-HTTP $code}" >&2; return 22
  fi
  cat "$tmp"; rm -f "$tmp"
}

pubkey_line() { # file -> "type base64" (comment dropped; keeps JSON simple)
  local key; key="$(awk '{print $1" "$2; exit}' "$1")"
  [[ "$key" =~ ^[a-z0-9@.-]+\ [A-Za-z0-9+/=]+$ ]] || die "$1 does not look like an OpenSSH public key"
  echo "$key"
}

fetch_to() { # url dest — atomic replace; returns 0 if changed
  local url="$1" dest="$2" tmp
  tmp="$(mktemp "${dest}.XXXXXX")"
  if ! curl -fsS "$url" -o "$tmp"; then rm -f "$tmp"; die "could not fetch $url"; fi
  if cmp -s "$tmp" "$dest" 2>/dev/null; then rm -f "$tmp"; return 1; fi
  chmod 644 "$tmp"; mv -f "$tmp" "$dest"; return 0
}



need_root() { [[ $(id -u) == 0 ]] || die "run this as root"; }
device_token() { [[ -r "$DEVICE_TOKEN_FILE" ]] || die "this server is not enrolled (sshking-server enroll URL TOKEN)"; tr -d '[:space:]' < "$DEVICE_TOKEN_FILE"; }

reload_sshd() {
  [[ -d /run && ! -d /run/sshd ]] && { mkdir -p /run/sshd; chmod 755 /run/sshd; } 2>/dev/null || true
  if command -v sshd >/dev/null && ! sshd -t 2>/dev/null; then echo "sshking-server: sshd -t failed, not reloading:" >&2; sshd -t || true; return 1; fi
  if command -v systemctl >/dev/null && systemctl is-active -q ssh 2>/dev/null; then systemctl reload ssh
  elif command -v systemctl >/dev/null && systemctl is-active -q sshd 2>/dev/null; then systemctl reload sshd
  elif [[ -f /var/run/sshd.pid ]]; then kill -HUP "$(cat /var/run/sshd.pid)"
  elif command -v systemctl >/dev/null && systemctl is-active -q ssh.socket 2>/dev/null; then :   # socket-activated: the next connection starts sshd with the new settings
  elif command -v rc-service >/dev/null; then rc-service sshd reload
  else echo "sshking-server: reload sshd yourself" >&2; fi
}

host_key_path() { echo "${SSHCA_HOST_KEY:-/etc/ssh/ssh_host_ed25519_key}"; }

cmd_host() {
  need_root; need_url
  local key; key="$(host_key_path)"
  [[ -f "$key.pub" ]] || ssh-keygen -q -t ed25519 -N '' -f "$key"
  if [[ -n "${SSHCA_TOKEN:-}${SSHCA_TOKEN_FILE:-}" ]]; then legacy_host "$@"; return; fi
  local cert; cert="$(req POST "/api/device/host-cert?format=text" "{\"public_key\":\"$(pubkey_line "$key.pub")\"}" "$(device_token)")" || exit 1
  local tmp; tmp="$(mktemp "$key-cert.pub.XXXXXX")"; printf '%s\n' "$cert" > "$tmp"; chmod 644 "$tmp"
  if cmp -s "$tmp" "$key-cert.pub" 2>/dev/null; then rm -f "$tmp"; else mv -f "$tmp" "$key-cert.pub"; fi
  echo "host certificate renewed: $(ssh-keygen -L -f "$key-cert.pub" | awk '/Valid:/{print $NF}')"
  reload_sshd || true   # sshd reads HostCertificate at start/reload
}

cmd_sync() {
  need_root; need_url
  local env="${SSHCA_ENV:?SSHCA_ENV missing in /etc/sshca.conf}" changed=0
  fetch_to "$SSHCA_URL/v1/envs/$env/user_ca.pub" /etc/ssh/sshca_user_ca.pub && changed=1 || true
  grep -q '^ssh-' /etc/ssh/sshca_user_ca.pub || die "user CA bundle is empty; refusing to continue"
  fetch_to "$SSHCA_URL/v1/envs/$env/revoked_keys" /etc/ssh/sshca_revoked_keys && changed=1 || true
  [[ -n "${SSHCA_TOKEN:-}${SSHCA_TOKEN_FILE:-}" ]] && return 0   # legacy mode has no access map

  local map; map="$(mktemp)"
  req GET "/api/device/sync" "" "$(device_token)" > "$map" || { rm -f "$map"; exit 1; }
  grep -q "^env $env\$" "$map" || { rm -f "$map"; die "access map is for another environment"; }
  local create_accounts sudo_admins
  create_accounts="$(awk '$1=="option" && $2=="create_accounts"{print $3}' "$map")"
  sudo_admins="$(awk '$1=="option" && $2=="sudo_admins"{print $3}' "$map")"
  write_limits "$map" && changed=1 || true
  install_helper || true
  if [[ -f "$DROPIN" ]] && write_sshd_dropin; then reload_sshd || true; changed=1; fi
  migrate_timers
  remove_local_limits
  mkdir -p "$PRINCIPALS_DIR"; chmod 755 "$PRINCIPALS_DIR"
  local keep=" " uid
  while read -r kw login users; do
    [[ "$kw" == login ]] || continue
    [[ "$login" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] || continue
    [[ "$login" != root ]] || { echo "sshking-server: certificates never log in as root; ignoring" >&2; continue; }
    if uid="$(id -u "$login" 2>/dev/null)" && [[ "$uid" -lt 1000 ]]; then echo "sshking-server: '$login' is a system account (uid $uid); certificates never log in to it" >&2; continue; fi
    keep+="$login "
    local f="$PRINCIPALS_DIR/$login" tmp; tmp="$(mktemp "$f.XXXXXX")"
    printf '%s\n' $users > "$tmp"; chmod 644 "$tmp"
    if cmp -s "$tmp" "$f" 2>/dev/null; then rm -f "$tmp"; else mv -f "$tmp" "$f"; changed=1; fi
    if ! id "$login" >/dev/null 2>&1; then
      if [[ "$create_accounts" == 1 ]]; then create_account "$login" && changed=1
      else echo "sshking-server: note: no local account '$login'. Create it, or turn on 'Create accounts on servers' for this environment" >&2; fi
    fi
  done < "$map"
  write_sudoers "$map" "$keep" && changed=1 || true
  rm -f "$map"
  for f in "$PRINCIPALS_DIR"/*; do
    [[ -e "$f" ]] || continue
    case "$keep" in *" $(basename "$f") "*) ;; *) if [[ -s "$f" ]]; then : > "$f"; changed=1; fi;; esac   # emptied, not deleted
  done
  [[ $changed == 1 ]] && echo "access updated: $(echo $keep | wc -w | tr -d ' ') account(s)" || true
}

# create_account LOGIN — a normal user with a home directory and no usable password (certificate login only)
create_account() {
  local login="$1"
  if command -v useradd >/dev/null 2>&1; then
    useradd -m -s /bin/bash -c "Created by SSHKing" "$login" || { echo "sshking-server: could not create account '$login'" >&2; return 1; }
    usermod -p '*' "$login"
  elif command -v adduser >/dev/null 2>&1; then   # BusyBox / Alpine
    adduser -D -s /bin/sh -g "Created by SSHKing" "$login" || { echo "sshking-server: could not create account '$login'" >&2; return 1; }
    sed -i "s/^$login:!/$login:*/" /etc/shadow
  else
    echo "sshking-server: no useradd/adduser; cannot create '$login'" >&2; return 1
  fi
  mkdir -p /etc/sshca; echo "$login $(date -u +%Y-%m-%dT%H:%M:%SZ)" >> /etc/sshca/created-accounts
  echo "created account '$login'"
}

# write_sudoers MAPFILE — /etc/sudoers.d/sshca lists every account the service says gets sudo; rebuilt every sync
write_sudoers() {
  local map="$1" keep="$2" dest=/etc/sudoers.d/sshca tmp
  [[ -d /etc/sudoers.d ]] || { grep -q '^sudo ' "$map" && echo "sshking-server: note: sudo is not installed; skipping sudo entries" >&2; return 1; }
  tmp="$(mktemp)"
  echo "# Managed by SSHKing: accounts given sudo in the web app. Rebuilt on every sync; do not edit." > "$tmp"
  local u
  for u in $(awk '$1=="sudo"{print $2}' "$map"); do
    [[ "$u" =~ ^[a-z_][a-z0-9_-]{0,31}$ && "$u" != root ]] || continue
    case "$keep" in *" $u "*) ;; *) continue;; esac          # only accounts certificates may log in to
    id "$u" >/dev/null 2>&1 || continue
    echo "$u ALL=(ALL:ALL) NOPASSWD: ALL" >> "$tmp"
  done
  if cmp -s "$tmp" "$dest" 2>/dev/null; then rm -f "$tmp"; return 1; fi
  if command -v visudo >/dev/null 2>&1 && ! visudo -cqf "$tmp"; then rm -f "$tmp"; echo "sshking-server: generated sudoers failed validation; left unchanged" >&2; return 1; fi
  chmod 440 "$tmp"; mv -f "$tmp" "$dest"; return 0
}

# write_limits MAPFILE — /etc/ssh/sshca_limits: the certificate age limits set in the web app
write_limits() {
  local map="$1" age re tmp
  age="$(awk '$1=="option" && $2=="max_cert_age"{print $3}' "$map")"
  re="$(awk '$1=="option" && $2=="max_recheck_age"{print $3}' "$map")"
  tmp="$(mktemp)"
  echo "# managed by SSHKing: certificate logins are refused if older than this (seconds). Rebuilt on every sync." > "$tmp"
  [[ "$age" =~ ^[0-9]+$ ]] && echo "max_cert_age $age" >> "$tmp"
  [[ "$re" =~ ^[0-9]+$ ]] && echo "max_recheck_age $re" >> "$tmp"
  if cmp -s "$tmp" /etc/ssh/sshca_limits 2>/dev/null; then rm -f "$tmp"; return 1; fi
  chmod 644 "$tmp"; mv -f "$tmp" /etc/ssh/sshca_limits; return 0
}

DROPIN=/etc/ssh/sshd_config.d/50-sshca.conf
PRINCIPALS_HELPER=/usr/local/lib/sshking/principals
# install_helper -> writes the login check sshd runs (root-owned, 755; sshd refuses anything else)
install_helper() {
  mkdir -p /usr/local/lib/sshking; chown 0:0 /usr/local/lib/sshking; chmod 755 /usr/local/lib/sshking
  local tmp; tmp="$(mktemp /usr/local/lib/sshking/.principals.XXXXXX)"
  cat > "$tmp" <<'SSHCA_HELPER_EOF'
#!/bin/bash
# sshking login helper. sshd runs it as "nobody" for every certificate login:
#   AuthorizedPrincipalsCommand /usr/local/lib/sshking/principals %u %i
# It prints who may log in to the account (from /etc/ssh/sshca_principals/<account>), but only if the
# certificate is fresh enough: issued, and its owner last confirmed it's them, within this server's
# limits (/etc/ssh/sshca_limits). Both times are inside the signed key ID. Installed by sshking-server.
u="${1:-}"; kid="${2:-}"; f="/etc/ssh/sshca_principals/${1:-}"
[[ "$u" =~ ^[a-z_][a-z0-9_-]{0,31}$ && -r "$f" ]] || exit 0
max_age=""; max_re=""
if [[ -r /etc/ssh/sshca_limits ]]; then
  while read -r k v; do case "$k" in max_cert_age) max_age="$v";; max_recheck_age) max_re="$v";; esac; done < /etc/ssh/sshca_limits
fi
deny() { command -v logger >/dev/null 2>&1 && logger -t sshking -p auth.notice "refused certificate for $u: $1 (key id $kid)"; exit 0; }
if [[ -n "$max_age$max_re" ]]; then
  [[ "$kid" =~ ,i=([0-9]+),v=([0-9]+)/[0-9]+$ ]] || deny "certificate has no issue time (made by an older client); run: sshking-user login"
  i="${BASH_REMATCH[1]}"; v="${BASH_REMATCH[2]}"; t="$(date +%s)"
  [[ "$max_age" =~ ^[0-9]+$ ]] && (( t - i > max_age + 120 )) && deny "issued more than $((max_age / 3600))h ago"
  [[ "$max_re" =~ ^[0-9]+$ ]] && (( t - v > max_re + 120 )) && deny "not confirmed in the last $((max_re / 3600))h"
fi
cat "$f"; exit 0
SSHCA_HELPER_EOF
  chown 0:0 "$tmp"; chmod 755 "$tmp"
  if cmp -s "$tmp" "$PRINCIPALS_HELPER"; then rm -f "$tmp"; selinux_label; return 1; fi
  mv -f "$tmp" "$PRINCIPALS_HELPER"; selinux_label; return 0
}
# SELinux (RHEL, Rocky, Alma, Fedora): sshd may only run programs labelled bin_t
selinux_label() {
  command -v selinuxenabled >/dev/null 2>&1 && selinuxenabled || return 0
  if command -v semanage >/dev/null 2>&1; then
    semanage fcontext -l 2>/dev/null | grep -q '^/usr/local/lib/sshking' || semanage fcontext -a -t bin_t '/usr/local/lib/sshking(/.*)?' 2>/dev/null || true
    restorecon -R /usr/local/lib/sshking 2>/dev/null || true
  else
    chcon -R -t bin_t /usr/local/lib/sshking 2>/dev/null || true
  fi
}
dropin_content() {
  local key; key="$(host_key_path)"
  echo "# managed by SSHKing"
  echo "TrustedUserCAKeys /etc/ssh/sshca_user_ca.pub"
  echo "RevokedKeys /etc/ssh/sshca_revoked_keys"
  if [[ -x "$PRINCIPALS_HELPER" ]] && id nobody >/dev/null 2>&1; then
    echo "# who may log in to each account, checked against this server's certificate age limits"
    echo "AuthorizedPrincipalsCommand $PRINCIPALS_HELPER %u %i"
    echo "AuthorizedPrincipalsCommandUser nobody"
  else
    echo "# (no 'nobody' user: certificate age limits are not enforced on this server)"
    echo "AuthorizedPrincipalsFile $PRINCIPALS_DIR/%u"
  fi
  echo "HostKey $key"
  echo "HostCertificate $key-cert.pub"
}
# write_sshd_dropin -> 0 if the drop-in changed (validated with sshd -t; the old one is restored on failure)
write_sshd_dropin() {
  mkdir -p /etc/ssh/sshd_config.d
  local tmp; tmp="$(mktemp)"; dropin_content > "$tmp"
  if cmp -s "$tmp" "$DROPIN" 2>/dev/null; then rm -f "$tmp"; return 1; fi
  [[ -f "$DROPIN" ]] && cp -p "$DROPIN" "$DROPIN.sshca-prev"
  # socket-activated sshd (Ubuntu 22.10+) has no /run/sshd until a connection comes in; sshd -t needs it
  [[ -d /run && ! -d /run/sshd ]] && { mkdir -p /run/sshd; chmod 755 /run/sshd; } 2>/dev/null || true
  local before=0; command -v sshd >/dev/null 2>&1 && sshd -t >/dev/null 2>&1 && before=1
  chmod 644 "$tmp"; mv -f "$tmp" "$DROPIN"
  if [[ $before == 1 && -f "$DROPIN.sshca-prev" ]] && ! sshd -t >/dev/null 2>&1; then
    local err; err="$(sshd -t 2>&1 | head -3)"
    mv -f "$DROPIN.sshca-prev" "$DROPIN"; echo "sshking-server: new sshd settings failed 'sshd -t'; kept the previous ones:" >&2; echo "$err" | sed 's/^/  /' >&2; return 1
  fi
  rm -f "$DROPIN.sshca-prev"
  if ! grep -qE '^\s*Include\s+/etc/ssh/sshd_config.d/\*\.conf' /etc/ssh/sshd_config; then
    sed -i.sshca-bak '1i Include /etc/ssh/sshd_config.d/*.conf' /etc/ssh/sshd_config
    echo "added 'Include /etc/ssh/sshd_config.d/*.conf' to /etc/ssh/sshd_config"
  fi
}

cmd_enroll() {
  need_root
  local url="${1:-}" token="${2:-}" timer=1; shift 2 || true
  while [[ $# -gt 0 ]]; do case "$1" in
    --no-timer) timer=0;; --allow-sudo|--allow-create) ;;   # older enrol commands; settings live in the web app now
    *) die "unknown option $1";; esac; shift; done
  [[ -n "$url" && "$token" == sshenr_* ]] || die "usage: sshking-server enroll URL sshenr_TOKEN"
  SSHCA_URL="${url%/}"
  local tmp; tmp="$(mktemp)"
  req POST "/api/device/enroll?format=text" "{\"token\":\"$token\"}" > "$tmp" || { rm -f "$tmp"; exit 1; }
  local dtok env name
  dtok="$(kv "$tmp" device_token)"; env="$(kv "$tmp" env_id)"; name="$(kv "$tmp" name)"; rm -f "$tmp"
  mkdir -p /etc/sshca; chmod 700 /etc/sshca
  ( umask 077; printf '%s\n' "$dtok" > "$DEVICE_TOKEN_FILE" )
  cat > /etc/sshca.conf <<EOF
SSHCA_URL=$SSHCA_URL
SSHCA_ENV=$env
SSHCA_DEVICE_TOKEN_FILE=$DEVICE_TOKEN_FILE
EOF
  chmod 644 /etc/sshca.conf
  export SSHCA_ENV="$env"
  echo "enrolled as '$name' in environment $env"
  cmd_sync
  install_helper || true
  write_sshd_dropin || true
  cmd_host
  [[ $timer == 1 ]] && cmd_install_timer
  echo "done. Existing authorized_keys logins keep working; certificate logins follow the access set in the web app."
}

cmd_policy() {
  echo "Local limits were removed. Accounts, sudo and certificate limits are all set in the web app."
}
# remove_local_limits — older versions kept SSHCA_ALLOW_*/SSHCA_MAX_* in /etc/sshca.conf; they no longer do anything
remove_local_limits() {
  grep -qE '^(SSHCA_ALLOW_|SSHCA_MAX_)|^# Local limits' /etc/sshca.conf 2>/dev/null || return 0
  local tmp; tmp="$(mktemp)"
  grep -vE '^(SSHCA_ALLOW_|SSHCA_MAX_)|^# Local limits' /etc/sshca.conf | cat -s > "$tmp"
  chmod 644 "$tmp"; mv -f "$tmp" /etc/sshca.conf
  echo "removed the old local limits from /etc/sshca.conf (settings are in the web app now)"
}

# timers or cron entries made by an older sshca still run "sshca sync"; point them at this script
migrate_timers() {
  if [[ -d /run/systemd/system ]] && command -v systemctl >/dev/null; then
    if [[ -f /etc/systemd/system/sshca-sync.timer || ( -f /etc/systemd/system/sshking-sync.service && ! -f /etc/systemd/system/sshking-sync.timer ) ]] \
       || { [[ -f /etc/systemd/system/sshking-sync.service ]] && ! grep -q "ExecStart=$(self_path) sync" /etc/systemd/system/sshking-sync.service; }; then
      cmd_install_timer >/dev/null && echo "timers now run $(self_path) (sshking-sync, sshking-host)"
    fi
  elif command -v crontab >/dev/null 2>&1 && crontab -l 2>/dev/null | grep -qE '/ssh(ca|ca-server) (sync|host)'; then
    cmd_install_timer >/dev/null && echo "cron entries now run $(self_path)"
  fi
  return 0
}
self_path() { local p="/usr/local/bin/sshking-server"; [[ -x "$p" ]] || p="$(cd "$(dirname "$0")" && pwd)/$(basename "$0")"; echo "$p"; }

cmd_install_timer() {
  need_root
  local self; self="$(self_path)"
  if command -v systemctl >/dev/null && [[ -d /run/systemd/system ]]; then
    for u in sync host; do
      cat > /etc/systemd/system/sshking-$u.service <<EOF
[Unit]
Description=SSHKing $u
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
Environment=HOME=/root
ExecStart=$self $u
EOF
    done
    cat > /etc/systemd/system/sshking-sync.timer <<EOF
[Unit]
Description=Refresh SSHKing trust and access every 5 minutes
[Timer]
OnBootSec=30s
OnUnitActiveSec=5min
RandomizedDelaySec=30s
[Install]
WantedBy=timers.target
EOF
    cat > /etc/systemd/system/sshking-host.timer <<EOF
[Unit]
Description=Renew SSH host certificate daily
[Timer]
OnBootSec=2min
OnUnitActiveSec=1d
RandomizedDelaySec=1h
Persistent=true
[Install]
WantedBy=timers.target
EOF
    systemctl daemon-reload
    # timers from the older sshca tool
    local o; for o in sshca-trust sshca-sync sshca-host; do
      if [[ -f /etc/systemd/system/$o.timer ]]; then systemctl disable --now $o.timer >/dev/null 2>&1 || true; rm -f /etc/systemd/system/$o.timer /etc/systemd/system/$o.service; fi
    done
    systemctl daemon-reload
    systemctl enable --now sshking-sync.timer sshking-host.timer >/dev/null
    echo "installed systemd timers: sshking-sync (5 min), sshking-host (daily)"
  else
    ( crontab -l 2>/dev/null | grep -vE 'ssh(ca|ca-server|king-server) (sync|host|trust)'; echo "*/5 * * * * $self sync >/dev/null"; echo "$((RANDOM%60)) $((RANDOM%24)) * * * $self host >/dev/null" ) | crontab -
    echo "installed cron entries: sync every 5 min, host daily"
  fi
}

# ======================================================================= legacy signer-token mode (/v1 API)

legacy_token() {
  if [[ -n "${SSHCA_TOKEN:-}" ]]; then printf %s "$SSHCA_TOKEN"; else tr -d '[:space:]' < "$SSHCA_TOKEN_FILE"; fi
}
legacy_host() {
  local key; key="$(host_key_path)"; : "${SSHCA_ENV:?set SSHCA_ENV}"
  local names; names="$(hostname -f 2>/dev/null || hostname)"
  req POST "/v1/envs/$SSHCA_ENV/sign/host?format=text" "{\"public_key\":\"$(pubkey_line "$key.pub")\",\"principals\":[\"$names\"]}" "$(legacy_token)" > "$key-cert.pub" || exit 1
  echo "wrote $key-cert.pub"; reload_sshd || true
}

case "${1:-}" in
  enroll) shift; cmd_enroll "$@";;
  sync|trust) cmd_sync;;
  policy) shift; cmd_policy "$@";;
  host) shift; cmd_host "$@";;
  install-timer) cmd_install_timer;;
  principals) exec "$PRINCIPALS_HELPER" "${2:-}" "${3:-}";;
  *) sed -n '2,22p' "$0" | sed 's/^# \{0,1\}//'; exit 1;;
esac
