3 changed files with 365 additions and 45 deletions
+14 -45
View File
@@ -1,65 +1,35 @@
# luki-net / proxmox-scripts
Community-script-style installers for LXC services in the luki-net Proxmox VE homelab. Each script creates an unprivileged Debian 12 LXC and installs one specific app sensible defaults, interactive prompts, env-overridable for non-interactive runs.
Community-script-style installers for LXC services in my Proxmox VE homelab. Each script creates an unprivileged Debian 12 LXC and installs one specific app, with sensible defaults and interactive prompts.
Inspired by [community-scripts/ProxmoxVE](https://github.com/community-scripts/ProxmoxVE), but minimal, self-hosted and tailored to this stack.
Inspired by [community-scripts/ProxmoxVE](https://github.com/community-scripts/ProxmoxVE), but minimal, self-hosted and tailored to my stack.
## Script catalog
## Available scripts
| App | What it provisions | Status |
|-----|--------------------|--------|
| [devpi](ct/devpi.sh) | Private PyPI cache/mirror — saves time on CUDA/torch rebuilds | ✅ stable |
| [webapp](ct/webapp.sh) | Next.js site with deploy-as-code via a repo-scoped Gitea Actions runner (host mode, no inbound port) | ✅ stable |
| [nexus](ct/nexus.sh) | App LXC for [nexus](https://gitea.luki-net.org/l.kirchner/nexus-hub) (Family Knowledge Hub): host-mode runner (label `nexus`, CI + deploy), service skeleton, `/opt/nexus` layout. Runtime is provisioned/extended via [`install/nexus-runtime.sh`](install/nexus-runtime.sh) (idempotent, re-runnable) | ✅ in production |
| [nexus-db](ct/nexus-db.sh) | PostgreSQL 16 + pgvector for nexus — least-privilege role, pg_hba allowlist (only the nexus LXC), DSN handed over via credentials file | ✅ in production |
| [authentik](ct/authentik.sh) | Central homelab IdP (official Docker Compose via LXC nesting) — headless bootstrap admin **and** API token for agent-driven blueprint configuration, blueprints mount, permanent auth domain (WebAuthn RP-ID) | ✅ in production |
| [runner](ct/runner.sh) | General **instance-wide** Gitea Actions runner (label `homelab`) — Docker for throwaway CI test containers, deliberately **no** sudoers/deploy rights | 🔄 in review ([PR #6](https://gitea.luki-net.org/luki-net/proxmox-scripts/pulls/6)) |
Run any one-liner on a Proxmox VE host as root:
```bash
bash -c "$(curl -fsSL https://gitea.luki-net.org/luki-net/proxmox-scripts/raw/branch/main/ct/<app>.sh)"
```
| App | Description | One-liner |
|-----|-------------|-----------|
| [devpi](ct/devpi.sh) | Private PyPI cache / mirror — saves time on CUDA/torch rebuilds | `bash -c "$(curl -fsSL https://gitea.luki-net.org/luki-net/proxmox-scripts/raw/branch/main/ct/devpi.sh)"` |
| [webapp](ct/webapp.sh) | Next.js site with deploy-as-code via a self-hosted Gitea Actions runner (host mode, no inbound port) | `bash -c "$(curl -fsSL https://gitea.luki-net.org/luki-net/proxmox-scripts/raw/branch/main/ct/webapp.sh)"` |
## Usage
Interactive prompts cover CTID, hostname, disk, RAM, CPU, bridge/VLAN, storage, IP/gateway/DNS — plus app-specific values. Defaults are sane. Non-interactive override via env vars:
Run any one-liner on a Proxmox VE host as root (Web UI → Node → Shell works fine). You'll be prompted for CTID, hostname, disk, RAM, CPU, bridge, storage and IP. Defaults are sane.
Non-interactive override via env vars:
```bash
CTID=200 HOSTNAME=devpi DISK_SIZE=30 RAM=4096 CORES=4 IPCFG=dhcp \
bash -c "$(curl -fsSL https://gitea.luki-net.org/luki-net/proxmox-scripts/raw/branch/main/ct/devpi.sh)"
```
All defaults (`DEFAULT_HOSTNAME`, `DEFAULT_DISK`, …) and app config values are settable per call via env vars.
## The pattern
Two files per app, both sourcing the shared libs via `curl`:
- **`ct/<app>.sh`** runs on the PVE host: prompts → unprivileged LXC → pushes a config env file into the container → bootstraps the installer. Apps that need Docker (authentik, runner) enable `nesting+keyctl` automatically.
- **`install/<app>-install.sh`** runs inside the LXC: packages, unprivileged app user, secrets generated on-host (never printed), systemd units, a `/root/<app>.credentials` notes file — then shreds the bootstrap env. Idempotent where it matters: re-runs skip what exists.
Shared libs: [`lib/build.func`](lib/build.func) (host-side: prompts, LXC create, bootstrap) and [`lib/install.func`](lib/install.func) (in-container: apt, users, systemd, http-wait).
## Security conventions
- Unprivileged LXCs only; app processes run as dedicated system users.
- Least privilege everywhere: narrow sudoers (exact-match commands — sudoers compares **verbatim incl. arguments**), DB allowlists, LAN-only binds for inference ports.
- Secrets are generated on the target host and live in `0600` files — never in the repo, the wiki or chat logs.
- **Runner separation:** the nexus runner is repo-scoped and lives on the production LXC *because* it holds deploy rights; the general `runner` LXC is instance-wide *because* it holds none. Don't mix these scopes.
## Contributing
**All changes go through a pull request with cross-review** (Claude Code ↔ Codex, or a human) — no direct pushes to `main`. This rule exists because of two real incidents where an un-reviewed script shipped a missing `source build.func` (see wiki → Lessons). CI (`bash -n`, source-check, validation suite) is being introduced with [PR #5](https://gitea.luki-net.org/luki-net/proxmox-scripts/pulls/5) and runs on the `homelab` runner.
How to add a script: [docs/adding-a-script.md](docs/adding-a-script.md).
All defaults (`DEFAULT_HOSTNAME`, `DEFAULT_DISK`, …) are settable per-call via env vars as well.
## Repo layout
```
.
├── ct/ # Host-side scripts, one per app
├── install/ # In-container installers (+ nexus-runtime.sh re-provisioner)
├── install/ # In-container installers, one per app
├── lib/
│ ├── build.func # Shared host-side helpers (prompts, LXC create, bootstrap)
│ └── install.func # Shared in-container helpers (apt, systemd, users, http-wait)
@@ -69,10 +39,9 @@ How to add a script: [docs/adding-a-script.md](docs/adding-a-script.md).
└── LICENSE
```
## Related
## Adding a new script
- [nexus-hub](https://gitea.luki-net.org/l.kirchner/nexus-hub) — Family Knowledge Hub (main consumer of nexus/nexus-db/authentik/runner)
- [Wiki](https://gitea.luki-net.org/luki-net/proxmox-scripts/wiki) — per-app runbook pointers, conventions, lessons learned
See [docs/adding-a-script.md](docs/adding-a-script.md). Two files per app, both source the shared libs via `curl`.
## License
+155
View File
@@ -0,0 +1,155 @@
#!/usr/bin/env bash
# Allgemeiner Gitea-Actions-Runner — instanzweit, OHNE Deploy-Rechte
#
# Motivation (nexus K-114-Blocker): Der nexus-Runner läuft auf dem
# Produktions-LXC und besitzt sudoers-Deploy-Rechte — er darf deshalb NICHT
# instanzweit registriert werden (jedes Repo könnte sonst Workflows auf der
# Produktionsmaschine ausführen). Dieser LXC ist die saubere Trennung:
# - instanzweite Registrierung (Site Administration → Actions → Runners)
# - Label "homelab:host" (Deploy-Jobs bleiben auf "nexus")
# - KEINE sudoers-Regeln, kein Zugriff auf Produktions-Verzeichnisse
# - Docker via Nesting für Wegwerf-Test-Container (z. B. pgvector in CI)
#
# Sicherheitsmodell: siehe Kopfkommentar in install/runner-install.sh —
# instanzweit + docker-Gruppe heißt: jedes Repo der Instanz kann diesen LXC
# kontrollieren. Akzeptiert, WEIL er nichts besitzt. Nicht auf privilegierte
# LXCs übertragen.
#
# Run on a Proxmox VE host:
# bash -c "$(curl -fsSL https://gitea.luki-net.org/luki-net/proxmox-scripts/raw/branch/main/ct/runner.sh)"
set -euo pipefail
APP="runner"
APP_DESCRIPTION="Allgemeiner Gitea-Actions-Runner (instanzweit, Label homelab, Docker, ohne Deploy-Rechte)"
LIB_URL="${LIB_URL:-https://gitea.luki-net.org/luki-net/proxmox-scripts/raw/branch/main/lib}"
INSTALL_SCRIPT_URL="${INSTALL_SCRIPT_URL:-https://gitea.luki-net.org/luki-net/proxmox-scripts/raw/branch/main/install/runner-install.sh}"
source <(curl -fsSL "$LIB_URL/build.func")
DEFAULT_HOSTNAME="runner"
DEFAULT_DISK="30"
DEFAULT_CORES="4"
DEFAULT_RAM="6144"
# ── Validierung (Review-Finding 80): Werte wandern in die Deploy-Env und in
# systemd/argv — strikte Zeichenklassen, Re-Prompt statt Abbruch.
# Bewusst LOKALE Validatoren: die Libs werden zur Laufzeit von main geladen,
# dieses Script muss aber unabhängig vom Merge-Stand der K-114-Helfer
# (prompt_validated/require_valid) funktionieren. Semantik ist identisch;
# Konsolidierung auf die build.func-Helfer ist als Follow-up notiert. ──────
_valid_url() { [[ "$1" =~ ^https?://[A-Za-z0-9.-]+(:[0-9]{1,5})?$ ]]; }
_valid_token() { [[ "$1" =~ ^[A-Za-z0-9_-]{16,128}$ ]]; }
_valid_word() { [[ "$1" =~ ^[A-Za-z0-9._:,-]+$ ]]; }
_prompt_until_valid() { # var prompt default validator secret(0|1)
local __var="$1" __prompt="$2" __default="$3" __validator="$4" __secret="${5:-0}" __val
while true; do
if [[ "$__secret" == "1" ]]; then
read -rsp "$__prompt" __val || { echo; msg_err "Eingabe abgebrochen (EOF)"; exit 1; }
echo
else
read -rp "$__prompt" __val || { echo; msg_err "Eingabe abgebrochen (EOF)"; exit 1; }
fi
__val="${__val:-$__default}"
if [[ -n "$__val" ]] && "$__validator" "$__val"; then
printf -v "$__var" '%s' "$__val"
return 0
fi
msg_warn "Ungültige Eingabe — bitte erneut (kein Paste mit Sonderzeichen)."
done
}
prompt_app_config() {
echo
echo "── Runner configuration ─────────────────────────────────────"
# env-präsetzte Werte werden validiert (Abbruch bei ungültig — K-114-Konvention),
# interaktive Eingaben re-prompten bis gültig.
if [[ -n "${GITEA_INSTANCE_URL:-}" ]]; then
_valid_url "$GITEA_INSTANCE_URL" || { msg_err "GITEA_INSTANCE_URL (env) ungültig"; exit 1; }
else
_prompt_until_valid GITEA_INSTANCE_URL \
"Gitea instance URL [https://gitea.luki-net.org]: " \
"https://gitea.luki-net.org" _valid_url 0
fi
# WICHTIG: den INSTANZWEITEN Token verwenden
# (Site Administration → Actions → Runners → Create new runner),
# NICHT den Repo-Token — sonst wiederholt sich der K-114-Scope-Blocker.
if [[ -n "${RUNNER_TOKEN:-}" ]]; then
_valid_token "$RUNNER_TOKEN" || { msg_err "RUNNER_TOKEN (env) ungültig (16128 Zeichen [A-Za-z0-9_-])"; exit 1; }
else
_prompt_until_valid RUNNER_TOKEN \
"INSTANZWEITER Runner-Registration-Token: " \
"" _valid_token 1
fi
RUNNER_NAME="${RUNNER_NAME:-$CT_HOSTNAME}"
RUNNER_LABELS="${RUNNER_LABELS:-homelab:host}"
RUNNER_VERSION="${RUNNER_VERSION:-0.2.13}"
NODE_MAJOR="${NODE_MAJOR:-22}"
_valid_word "$RUNNER_NAME" || { msg_err "RUNNER_NAME ungültig"; exit 1; }
_valid_word "$RUNNER_LABELS" || { msg_err "RUNNER_LABELS ungültig"; exit 1; }
[[ "$RUNNER_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { msg_err "RUNNER_VERSION ungültig"; exit 1; }
[[ "$NODE_MAJOR" =~ ^[0-9]+$ ]] || { msg_err "NODE_MAJOR ungültig"; exit 1; }
echo " → instance: $GITEA_INSTANCE_URL"
echo " → runner: $RUNNER_NAME labels: $RUNNER_LABELS (act_runner $RUNNER_VERSION, host mode, scope: INSTANZ)"
}
push_app_config() {
msg_info "Pushing runner config into container..."
local tmpf; tmpf=$(mktemp)
# Werte sind oben strikt validiert (keine Quotes/Whitespace möglich) —
# damit ist die env-Datei frei von Quoting-/Injection-Fallen (vgl. Finding 76).
cat >"$tmpf" <<EOF
GITEA_INSTANCE_URL=$GITEA_INSTANCE_URL
RUNNER_TOKEN=$RUNNER_TOKEN
RUNNER_NAME=$RUNNER_NAME
RUNNER_LABELS=$RUNNER_LABELS
RUNNER_VERSION=$RUNNER_VERSION
NODE_MAJOR=$NODE_MAJOR
EOF
pct push "$CTID" "$tmpf" /root/runner.deploy.env --perms 600
rm -f "$tmpf"
}
# Docker im unprivilegierten LXC braucht nesting+keyctl (Test-Container in CI).
enable_nesting() {
msg_info "Enabling nesting+keyctl features (Docker für CI-Test-Container)..."
pct set "$CTID" --features nesting=1,keyctl=1
pct reboot "$CTID"
for _ in $(seq 1 30); do
pct exec "$CTID" -- true >/dev/null 2>&1 && break
sleep 2
done
msg_ok "Container restarted with nesting enabled"
}
print_app_summary() {
local runner_state
runner_state=$(pct exec "$CTID" -- systemctl is-active act-runner.service 2>/dev/null | tr -d '\r\n')
cat <<EOF
Gitea-Actions-Runner: $RUNNER_NAME [$RUNNER_LABELS] — $runner_state
Scope: INSTANZWEIT — bedient alle Repos der Instanz
Mode: host (Docker verfügbar für Test-Container)
Sicherheit: kein sudoers, keine Deploy-Rechte, keine Produktions-Mounts
(Modell: siehe install/runner-install.sh Kopfkommentar)
Verify: $GITEA_INSTANCE_URL → Site Administration → Actions → Runners
Workflows anderer Repos nutzen: runs-on: ${RUNNER_LABELS%%:*}
(Deploy-Jobs von nexus bleiben auf dem nexus-LXC-Runner, Label "nexus".)
Logs: pct exec $CTID -- journalctl -u act-runner -f
EOF
}
trap _on_error ERR
preflight_pve
show_header "$APP" "$APP_DESCRIPTION"
prompt_lxc_config
prompt_app_config
resolve_debian_template
create_lxc
enable_nesting
push_app_config
bootstrap_install_script "$INSTALL_SCRIPT_URL"
print_summary
+196
View File
@@ -0,0 +1,196 @@
#!/usr/bin/env bash
# Runner installer — runs inside the LXC, called by ct/runner.sh
#
# Allgemeiner, INSTANZWEITER Gitea-Actions-Runner ohne Deploy-Rechte:
# - Node.js (für actions/checkout im Host-Mode) + git + rsync
# - Docker (für Wegwerf-Test-Container in CI, z. B. pgvector)
# - act_runner als unprivilegierter User "runner" in der docker-Gruppe
# - systemd-Unit act-runner.service (Requires=docker.service)
#
# SICHERHEITSMODELL (Review-Finding 88, bewusst akzeptiert & dokumentiert):
# docker-Gruppe == de-facto root IN DIESEM LXC. Da der Runner instanzweit
# ist, kann jedes Repo der Gitea-Instanz via Workflow den Runner-Container
# vollständig kontrollieren. Das ist hier akzeptiert, weil (a) alle Repos
# der Instanz vom selben Admin (Lutz) stammen — kein Multi-Tenant — und
# (b) dieser LXC GENAU DESHALB nichts besitzt: keine sudoers, keine
# Produktions-Mounts, keine Secrets außer dem (geshredderten) Reg-Token.
# Bei Öffnung der Instanz für Dritte: Docker rootless oder eigener Runner
# pro Vertrauenszone. NICHT auf LXCs mit Deploy-Rechten übertragen.
#
# Idempotent: erneuter Lauf (auch OHNE /root/runner.deploy.env) überspringt
# Vorhandenes und provisioniert nur nach — Registrierung braucht die env-Datei
# nur beim Erstlauf (Finding 90).
set -euo pipefail
LIB_URL="${LIB_URL:-https://gitea.luki-net.org/luki-net/proxmox-scripts/raw/branch/main/lib}"
source <(curl -fsSL "$LIB_URL/install.func")
[[ "$EUID" -eq 0 ]] || { msg_err "Must run as root"; exit 1; }
APP_USER="runner"
APP_HOME="/opt/runner"
RUNNER_DIR="$APP_HOME/data"
# ── Konfiguration laden (optional bei Re-Run, Finding 90) ─────────────────────
CONF="/root/runner.deploy.env"
if [[ -f "$CONF" ]]; then
set -a; . "$CONF"; set +a
fi
RUNNER_NAME="${RUNNER_NAME:-$(hostname)}"
RUNNER_LABELS="${RUNNER_LABELS:-homelab:host}"
RUNNER_VERSION="${RUNNER_VERSION:-0.2.13}"
NODE_MAJOR="${NODE_MAJOR:-22}"
# ── Validierung (Finding 80): Werte landen in Shell-Fragmenten/systemd —
# strikte Zeichenklassen statt Vertrauen. Re-Runs ohne CONF validieren
# nur, was gesetzt ist; Registrierungs-Pflichtwerte prüft der Reg-Block. ──
valid_url() { [[ "$1" =~ ^https?://[A-Za-z0-9.-]+(:[0-9]{1,5})?$ ]]; }
valid_token() { [[ "$1" =~ ^[A-Za-z0-9_-]{16,128}$ ]]; }
valid_token_word() { [[ "$1" =~ ^[A-Za-z0-9._:,-]+$ ]]; }
[[ -z "${GITEA_INSTANCE_URL:-}" ]] || valid_url "$GITEA_INSTANCE_URL" \
|| { msg_err "GITEA_INSTANCE_URL ungültig: nur http(s)://host[:port]"; exit 1; }
[[ -z "${RUNNER_TOKEN:-}" ]] || valid_token "$RUNNER_TOKEN" \
|| { msg_err "RUNNER_TOKEN ungültig (erwartet 16128 Zeichen [A-Za-z0-9_-])"; exit 1; }
valid_token_word "$RUNNER_NAME" || { msg_err "RUNNER_NAME enthält unzulässige Zeichen"; exit 1; }
valid_token_word "$RUNNER_LABELS" || { msg_err "RUNNER_LABELS enthält unzulässige Zeichen"; exit 1; }
[[ "$RUNNER_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { msg_err "RUNNER_VERSION ungültig"; exit 1; }
[[ "$NODE_MAJOR" =~ ^[0-9]+$ ]] || { msg_err "NODE_MAJOR ungültig"; exit 1; }
# Finding 85 (Re-Review): act_runner schreibt .runner ins CWD — run_user wechselt
# deshalb hart ins RUNNER_DIR (env -C), sonst landet die Registrierung in / und
# der unprivilegierte User darf dort nicht schreiben.
run_user() { runuser -u "$APP_USER" -- env -C "$RUNNER_DIR" HOME="$APP_HOME" "$@"; }
# ── Pakete: git/rsync, Node (checkout-Action), Docker (Test-Container) ────────
setup_base_apt git rsync ca-certificates curl
NODE_HAVE="$(command -v node >/dev/null 2>&1 && node -v | sed -E 's/^v([0-9]+).*/\1/' || echo 0)"
if [[ "$NODE_HAVE" != "$NODE_MAJOR" ]]; then
msg_info "Installing Node.js ${NODE_MAJOR}.x (NodeSource)..."
curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | bash - >/dev/null
apt-get install -y -qq nodejs >/dev/null
msg_ok "Node $(node -v) installed"
else
msg_warn "Node $(node -v) already present, skipping"
fi
if ! command -v docker >/dev/null 2>&1; then
msg_info "Installing Docker (get.docker.com)..."
curl -fsSL https://get.docker.com | sh >/dev/null
msg_ok "Docker $(docker --version | awk '{print $3}' | tr -d ',')"
else
msg_warn "Docker already present, skipping"
fi
# ── act_runner binary ─────────────────────────────────────────────────────────
if [[ ! -x /usr/local/bin/act_runner ]]; then
ARCH="$(dpkg --print-architecture)"
case "$ARCH" in amd64|arm64) ;; *) msg_err "unsupported arch: $ARCH"; exit 1 ;; esac
msg_info "Downloading act_runner $RUNNER_VERSION ($ARCH)..."
curl -fsSL "https://dl.gitea.com/act_runner/${RUNNER_VERSION}/act_runner-${RUNNER_VERSION}-linux-${ARCH}" \
-o /usr/local/bin/act_runner
chmod +x /usr/local/bin/act_runner
msg_ok "act_runner $(/usr/local/bin/act_runner --version 2>/dev/null | head -n1)"
else
msg_warn "act_runner already present, skipping download"
fi
# ── User (docker-Gruppe VOR dem Daemon-Start — Lesson aus nexus K-103:
# Gruppenmitgliedschaften werden beim Prozessstart eingefroren) ─────────────
create_system_user "$APP_USER" "$APP_HOME"
usermod -aG docker "$APP_USER"
mkdir -p "$RUNNER_DIR"
chown -R "$APP_USER:$APP_USER" "$APP_HOME"
# ── Registrierung (idempotent; braucht CONF-Werte nur beim Erstlauf) ──────────
if [[ ! -f "$RUNNER_DIR/.runner" ]]; then
: "${GITEA_INSTANCE_URL:?missing GITEA_INSTANCE_URL (Erstlauf braucht /root/runner.deploy.env)}"
: "${RUNNER_TOKEN:?missing RUNNER_TOKEN (Erstlauf braucht /root/runner.deploy.env)}"
msg_info "Registering runner '$RUNNER_NAME' [$RUNNER_LABELS] with $GITEA_INSTANCE_URL (instance scope)..."
# Werte sind oben strikt validiert (keine Quotes/Whitespace möglich) —
# Übergabe als argv an runuser, keine erneute Shell-Interpolation (vgl. Finding 76).
# CWD = RUNNER_DIR via run_user (Finding 85).
run_user /usr/local/bin/act_runner register \
--no-interactive \
--config /dev/null \
--instance "$GITEA_INSTANCE_URL" \
--token "$RUNNER_TOKEN" \
--name "$RUNNER_NAME" \
--labels "$RUNNER_LABELS" \
2>&1 | sed "s#$RUNNER_TOKEN#<redacted>#g" || { msg_err "Registrierung fehlgeschlagen"; exit 1; }
# Belt-and-suspenders: falls eine künftige act_runner-Version doch ins HOME schreibt
if [[ -f "$APP_HOME/.runner" && ! -f "$RUNNER_DIR/.runner" ]]; then
mv "$APP_HOME/.runner" "$RUNNER_DIR/.runner"
fi
[[ -f "$RUNNER_DIR/.runner" ]] || { msg_err ".runner nach Registrierung nicht gefunden"; exit 1; }
chown -R "$APP_USER:$APP_USER" "$RUNNER_DIR"
msg_ok "Runner registered"
else
msg_warn "Runner already registered (.runner exists), skipping"
fi
# ── SSH-Root-Login-Policy anwenden, falls vom Host-Script übergeben
# (Funktion existiert in install.func ab K-114/PR #5 — guarded Aufruf,
# damit dieser Branch vor und nach dem Merge funktioniert) ────────────────
if declare -F configure_ssh_root_login >/dev/null 2>&1; then
configure_ssh_root_login
fi
# ── systemd-Unit (bewusst KEINE sudoers-Datei; Findings 82 + 70) ──────────────
cat >/etc/systemd/system/act-runner.service <<EOF
[Unit]
Description=Gitea Actions runner (instance-wide, label ${RUNNER_LABELS%%:*}, no deploy rights)
After=network-online.target docker.service
Wants=network-online.target
# Finding 82: ohne Docker keine Jobs annehmen — harte Kopplung
Requires=docker.service
[Service]
Type=simple
User=$APP_USER
Group=$APP_USER
WorkingDirectory=$RUNNER_DIR
Environment=HOME=$APP_HOME
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
ExecStart=/usr/local/bin/act_runner daemon
Restart=on-failure
RestartSec=5
# Finding 70: Basis-Härtung (docker-CLI über Socket bleibt funktional)
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now act-runner.service
msg_ok "act-runner.service installed + started"
CRED_FILE="/root/runner.credentials"
cat >"$CRED_FILE" <<EOF
Allgemeiner Gitea-Actions-Runner (instanzweit)
Instance: ${GITEA_INSTANCE_URL:-<bestehende Registrierung, siehe $RUNNER_DIR/.runner>}
Name/Label: $RUNNER_NAME [$RUNNER_LABELS]
User: $APP_USER (docker-Gruppe, KEIN sudo)
Workdir: $RUNNER_DIR
Logs: journalctl -u act-runner -f
Repos nutzen ihn mit: runs-on: ${RUNNER_LABELS%%:*}
SICHERHEITSMODELL: docker-Gruppe == de-facto root in DIESEM LXC; instanzweit
heißt: jedes Repo der Instanz kann den Runner-LXC kontrollieren. Akzeptiert,
weil Single-Admin-Instanz und dieser LXC nichts besitzt (keine sudoers, keine
Produktions-Mounts). Deploy-Jobs gehören NICHT hierher — die bleiben auf dem
repo-scoped nexus-Runner. Trennung beibehalten.
EOF
chmod 600 "$CRED_FILE"
if [[ -f "$CONF" ]]; then
shred -u "$CONF" 2>/dev/null || rm -f "$CONF"
fi
apt_cleanup
msg_ok "runner installation finished"