feat(service): optional OpenCode HTTP basic auth, set at install time

- install-service.sh: interactive password prompt (hidden input); --password/--username/--no-password and OPENCODE_SERVER_PASSWORD/USERNAME env
- credentials are written to a root-only env file (mode 600) referenced by the unit via EnvironmentFile=; the password never appears in the unit
- unit name/env path overridable for testing (OPENCODE_SERVICE_NAME, OPENCODE_ENV_FILE)
- README documents the auth option
This commit is contained in:
Claus Lohmar 2026-10-13 07:51:22 +00:00
parent bbe6b042a7
commit 381c1008e9
2 changed files with 133 additions and 18 deletions

View file

@ -243,14 +243,19 @@ WantedBy=multi-user.target
The service step is a **separate program** — `install-service.sh` — so you can also run it on its own (the installer just calls it after OpenCode is installed):
```bash
./install-service.sh # interactive
./install-service.sh # interactive (also asks for a password)
./install-service.sh --port 4096
./install-service.sh --host 0.0.0.0 --port 8080
./install-service.sh --yes # non-interactive defaults
./install-service.sh --uninstall # stop and remove the service
./install-service.sh --dry-run # show the unit, change nothing
./install-service.sh --yes # non-interactive defaults
./install-service.sh --password secret # enable OpenCode's HTTP basic auth
./install-service.sh --username admin --password secret
./install-service.sh --no-password # leave the server unsecured
./install-service.sh --uninstall # stop and remove the service
./install-service.sh --dry-run # show the unit, change nothing
```
**Auth (optional).** OpenCode's server has its own HTTP basic auth — enabled purely by setting `OPENCODE_SERVER_PASSWORD` (username defaults to `opencode`, or set `OPENCODE_SERVER_USERNAME`). The installer offers this interactively, or accept it non-interactively with `--password`. The secret is **never written into the unit**: it goes into a root-only env file (`/etc/opencode/opencode.env`, mode `600`) that the unit pulls in via `EnvironmentFile=`. Use this **or** your proxy's basic auth — they don't conflict.
It refuses to run non-interactively unless you pass `--yes`, and backs up any existing unit before replacing it.
- **Default bind is `127.0.0.1`** — the server has **no authentication of its own**, so keep it loopback-only and put a reverse proxy (Zoraxy, nginx, …) in front. Choose `0.0.0.0` only if you deliberately want it exposed on your network.

View file

@ -2,24 +2,31 @@
#
# OpenAgents Control — Architect Fork · web UI service installer
# ============================================================================
# Creates (or removes) a systemd service that runs the OpenCode web UI as a
# background process:
# Creates (or removes) a systemd service that runs the OpenCode web UI.
#
# opencode serve --hostname <host> --port <port>
#
# Run it on its own, or let install.sh call it after OpenCode is installed.
# Optionally protects it with OpenCode's own HTTP basic auth: the password is
# written to a root-readable env file (mode 600) referenced by the unit via
# EnvironmentFile= — the password never appears in the unit itself.
#
# Usage:
# ./install-service.sh # interactive
# ./install-service.sh --port 4096 # interactive, prefilled port
# ./install-service.sh # interactive (asks for a password)
# ./install-service.sh --port 4096
# ./install-service.sh --host 0.0.0.0 --port 8080
# ./install-service.sh --yes # non-interactive defaults
# ./install-service.sh --password secret # enable basic auth
# ./install-service.sh --username admin --password secret
# ./install-service.sh --no-password # ensure the server is unsecured
# ./install-service.sh --uninstall # stop and remove the service
# ./install-service.sh --dry-run # show the unit, change nothing
#
# Environment:
# OPENCODE_SERVICE_NAME unit name (default: opencode.service)
# OAC_FORCE_SYSTEMD 1/0 to force systemd detection (containers/CI)
# OPENCODE_SERVICE_NAME unit name (default: opencode.service)
# OPENCODE_SERVER_PASSWORD basic-auth password (same as --password)
# OPENCODE_SERVER_USERNAME basic-auth username (default: opencode)
# OPENCODE_ENV_FILE env file path (default: /etc/opencode/opencode.env)
# OAC_FORCE_SYSTEMD 1/0 to force systemd detection (containers/CI)
# ============================================================================
set -euo pipefail
@ -54,11 +61,15 @@ have_tty() {
}
tty_read() {
local __var="$1" __val=""
if [ -t 0 ] || [ ! -r /dev/tty ]; then
read -r __val || true
else
read -r __val </dev/tty || true
fi
if [ -t 0 ] || [ ! -r /dev/tty ]; then read -r __val || true
else read -r __val </dev/tty || true; fi
printf -v "$__var" '%s' "$__val"
}
tty_read_secret() {
local __var="$1" __val=""
if [ -t 0 ] || [ ! -r /dev/tty ]; then read -rs __val || true
else read -rs __val </dev/tty || true; fi
printf '\n'
printf -v "$__var" '%s' "$__val"
}
@ -82,13 +93,45 @@ is_systemd() {
# Arguments
# ---------------------------------------------------------------------------
HOST=""; PORT=""; WORKDIR=""; SVC_USER=""; SVC_GROUP=""
ENABLE="" # 1 | 0 | empty=ask
ENABLE="" # 1 | 0 | empty=ask
SVC_USERNAME="${OPENCODE_SERVER_USERNAME:-opencode}"
PASSWORD="${OPENCODE_SERVER_PASSWORD:-}"
PASSWORD_SET=0
[ -n "$PASSWORD" ] && PASSWORD_SET=1
ENV_FILE="${OPENCODE_ENV_FILE:-/etc/opencode/opencode.env}"
ASSUME_YES=0
DRY_RUN=0
UNINSTALL=0
usage() {
sed -n '2,25p' "$0" | sed 's/^# \{0,1\}//'
cat <<'EOF'
OpenAgents Control — Architect Fork · web UI service installer
Creates (or removes) a systemd service that runs the OpenCode web UI
(opencode serve). Optionally protects it with HTTP basic auth.
Usage: install-service.sh [OPTIONS]
Options:
--host HOST bind address (default: 127.0.0.1)
--port PORT port (default: 4096)
--dir PATH working directory (default: current dir)
--user USER run the service as USER (default: you)
--enable start and enable on boot
--no-enable install only; do not start
--password PW enable basic auth with this password
--username USER basic-auth username (default: opencode)
--no-password do not set a password (server unsecured)
--env-file PATH where to store credentials (default: /etc/opencode/opencode.env)
-y, --yes non-interactive (defaults; no prompts)
--uninstall stop, disable and remove the service
--dry-run show the unit and exit without changing anything
-h, --help show this help
Environment:
OPENCODE_SERVICE_NAME, OPENCODE_SERVER_PASSWORD, OPENCODE_SERVER_USERNAME,
OPENCODE_ENV_FILE, OAC_FORCE_SYSTEMD
EOF
}
while [ $# -gt 0 ]; do
@ -101,6 +144,13 @@ while [ $# -gt 0 ]; do
--dir=*) WORKDIR="${1#*=}" ;;
--user) [ $# -ge 2 ] || die "--user needs a value"; SVC_USER="$2"; shift ;;
--user=*) SVC_USER="${1#*=}" ;;
--username) [ $# -ge 2 ] || die "--username needs a value"; SVC_USERNAME="$2"; shift ;;
--username=*) SVC_USERNAME="${1#*=}" ;;
--password) [ $# -ge 2 ] || die "--password needs a value"; PASSWORD="$2"; PASSWORD_SET=1; shift ;;
--password=*) PASSWORD="${1#*=}"; PASSWORD_SET=1 ;;
--no-password) PASSWORD=""; PASSWORD_SET=0 ;;
--env-file) [ $# -ge 2 ] || die "--env-file needs a value"; ENV_FILE="$2"; shift ;;
--env-file=*) ENV_FILE="${1#*=}" ;;
--enable) ENABLE=1 ;;
--no-enable) ENABLE=0 ;;
-y|--yes) ASSUME_YES=1 ;;
@ -136,6 +186,7 @@ if [ "$UNINSTALL" = 1 ]; then
info "No unit at ${UNIT_PATH}"
fi
$SUDO systemctl daemon-reload 2>/dev/null || true
info "Credentials file left in place: ${ENV_FILE} (remove it manually if you like)."
exit 0
fi
@ -172,12 +223,45 @@ if [ "$INTERACTIVE" = 1 ] && [ "$DRY_RUN" = 0 ]; then
printf " Working directory [%s]: " "$WORKDIR"; tty_read v; [ -n "$v" ] && WORKDIR="$v"
printf " Start now and enable on boot? [Y/n]: "; v=""; tty_read v
case "$v" in [Nn]*) ENABLE=0 ;; *) ENABLE=1 ;; esac
if [ "$PASSWORD_SET" = 0 ]; then
printf " Protect the server with a password? [y/N]: "; v=""; tty_read v
case "$v" in
[Yy]*)
printf " Username [%s]: " "$SVC_USERNAME"; v=""; tty_read v; [ -n "$v" ] && SVC_USERNAME="$v"
pw1=""; printf " Password: "; tty_read_secret pw1
pw2=""; printf " Confirm password: "; tty_read_secret pw2
if [ -z "$pw1" ]; then
warn "Empty password — the server will be unsecured."
elif [ "$pw1" != "$pw2" ]; then
warn "Passwords do not match — the server will be unsecured."
else
PASSWORD="$pw1"; PASSWORD_SET=1
fi
;;
esac
fi
fi
[ -n "$ENABLE" ] || ENABLE=1
# ---------------------------------------------------------------------------
# Validate the password (env-file safety)
# ---------------------------------------------------------------------------
if [ "$PASSWORD_SET" = 1 ]; then
case "$PASSWORD" in
*$'\n'*) warn "Password contains a newline — disabling auth."; PASSWORD_SET=0 ;;
esac
case "$SVC_USERNAME" in
*$'\n'*) SVC_USERNAME="opencode" ;;
esac
fi
# ---------------------------------------------------------------------------
# Build the unit
# ---------------------------------------------------------------------------
ENV_LINE=""
[ "$PASSWORD_SET" = 1 ] && ENV_LINE="EnvironmentFile=-${ENV_FILE}"
UNIT="$(mktemp 2>/dev/null || mktemp -t oac-unit)"
cat > "$UNIT" <<EOF
[Unit]
@ -189,6 +273,7 @@ Wants=network-online.target
Type=simple
User=${SVC_USER}
Group=${SVC_GROUP}
${ENV_LINE}
WorkingDirectory=${WORKDIR}
ExecStart=${OC_BIN} serve --hostname ${HOST} --port ${PORT}
Restart=on-failure
@ -207,6 +292,7 @@ trap cleanup EXIT INT TERM
if ! is_systemd; then
warn "systemd not detected — cannot install a service here."
info "Start the web UI manually: ${OC_BIN} serve --hostname ${HOST} --port ${PORT}"
[ "$PASSWORD_SET" = 1 ] && info "with auth: OPENCODE_SERVER_PASSWORD=*** ${OC_BIN} serve ..."
exit 0
fi
@ -217,6 +303,7 @@ cat "$UNIT"
echo "------------------------------------------------------------"
if [ "$DRY_RUN" = 1 ]; then
[ "$PASSWORD_SET" = 1 ] && info "[dry-run] would write credentials to ${ENV_FILE} (mode 600)"
warn "Dry run — nothing was written."
exit 0
fi
@ -230,6 +317,23 @@ if [ "$INTERACTIVE" = 1 ]; then
esac
fi
# Write the credentials file (root-readable) before pointing the unit at it.
if [ "$PASSWORD_SET" = 1 ]; then
esc="$(printf '%s' "$PASSWORD" | sed 's/\\/\\\\/g; s/"/\\"/g')"
if ! $SUDO mkdir -p "$(dirname "$ENV_FILE")"; then
die "Could not create $(dirname "$ENV_FILE")"
fi
if ! { printf 'OPENCODE_SERVER_PASSWORD="%s"\n' "$esc"
if [ "$SVC_USERNAME" != "opencode" ]; then
printf 'OPENCODE_SERVER_USERNAME="%s"\n' "$SVC_USERNAME"
fi
} | $SUDO tee "$ENV_FILE" >/dev/null; then
die "Could not write ${ENV_FILE}"
fi
$SUDO chmod 600 "$ENV_FILE"
success "Wrote credentials to ${ENV_FILE} (mode 600)"
fi
# Back up an existing unit before replacing it
if [ -e "$UNIT_PATH" ]; then
backup="${UNIT_PATH}.bak.$(date +%Y%m%d-%H%M%S)"
@ -252,3 +356,9 @@ if [ "$ENABLE" = 1 ]; then
else
success "Unit installed — start it with: sudo systemctl start ${SERVICE_NAME}"
fi
if [ "$PASSWORD_SET" = 1 ]; then
info "Auth: HTTP basic — username '${SVC_USERNAME}', password from ${ENV_FILE}"
else
warn "No password set — the server is unsecured. Put it behind your proxy's auth."
fi