Compare commits
12
Commits
e545a27e9a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c07727d35 | ||
|
|
e5d59473d2 | ||
|
|
9927e4dec1 | ||
|
|
8299431aed | ||
|
|
d86848eb71 | ||
|
|
936ab2c2fb | ||
|
|
3e4ede907e | ||
|
|
fef37d2676 | ||
|
|
82540472ae | ||
|
|
294960f44d | ||
|
|
51f2474db6 | ||
|
|
d0138b742d |
@@ -12,21 +12,17 @@ These scripts automate the deployment of personal infrastructure components. The
|
||||
- **Multi-OS**: Supports Debian and Alpine-based deployments, chosen per-script based on that service's requirements
|
||||
- **Loopback by default**: Services bind to `127.0.0.1`; Tailscale handles the reverse proxy and TLS termination
|
||||
- **Log hygiene**: Every long-running service ships with a `logrotate` config (no unbounded log files)
|
||||
- **Console auto-login**: Proxmox LXCs are configured for root auto-login on `tty1` (fast `pct enter` and Web UI shell access)
|
||||
- **Keep it simple**: One script per service, plain bash, no frameworks — readability over cleverness
|
||||
|
||||
### Available Scripts
|
||||
|
||||
| Script | Description | Usage |
|
||||
| ------------------------------------------ | ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
||||
| [`proxy/install.sh`](proxy/) | Reverse proxy with Tailscale + Nginx Proxy Manager | `curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/proxy/install.sh` \| `bash` |
|
||||
| [`netlab/install.sh`](netlab/) | Network lab with ContainerLab | `curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/netlab/install.sh` \| `bash` |
|
||||
| [`gitea-runner/install.sh`](gitea-runner/) | Gitea Act Runner on Alpine LXC (Proxmox) | `bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/gitea-runner/install.sh)"` |
|
||||
| [`openbao/install.sh`](openbao/) | OpenBao secrets manager on Alpine LXC (Proxmox) | `bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/openbao/install.sh)"` |
|
||||
| [`komodo/install.sh`](komodo/) | Komodo (Docker + MongoDB) on Alpine VM | `bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/komodo/install.sh)"` |
|
||||
| Script | Description | Usage |
|
||||
| -------------------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
||||
| [`netlab/install.sh`](netlab/) | Network lab with ContainerLab | `curl -fsSL https://raw.githubusercontent.com/darnodo/infra-scripts/main/netlab/install.sh` \| `bash` |
|
||||
| [`komodo/install.sh`](komodo/) | Komodo (Docker + MongoDB) on Alpine VM | `bash -c "$(curl -fsSL https://raw.githubusercontent.com/darnodo/infra-scripts/main/komodo/install.sh)"` |
|
||||
|
||||
### Requirements
|
||||
|
||||
- Fresh Debian 12/13 installation (proxy, netlab) or Proxmox VE host (gitea-runner, openbao) or Alpine VM (komodo)
|
||||
- User with sudo privileges (do not run as root) — except gitea-runner, openbao, and komodo which run as root
|
||||
- Fresh Debian 12/13 installation (netlab) or Alpine VM (komodo)
|
||||
- User with sudo privileges (do not run as root) — except komodo, which runs as root
|
||||
- Internet access
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
# Gitea Act Runner
|
||||
|
||||
Automated installation script for a Gitea Actions runner in an Alpine LXC on Proxmox.
|
||||
|
||||
### Features
|
||||
|
||||
Single script, three automatic modes:
|
||||
|
||||
| Context | Action |
|
||||
|---------|--------|
|
||||
| From Proxmox host | Creates Alpine LXC + installs everything |
|
||||
| From empty LXC | Installs Docker + act_runner + OpenRC service |
|
||||
| From LXC with act_runner installed | Updates binary to latest version |
|
||||
|
||||
### Usage
|
||||
|
||||
#### Full install (from Proxmox shell)
|
||||
|
||||
```bash
|
||||
bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/gitea-runner/install.sh)"
|
||||
```
|
||||
|
||||
The script automatically creates an Alpine 3.23 LXC with Docker and act_runner.
|
||||
|
||||
#### Customization
|
||||
|
||||
Environment variables to override defaults:
|
||||
|
||||
```bash
|
||||
CTID=120 HOSTNAME=runner-02 CORES=4 RAM=4096 bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/gitea-runner/install.sh)"
|
||||
```
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `CTID` | auto | Container ID |
|
||||
| `RUNNER_HOSTNAME` | `gitea-runner` | LXC Hostname |
|
||||
| `CORES` | `2` | CPU cores |
|
||||
| `RAM` | `2048` | RAM in MiB |
|
||||
| `DISK` | `8` | Disk in GB |
|
||||
| `STORAGE` | `local-lvm` | Proxmox storage for the LXC |
|
||||
| `BRIDGE` | `vmbr0` | Network bridge |
|
||||
|
||||
#### Runner registration
|
||||
|
||||
After installation, enter the LXC and register the runner:
|
||||
|
||||
```bash
|
||||
pct enter <CTID>
|
||||
cd /var/lib/gitea-runner
|
||||
su -s /bin/bash gitea-runner -c "act_runner register"
|
||||
rc-service gitea-runner start
|
||||
```
|
||||
|
||||
#### Update
|
||||
|
||||
From inside the LXC:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/gitea-runner/install.sh | bash
|
||||
```
|
||||
|
||||
The script detects that act_runner is already installed and switches to update mode automatically.
|
||||
|
||||
### Architecture
|
||||
|
||||
- **OS**: Alpine 3.23 (LXC non-privileged, nesting active)
|
||||
- **Docker**: installed via apk, OpenRC service
|
||||
- **act_runner**: official binary from gitea.com/gitea/act_runner
|
||||
- **Service**: OpenRC with logs in `/var/log/gitea-runner.log`
|
||||
- **User**: `gitea-runner` (group `docker`)
|
||||
@@ -1,323 +0,0 @@
|
||||
#!/bin/bash
|
||||
# install.sh - Gitea Act Runner: LXC creation, installation & update
|
||||
# Usage:
|
||||
# From Proxmox host : curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/gitea-runner/install.sh | bash
|
||||
# From inside LXC : bash /root/install.sh (updates act_runner binary)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# --- Config (override via environment) ---
|
||||
CTID="${CTID:-}"
|
||||
HOSTNAME="${RUNNER_HOSTNAME:-gitea-runner}"
|
||||
TEMPLATE="${TEMPLATE:-alpine-3.23-default_20260116_amd64.tar.xz}"
|
||||
STORAGE="${STORAGE:-local-lvm}"
|
||||
TEMPLATE_STORAGE="${TEMPLATE_STORAGE:-local}"
|
||||
CORES="${CORES:-2}"
|
||||
RAM="${RAM:-2048}"
|
||||
DISK="${DISK:-8}"
|
||||
BRIDGE="${BRIDGE:-vmbr0}"
|
||||
SCRIPT_URL="https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/gitea-runner/install.sh"
|
||||
GITEA_HOSTNAME="${GITEA_HOSTNAME:-gitea.taila5ad8.ts.net}"
|
||||
GITEA_API="https://gitea.com/api/v1/repos/gitea/act_runner/releases"
|
||||
VERSION_FILE="/opt/gitea-runner_version.txt"
|
||||
|
||||
# --- Colors ---
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
|
||||
# --- Helpers ---
|
||||
get_latest_release() {
|
||||
local release
|
||||
release=$(curl -fsSL "$GITEA_API" | jq -r '.[0].tag_name')
|
||||
if [[ -z "$release" || "$release" == "null" ]]; then
|
||||
log_error "Failed to fetch latest release from Gitea API"
|
||||
exit 1
|
||||
fi
|
||||
echo "$release"
|
||||
}
|
||||
|
||||
get_arch() {
|
||||
local arch
|
||||
arch=$(uname -m)
|
||||
case "$arch" in
|
||||
x86_64) echo "amd64" ;;
|
||||
aarch64) echo "arm64" ;;
|
||||
armv7l) echo "armv7" ;;
|
||||
*) log_error "Unsupported architecture: $arch"; exit 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
download_runner() {
|
||||
local release="$1"
|
||||
local version="${release#v}"
|
||||
local arch
|
||||
arch=$(get_arch)
|
||||
local url="https://gitea.com/gitea/act_runner/releases/download/${release}/gitea-runner-${version}-linux-${arch}"
|
||||
|
||||
log_info "Downloading act_runner ${release} (${arch})..."
|
||||
curl -fsSL "$url" -o /usr/local/bin/act_runner
|
||||
chmod +x /usr/local/bin/act_runner
|
||||
ln -sf /usr/local/bin/act_runner /usr/bin/act_runner
|
||||
echo "$release" > "$VERSION_FILE"
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# MODE 1: Proxmox host — create LXC container
|
||||
# ============================================================
|
||||
create_lxc() {
|
||||
log_info "=== Gitea Act Runner — LXC Creation ==="
|
||||
|
||||
# Auto-select next CTID if not specified
|
||||
if [[ -z "$CTID" ]]; then
|
||||
CTID=$(pvesh get /cluster/resources --type vm --output-format json 2>/dev/null \
|
||||
| jq '[.[].vmid] | max + 1' 2>/dev/null || echo "100")
|
||||
log_info "Auto-selected CTID: $CTID"
|
||||
fi
|
||||
|
||||
# Download template if needed
|
||||
if ! pveam list "$TEMPLATE_STORAGE" 2>/dev/null | grep -q "$TEMPLATE"; then
|
||||
log_info "Downloading template $TEMPLATE..."
|
||||
pveam download "$TEMPLATE_STORAGE" "$TEMPLATE"
|
||||
fi
|
||||
|
||||
log_info "Creating LXC $CTID ($HOSTNAME)..."
|
||||
pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" \
|
||||
--hostname "$HOSTNAME" \
|
||||
--cores "$CORES" \
|
||||
--memory "$RAM" \
|
||||
--rootfs "${STORAGE}:${DISK}" \
|
||||
--net0 "name=eth0,bridge=${BRIDGE},ip=dhcp" \
|
||||
--unprivileged 1 \
|
||||
--features nesting=1,keyctl=1 \
|
||||
--tags "infra-script,cicd" \
|
||||
--start 0
|
||||
|
||||
log_info "Configuring LXC for Docker and Tailscale..."
|
||||
cat >> "/etc/pve/lxc/${CTID}.conf" <<EOF
|
||||
lxc.apparmor.profile: unconfined
|
||||
lxc.cgroup2.devices.allow: c 10:200 rwm
|
||||
lxc.mount.entry: /dev/net dev/net none bind,create=dir
|
||||
lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file
|
||||
EOF
|
||||
|
||||
log_info "Starting LXC $CTID..."
|
||||
pct start "$CTID"
|
||||
sleep 5
|
||||
|
||||
log_info "Injecting install script into container..."
|
||||
pct exec "$CTID" -- sh -c "apk add --no-cache bash curl jq > /dev/null 2>&1"
|
||||
curl -fsSL "$SCRIPT_URL" | pct exec "$CTID" -- bash -s -- --install
|
||||
|
||||
local ip
|
||||
ip=$(pct exec "$CTID" -- ip -4 addr show eth0 2>/dev/null | awk '/inet /{print $2}' | cut -d/ -f1)
|
||||
|
||||
echo ""
|
||||
log_info "========================================="
|
||||
log_info "LXC $CTID created successfully!"
|
||||
log_info "========================================="
|
||||
echo ""
|
||||
echo " Hostname : $HOSTNAME"
|
||||
echo " IP : ${ip:-pending}"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " pct enter $CTID"
|
||||
echo " cd /var/lib/gitea-runner"
|
||||
echo " su -s /bin/bash gitea-runner -c 'act_runner register'"
|
||||
echo " rc-service gitea-runner start"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# MODE 2: Inside LXC — fresh install
|
||||
# ============================================================
|
||||
install_runner() {
|
||||
log_info "=== Gitea Act Runner — Installation ==="
|
||||
|
||||
log_info "Updating system..."
|
||||
apk update > /dev/null && apk upgrade > /dev/null
|
||||
|
||||
log_info "Installing dependencies..."
|
||||
apk add --no-cache curl jq tar bash docker docker-cli-compose > /dev/null
|
||||
|
||||
log_info "Installing Tailscale..."
|
||||
apk add --no-cache tailscale > /dev/null
|
||||
rc-update add tailscale default > /dev/null 2>&1
|
||||
rc-service tailscale start > /dev/null 2>&1
|
||||
|
||||
log_info "Starting Docker..."
|
||||
rc-update add docker default > /dev/null 2>&1
|
||||
rc-service docker start > /dev/null 2>&1
|
||||
|
||||
local release
|
||||
release=$(get_latest_release)
|
||||
download_runner "$release"
|
||||
log_info "act_runner $(act_runner --version 2>&1 || true)"
|
||||
|
||||
log_info "Creating gitea-runner user..."
|
||||
adduser -S -D -H -h /var/lib/gitea-runner -s /bin/bash -G docker gitea-runner 2>/dev/null || true
|
||||
addgroup gitea-runner docker 2>/dev/null || true
|
||||
mkdir -p /var/lib/gitea-runner
|
||||
chown -R gitea-runner:docker /var/lib/gitea-runner
|
||||
|
||||
log_info "Generating act_runner config with Prometheus metrics enabled..."
|
||||
act_runner generate-config > /var/lib/gitea-runner/config.yaml
|
||||
sed -i '/^metrics:/,/enabled:/{s/enabled: false/enabled: true/}' /var/lib/gitea-runner/config.yaml
|
||||
chown gitea-runner:docker /var/lib/gitea-runner/config.yaml
|
||||
chmod 640 /var/lib/gitea-runner/config.yaml
|
||||
|
||||
log_info "Creating OpenRC service..."
|
||||
cat <<'EOF' > /etc/init.d/gitea-runner
|
||||
#!/sbin/openrc-run
|
||||
|
||||
name="Gitea Act Runner"
|
||||
description="Gitea Actions Runner Daemon"
|
||||
command="/usr/local/bin/act_runner"
|
||||
command_args="daemon --config /var/lib/gitea-runner/config.yaml"
|
||||
command_user="gitea-runner:docker"
|
||||
command_background=true
|
||||
pidfile="/run/${RC_SVCNAME}.pid"
|
||||
directory="/var/lib/gitea-runner"
|
||||
|
||||
output_log="/var/log/gitea-runner.log"
|
||||
error_log="/var/log/gitea-runner.log"
|
||||
|
||||
depend() {
|
||||
need net docker tailscale
|
||||
after docker tailscale
|
||||
}
|
||||
|
||||
start_pre() {
|
||||
export PATH="/usr/local/bin:$PATH"
|
||||
checkpath --directory --owner gitea-runner:docker --mode 0755 /var/lib/gitea-runner
|
||||
checkpath --file --owner gitea-runner:docker --mode 0644 /var/log/gitea-runner.log
|
||||
|
||||
local timeout=30
|
||||
local elapsed=0
|
||||
ebegin "Waiting for Tailscale MagicDNS to resolve __GITEA_HOSTNAME__"
|
||||
while ! getent hosts "__GITEA_HOSTNAME__" > /dev/null 2>&1; do
|
||||
if [ "$elapsed" -ge "$timeout" ]; then
|
||||
eend 1
|
||||
eerror "Timed out after ${timeout}s waiting for MagicDNS resolution of __GITEA_HOSTNAME__"
|
||||
return 1
|
||||
fi
|
||||
sleep 1
|
||||
elapsed=$(( elapsed + 1 ))
|
||||
done
|
||||
eend 0
|
||||
}
|
||||
EOF
|
||||
sed -i "s/__GITEA_HOSTNAME__/${GITEA_HOSTNAME}/g" /etc/init.d/gitea-runner
|
||||
chmod +x /etc/init.d/gitea-runner
|
||||
rc-update add gitea-runner default > /dev/null
|
||||
|
||||
log_info "Configuring logrotate for gitea-runner..."
|
||||
apk add --no-cache logrotate > /dev/null
|
||||
|
||||
cat > /etc/logrotate.d/gitea-runner << 'LOGROTATE'
|
||||
/var/log/gitea-runner.log {
|
||||
daily
|
||||
rotate 7
|
||||
compress
|
||||
missingok
|
||||
notifempty
|
||||
copytruncate
|
||||
}
|
||||
LOGROTATE
|
||||
|
||||
ln -sf /usr/sbin/logrotate /etc/periodic/daily/logrotate 2>/dev/null || true
|
||||
|
||||
log_info "Enabling console auto-login on tty1..."
|
||||
# Alpine ships busybox getty by default; agetty (from util-linux) is what
|
||||
# supports --autologin.
|
||||
apk add --no-cache agetty >/dev/null 2>&1 || apk add --no-cache util-linux >/dev/null
|
||||
|
||||
# Replace any existing tty1 entry, then append our autologin line. Doing it
|
||||
# in two steps (delete + append) is more robust than an in-place sed against
|
||||
# a pattern that may drift across Alpine releases.
|
||||
sed -i '/^tty1::/d' /etc/inittab
|
||||
echo 'tty1::respawn:/sbin/agetty --autologin root --noclear 38400 tty1' >> /etc/inittab
|
||||
|
||||
# Tell PID 1 to re-read /etc/inittab so the change takes effect without a reboot.
|
||||
kill -HUP 1 2>/dev/null || true
|
||||
|
||||
# Kick any getty/agetty still attached to tty1 so init respawns it *now* with
|
||||
# the new line — otherwise the first web-console session lands on the stale
|
||||
# process and the operator has to type `exit` once before autologin kicks in.
|
||||
pkill -KILL -f '(getty|agetty).*tty1' 2>/dev/null || true
|
||||
|
||||
log_info "Cleaning up..."
|
||||
rm -rf /var/cache/apk/*
|
||||
|
||||
echo ""
|
||||
log_info "========================================="
|
||||
log_info "Installation complete!"
|
||||
log_info "========================================="
|
||||
echo ""
|
||||
echo "Connect to Tailscale first:"
|
||||
echo " tailscale up --ssh"
|
||||
echo ""
|
||||
echo "Register the runner:"
|
||||
echo " cd /var/lib/gitea-runner"
|
||||
echo " su -s /bin/bash gitea-runner -c 'act_runner register'"
|
||||
echo " rc-service gitea-runner start"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# MODE 3: Inside LXC — update binary
|
||||
# ============================================================
|
||||
update_runner() {
|
||||
log_info "=== Gitea Act Runner — Update ==="
|
||||
|
||||
local release
|
||||
release=$(get_latest_release)
|
||||
|
||||
if [[ -f "$VERSION_FILE" && "$release" == "$(cat "$VERSION_FILE")" ]]; then
|
||||
log_info "Already at latest version: $release"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
local current
|
||||
current=$(cat "$VERSION_FILE" 2>/dev/null || echo "unknown")
|
||||
log_info "Updating: $current → $release"
|
||||
|
||||
log_info "Stopping service..."
|
||||
rc-service gitea-runner stop 2>/dev/null || true
|
||||
|
||||
log_info "Backing up current binary..."
|
||||
cp /usr/local/bin/act_runner "/usr/local/bin/act_runner.bak.$(date +%s)"
|
||||
|
||||
download_runner "$release"
|
||||
|
||||
log_info "Starting service..."
|
||||
rc-service gitea-runner start
|
||||
|
||||
log_info "Updated to $release"
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# Main — detect context
|
||||
# ============================================================
|
||||
main() {
|
||||
if [[ "${1:-}" == "--install" ]]; then
|
||||
# Explicitly called in install mode (from pct exec)
|
||||
install_runner
|
||||
elif command -v pct &> /dev/null; then
|
||||
# We're on the Proxmox host
|
||||
create_lxc
|
||||
elif [[ -f /usr/local/bin/act_runner ]]; then
|
||||
# act_runner exists — update mode
|
||||
update_runner
|
||||
else
|
||||
# Fresh LXC — install mode
|
||||
install_runner
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
+1
-1
@@ -32,7 +32,7 @@ script can just run `apk add docker` and let dockerd own the kernel namespace.
|
||||
#### Install / update
|
||||
|
||||
```bash
|
||||
bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/komodo/install.sh)"
|
||||
bash -c "$(curl -fsSL https://raw.githubusercontent.com/darnodo/infra-scripts/main/komodo/install.sh)"
|
||||
```
|
||||
|
||||
The script prints the generated `KOMODO_DATABASE_PASSWORD`, `KOMODO_WEBHOOK_SECRET`, and
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
# lib/common.sh - Shared helpers for Proxmox LXC creator scripts.
|
||||
#
|
||||
# Sourced (not executed) by openbao/install.sh and gitea-runner/install.sh.
|
||||
# Assumes the sourcing script already defines log_info/log_warn/log_error
|
||||
# (both scripts do, identically) — this file does not redefine them.
|
||||
#
|
||||
# Contract for future LXC creator scripts:
|
||||
# - detect_latest_alpine_template(): Alpine only. A future Debian-based
|
||||
# script needs its own detect_latest_debian_template() (same pattern:
|
||||
# pveam available + sort -V + hardcoded fallback) — do not overload
|
||||
# this function with an OS parameter.
|
||||
# - enable_tty1_autologin(): implements the Alpine/OpenRC autologin
|
||||
# mechanism (inittab + agetty). A future Debian-based script needs a
|
||||
# distinct function (systemd container-getty override) rather than a
|
||||
# branch inside this one.
|
||||
# - find_existing_lxc(): OS-agnostic, works by tag/hostname via `pct
|
||||
# config`. Reusable as-is by any LXC creator script.
|
||||
# - refresh_os_packages(): Alpine only (apk update && apk upgrade). A
|
||||
# future Debian-based script needs its own apt-get variant.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ============================================================
|
||||
# #12 - Detect newest Alpine LXC template available from the Proxmox repos.
|
||||
# Echoes the template filename. Falls back to a hardcoded known-good
|
||||
# template if `pveam` is unavailable or returns nothing.
|
||||
# ============================================================
|
||||
detect_latest_alpine_template() {
|
||||
:
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# #14 - Enable root auto-login on tty1 for an Alpine/OpenRC LXC.
|
||||
# Idempotent: safe to call on every install/update.
|
||||
# ============================================================
|
||||
enable_tty1_autologin() {
|
||||
:
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# #15 - Find an existing LXC by tag or hostname (host-side, requires pct).
|
||||
# Echoes the CTID on match, returns 1 if none found.
|
||||
#
|
||||
# Expects HOSTNAME_LXC and LXC_TAG to be set by the caller (as openbao's
|
||||
# find_existing_lxc already does).
|
||||
# ============================================================
|
||||
find_existing_lxc() {
|
||||
:
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# #15 - Refresh OS packages (Alpine: apk update && apk upgrade).
|
||||
# Callable both host-side (via pct exec) and inside the LXC.
|
||||
# ============================================================
|
||||
refresh_os_packages() {
|
||||
:
|
||||
}
|
||||
+2
-2
@@ -5,7 +5,7 @@ Deploys a network lab server with ContainerLab for network simulation and testin
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/netlab/install.sh | bash
|
||||
curl -fsSL https://raw.githubusercontent.com/darnodo/infra-scripts/main/netlab/install.sh | bash
|
||||
```
|
||||
|
||||
## Components
|
||||
@@ -27,7 +27,7 @@ curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/netlab/i
|
||||
Example:
|
||||
|
||||
```bash
|
||||
NETLAB_HOSTNAME=clab01 SSH_PORT=22222 curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/netlab/install.sh | bash
|
||||
NETLAB_HOSTNAME=clab01 SSH_PORT=22222 curl -fsSL https://raw.githubusercontent.com/darnodo/infra-scripts/main/netlab/install.sh | bash
|
||||
```
|
||||
|
||||
## Network Access
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
# OpenBao
|
||||
|
||||
Automated installation and update script for an [OpenBao](https://openbao.org) secrets-manager
|
||||
server running inside an Alpine LXC on Proxmox.
|
||||
|
||||
### Features
|
||||
|
||||
Single script, automatic mode selection:
|
||||
|
||||
| Context | Action |
|
||||
| ---------------------------------------------------- | --------------------------------------------------------------------------------- |
|
||||
| From Proxmox host, no existing OpenBao container | Detects newest Alpine template, creates LXC, installs `bao` + OpenRC service |
|
||||
| From Proxmox host, OpenBao container already present | Reuses the existing LXC, refreshes packages, upgrades `bao` to the latest release |
|
||||
| From inside an LXC, no `bao` binary | Installs OpenBao from scratch |
|
||||
| From inside an LXC, `bao` already present | Updates the binary only (no config / data changes) |
|
||||
|
||||
The container is identified by hostname **and** the `openbao` tag, so it is
|
||||
re-found across reruns even if the CTID was auto-allocated the first time.
|
||||
|
||||
### Requirements
|
||||
|
||||
- Proxmox VE host with `pveam`, `pct`, `pvesh`, `jq` available
|
||||
- Internet access from both the host (template download) and the LXC (binary download)
|
||||
- Script must be run as **root** on the Proxmox host (enforced; the Web UI shell qualifies)
|
||||
|
||||
### Usage
|
||||
|
||||
#### Full install (from Proxmox shell)
|
||||
|
||||
```bash
|
||||
bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/openbao/install.sh)"
|
||||
```
|
||||
|
||||
Re-running the exact same command later upgrades packages inside the LXC and
|
||||
brings the `bao` binary to the latest release, without touching the config or
|
||||
the raft data directory.
|
||||
|
||||
#### Customisation
|
||||
|
||||
Every parameter is exposed as an environment variable:
|
||||
|
||||
| Variable | Default | Description |
|
||||
| --------------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `CTID` | auto | Container ID (auto-allocated via `pvesh get /cluster/nextid`) |
|
||||
| `OPENBAO_HOSTNAME` | `openbao` | LXC hostname (also used as raft `node_id`) |
|
||||
| `TEMPLATE` | auto-detected | Alpine template; auto-detected from `pveam available` |
|
||||
| `STORAGE` | `local-lvm` | Proxmox storage for the LXC root disk |
|
||||
| `TEMPLATE_STORAGE` | `local` | Storage where Alpine templates live |
|
||||
| `CORES` | `2` | vCPU cores |
|
||||
| `RAM` | `1024` | RAM in MiB |
|
||||
| `DISK` | `8` | Root disk size in GB |
|
||||
| `BRIDGE` | `vmbr0` | Network bridge |
|
||||
| `LXC_TAG` | `openbao` | Stable tag used to re-discover the container |
|
||||
| `OPENBAO_VERSION` | `latest` | Pin a specific release (e.g. `v2.0.3`) or `latest` |
|
||||
| `OPENBAO_LISTEN_ADDR` | `127.0.0.1:8200` | TCP listener address. Loopback by default — Tailscale fronts it. |
|
||||
| `OPENBAO_API_ADDR` | `http://<listen>` | Public API URL (used for UI / OIDC redirects). Set to `https://<host>.<tailnet>.ts.net` once known. |
|
||||
| `TS_AUTHKEY` | _(unset)_ | Pre-auth key (generate at <https://login.tailscale.com/admin/settings/keys>). If unset, finish `tailscale up` manually inside the LXC. |
|
||||
|
||||
```bash
|
||||
CTID=210 OPENBAO_HOSTNAME=vault CORES=4 RAM=2048 \
|
||||
bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/openbao/install.sh)"
|
||||
```
|
||||
|
||||
#### First-time initialisation
|
||||
|
||||
OpenBao starts sealed. Once the LXC is up:
|
||||
|
||||
```bash
|
||||
pct enter <CTID>
|
||||
export VAULT_ADDR=http://127.0.0.1:8200
|
||||
bao operator init # save the unseal keys + root token somewhere safe
|
||||
bao operator unseal # repeat with each key share until unsealed
|
||||
```
|
||||
|
||||
#### Tailscale reverse proxy
|
||||
|
||||
The listener binds to `127.0.0.1:8200` only — Tailscale (running inside the
|
||||
same LXC) acts as the reverse proxy and terminates TLS via tailnet
|
||||
certificates.
|
||||
|
||||
If `TS_AUTHKEY` was supplied at install time, the script runs
|
||||
`tailscale up` and `tailscale serve --bg --https=443 http://127.0.0.1:8200`
|
||||
automatically. OpenBao then becomes reachable at
|
||||
`https://<hostname>.<tailnet>.ts.net`.
|
||||
|
||||
Otherwise, finish setup manually inside the LXC:
|
||||
|
||||
```bash
|
||||
pct enter <CTID>
|
||||
tailscale up --ssh
|
||||
tailscale serve --bg --https=443 http://127.0.0.1:8200
|
||||
tailscale status # prints the tailnet FQDN
|
||||
```
|
||||
|
||||
Then point `OPENBAO_API_ADDR` at that FQDN and rerun the script so the UI / OIDC redirects use it:
|
||||
|
||||
```bash
|
||||
OPENBAO_API_ADDR=https://openbao.<tailnet>.ts.net \
|
||||
bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/feat/lxc-OpenBao/openbao/install.sh)"
|
||||
```
|
||||
|
||||
> HTTPS in `tailscale serve` requires HTTPS to be enabled on your tailnet
|
||||
> (Admin console → DNS → HTTPS Certificates).
|
||||
|
||||
#### Update (from inside the LXC)
|
||||
|
||||
```bash
|
||||
curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/feat/lxc-OpenBao/openbao/install.sh | bash
|
||||
```
|
||||
|
||||
The script auto-detects the presence of `/usr/local/bin/bao` and switches to
|
||||
update mode. The OpenRC service is stopped, the binary is swapped (the old one
|
||||
is kept as `bao.bak.<ts>`), then the service is restarted.
|
||||
|
||||
### Architecture
|
||||
|
||||
- **OS**: latest Alpine LXC template (auto-detected), unprivileged, `nesting=1`, `/dev/net/tun` passthrough for Tailscale
|
||||
- **Binary**: official `bao` release from `github.com/openbao/openbao`, installed in `/usr/local/bin`
|
||||
- **Service**: OpenRC, runs as user `openbao`, logs to `/var/log/openbao.log` (rotated daily, 7 days retained)
|
||||
- **Network**: listener bound to `127.0.0.1:8200`; **Tailscale** runs in the LXC and acts as the reverse proxy (`tailscale serve --https=443`)
|
||||
- **Config**: `/etc/openbao/config.hcl` — raft storage, TLS disabled on the listener (Tailscale terminates TLS), `disable_mlock = true` for unprivileged LXC
|
||||
- **Data**: `/var/lib/openbao/data` (raft)
|
||||
- **Version tracking**: `/opt/openbao_version.txt` records the currently installed tag for idempotent reruns
|
||||
@@ -1,610 +0,0 @@
|
||||
#!/bin/bash
|
||||
# install.sh - OpenBao: LXC creation, installation & update
|
||||
# Usage:
|
||||
# From Proxmox host : bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/feat/lxc-OpenBao/openbao/install.sh)"
|
||||
# From inside LXC : bash /root/install.sh (updates bao binary)
|
||||
#
|
||||
# Single entrypoint, three automatic modes:
|
||||
# 1. Proxmox host, no existing container -> create LXC + install OpenBao
|
||||
# 2. Proxmox host, container already present -> update packages + upgrade bao
|
||||
# 3. Inside an LXC -> install bao if missing, otherwise update
|
||||
#
|
||||
# The OpenBao binary install/upgrade logic lives in a single reusable function
|
||||
# (install_or_upgrade_bao) shared by both the create and update paths.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# --- Config (override via environment) ---
|
||||
CTID="${CTID:-}"
|
||||
HOSTNAME_LXC="${OPENBAO_HOSTNAME:-openbao}"
|
||||
TEMPLATE="${TEMPLATE:-}" # auto-detected when empty
|
||||
STORAGE="${STORAGE:-local-lvm}"
|
||||
TEMPLATE_STORAGE="${TEMPLATE_STORAGE:-local}"
|
||||
CORES="${CORES:-2}"
|
||||
RAM="${RAM:-1024}"
|
||||
DISK="${DISK:-8}"
|
||||
BRIDGE="${BRIDGE:-vmbr0}"
|
||||
LXC_TAG="${LXC_TAG:-openbao}" # stable identifier for the container
|
||||
OPENBAO_VERSION="${OPENBAO_VERSION:-latest}" # "latest" or e.g. "v2.0.3"
|
||||
# GitHub releases endpoint for the openbao/openbao repo (used to resolve "latest").
|
||||
OPENBAO_RELEASES_URL="${OPENBAO_RELEASES_URL:-https://api.github.com/repos/openbao/openbao/releases}"
|
||||
OPENBAO_LISTEN_ADDR="${OPENBAO_LISTEN_ADDR:-127.0.0.1:8200}"
|
||||
# Public API address advertised to clients (also used for OIDC / UI redirects).
|
||||
# Defaults to the local listener; override with the tailnet URL once known,
|
||||
# e.g. OPENBAO_API_ADDR="https://openbao.<tailnet>.ts.net".
|
||||
OPENBAO_API_ADDR="${OPENBAO_API_ADDR:-http://${OPENBAO_LISTEN_ADDR}}"
|
||||
# Optional: pre-authorise the LXC's Tailscale non-interactively.
|
||||
# Generate at https://login.tailscale.com/admin/settings/keys
|
||||
TS_AUTHKEY="${TS_AUTHKEY:-}"
|
||||
# SCRIPT_URL is what the host-side flow pipes into the LXC. Override it when
|
||||
# testing from a non-main branch, e.g.
|
||||
# SCRIPT_URL="https://gitea.arnodo.fr/.../branch/feat/lxc-OpenBao/openbao/install.sh"
|
||||
SCRIPT_URL="${SCRIPT_URL:-https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/openbao/install.sh}"
|
||||
VERSION_FILE="${VERSION_FILE:-/opt/openbao_version.txt}"
|
||||
BAO_USER="openbao"
|
||||
BAO_CONFIG_DIR="/etc/openbao"
|
||||
BAO_DATA_DIR="/var/lib/openbao"
|
||||
|
||||
# --- Colors ---
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Logs go to stderr so callers can safely use $(fn) without capturing log noise.
|
||||
log_info() { echo -e "${GREEN}[INFO]${NC} $1" >&2; }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1" >&2; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; }
|
||||
|
||||
# ============================================================
|
||||
# Generic helpers
|
||||
# ============================================================
|
||||
require_root() {
|
||||
if [[ "$(id -u)" -ne 0 ]]; then
|
||||
log_error "This script must be run as root (current uid: $(id -u))."
|
||||
log_error "On Proxmox, launch it from the host shell or via the Web UI shell, both of which run as root."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# OpenBao publishes release assets named with the raw `uname -m` arch
|
||||
# (e.g. x86_64, aarch64), not Go-style amd64/arm64.
|
||||
get_arch() {
|
||||
case "$(uname -m)" in
|
||||
x86_64) echo "x86_64" ;;
|
||||
aarch64) echo "aarch64" ;;
|
||||
*) log_error "Unsupported architecture: $(uname -m)"; exit 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Resolve "latest" -> concrete tag name, otherwise echo input unchanged.
|
||||
resolve_openbao_version() {
|
||||
local requested="$1"
|
||||
if [[ "$requested" != "latest" ]]; then
|
||||
echo "$requested"
|
||||
return 0
|
||||
fi
|
||||
local tag
|
||||
tag=$(curl -fsSL "${OPENBAO_RELEASES_URL}/latest" | jq -r '.tag_name')
|
||||
if [[ -z "$tag" || "$tag" == "null" ]]; then
|
||||
log_error "Failed to resolve latest OpenBao release from GitHub API."
|
||||
exit 1
|
||||
fi
|
||||
echo "$tag"
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# Reusable: install or upgrade the bao binary in-place.
|
||||
# Used by both fresh-install and update flows.
|
||||
# Returns 0 on success, exits on hard error.
|
||||
# ============================================================
|
||||
install_or_upgrade_bao() {
|
||||
local tag arch version url tmpdir current
|
||||
tag=$(resolve_openbao_version "$OPENBAO_VERSION")
|
||||
arch=$(get_arch)
|
||||
version="${tag#v}"
|
||||
|
||||
current=""
|
||||
if [[ -f "$VERSION_FILE" ]]; then
|
||||
current=$(cat "$VERSION_FILE")
|
||||
fi
|
||||
|
||||
if [[ "$current" == "$tag" && -x /usr/local/bin/bao ]]; then
|
||||
log_info "OpenBao already at $tag, nothing to do."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Asset naming convention: bao_<version>_Linux_<arch>.tar.gz
|
||||
url="https://github.com/openbao/openbao/releases/download/${tag}/bao_${version}_Linux_${arch}.tar.gz"
|
||||
log_info "Downloading OpenBao ${tag} (${arch}) from ${url}..."
|
||||
|
||||
tmpdir=$(mktemp -d)
|
||||
curl -fsSL "$url" -o "${tmpdir}/bao.tar.gz"
|
||||
tar -xzf "${tmpdir}/bao.tar.gz" -C "$tmpdir"
|
||||
|
||||
if [[ ! -f "${tmpdir}/bao" ]]; then
|
||||
log_error "Archive did not contain expected 'bao' binary."
|
||||
rm -rf "$tmpdir"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Stop service if running, swap binary atomically, then restart.
|
||||
local service_was_running=0
|
||||
if command -v rc-service >/dev/null 2>&1 && rc-service openbao status >/dev/null 2>&1; then
|
||||
service_was_running=1
|
||||
log_info "Stopping openbao service for upgrade..."
|
||||
rc-service openbao stop || true
|
||||
fi
|
||||
|
||||
if [[ -x /usr/local/bin/bao ]]; then
|
||||
cp /usr/local/bin/bao "/usr/local/bin/bao.bak.$(date +%s)"
|
||||
fi
|
||||
install -m 0755 "${tmpdir}/bao" /usr/local/bin/bao
|
||||
ln -sf /usr/local/bin/bao /usr/bin/bao
|
||||
echo "$tag" > "$VERSION_FILE"
|
||||
|
||||
log_info "Installed: $(/usr/local/bin/bao --version 2>&1 | head -n1 || true)"
|
||||
|
||||
if [[ "$service_was_running" -eq 1 ]]; then
|
||||
log_info "Restarting openbao service..."
|
||||
rc-service openbao start
|
||||
fi
|
||||
|
||||
rm -rf "$tmpdir"
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# Reusable: bring Tailscale up and publish OpenBao on the tailnet.
|
||||
# Idempotent: re-running is a no-op once Tailscale is logged in and the
|
||||
# serve mapping is already in place.
|
||||
# ============================================================
|
||||
configure_tailscale_proxy() {
|
||||
if ! command -v tailscale >/dev/null 2>&1; then
|
||||
log_warn "tailscale CLI not found, skipping reverse-proxy setup."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# 1. Authenticate the node (if it isn't already).
|
||||
local backend_state
|
||||
backend_state=$(tailscale status --json 2>/dev/null | jq -r '.BackendState // "unknown"')
|
||||
if [[ "$backend_state" != "Running" ]]; then
|
||||
if [[ -n "$TS_AUTHKEY" ]]; then
|
||||
log_info "Bringing Tailscale up with provided auth key..."
|
||||
tailscale up --authkey "$TS_AUTHKEY" --ssh --hostname "$HOSTNAME_LXC" \
|
||||
|| log_warn "tailscale up failed — run it manually inside the LXC."
|
||||
else
|
||||
log_warn "Tailscale not authenticated and TS_AUTHKEY was not supplied."
|
||||
log_warn "Finish setup inside the LXC with: tailscale up --ssh"
|
||||
log_warn "Then publish OpenBao with: tailscale serve --bg --https=443 http://${OPENBAO_LISTEN_ADDR}"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# 2. Publish the local OpenBao listener on the tailnet (auto-HTTPS).
|
||||
if tailscale serve status 2>/dev/null | grep -q "${OPENBAO_LISTEN_ADDR}"; then
|
||||
log_info "Tailscale serve already publishes http://${OPENBAO_LISTEN_ADDR}."
|
||||
else
|
||||
log_info "Publishing OpenBao on the tailnet via 'tailscale serve' (HTTPS:443)..."
|
||||
tailscale serve --bg --https=443 "http://${OPENBAO_LISTEN_ADDR}" \
|
||||
|| log_warn "tailscale serve failed — enable HTTPS on your tailnet and retry."
|
||||
fi
|
||||
|
||||
local fqdn
|
||||
fqdn=$(tailscale status --json 2>/dev/null | jq -r '.Self.DNSName // ""' | sed 's/\.$//')
|
||||
if [[ -n "$fqdn" ]]; then
|
||||
log_info "OpenBao should now be reachable at: https://${fqdn}"
|
||||
if [[ "$OPENBAO_API_ADDR" != "https://${fqdn}" ]]; then
|
||||
log_warn "OPENBAO_API_ADDR is '${OPENBAO_API_ADDR}'."
|
||||
log_warn "For OIDC / UI redirects, set it to 'https://${fqdn}' and re-run, or edit ${BAO_CONFIG_DIR}/config.hcl."
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# Proxmox-host helpers
|
||||
# ============================================================
|
||||
|
||||
# Detect newest Alpine LXC template available from the Proxmox repos.
|
||||
detect_latest_alpine_template() {
|
||||
local tmpl
|
||||
tmpl=$(pveam available --section system 2>/dev/null \
|
||||
| awk '/^system[[:space:]]+alpine-/ {print $2}' \
|
||||
| sort -V \
|
||||
| tail -n1)
|
||||
|
||||
if [[ -z "$tmpl" ]]; then
|
||||
log_warn "Could not query pveam; falling back to a known-good Alpine template."
|
||||
tmpl="alpine-3.22-default_20250617_amd64.tar.xz"
|
||||
fi
|
||||
log_info "Selected Alpine template: $tmpl"
|
||||
echo "$tmpl"
|
||||
}
|
||||
|
||||
# Find an existing LXC by tag or hostname. Echoes CTID, returns 1 if none.
|
||||
find_existing_lxc() {
|
||||
local id host tags
|
||||
while read -r id _; do
|
||||
[[ -z "$id" || "$id" == "VMID" ]] && continue
|
||||
host=$(pct config "$id" 2>/dev/null | awk -F': ' '/^hostname:/ {print $2}' || true)
|
||||
tags=$(pct config "$id" 2>/dev/null | awk -F': ' '/^tags:/ {print $2}' || true)
|
||||
if [[ "$host" == "$HOSTNAME_LXC" ]] || [[ ",${tags//;/,}," == *",${LXC_TAG},"* ]]; then
|
||||
echo "$id"
|
||||
return 0
|
||||
fi
|
||||
done < <(pct list | awk 'NR>1 {print $1}')
|
||||
return 1
|
||||
}
|
||||
|
||||
ensure_template_present() {
|
||||
local tmpl="$1"
|
||||
if ! pveam list "$TEMPLATE_STORAGE" 2>/dev/null | grep -q "$tmpl"; then
|
||||
log_info "Downloading template ${tmpl} to storage ${TEMPLATE_STORAGE}..."
|
||||
pveam update >/dev/null
|
||||
pveam download "$TEMPLATE_STORAGE" "$tmpl"
|
||||
else
|
||||
log_info "Template ${tmpl} already present on ${TEMPLATE_STORAGE}."
|
||||
fi
|
||||
}
|
||||
|
||||
# Pick next available CTID if user did not provide one.
|
||||
allocate_ctid() {
|
||||
pvesh get /cluster/nextid 2>/dev/null \
|
||||
|| pvesh get /cluster/resources --type vm --output-format json 2>/dev/null \
|
||||
| jq '[.[].vmid] | max + 1' \
|
||||
|| echo 100
|
||||
}
|
||||
|
||||
# Inject the script into the container and execute it in the requested mode.
|
||||
# Forwards the relevant runtime configuration through the environment so the
|
||||
# inner invocation produces the same config the user requested on the host.
|
||||
exec_in_lxc() {
|
||||
local ctid="$1"
|
||||
local mode="$2" # --install or --update
|
||||
|
||||
# Ensure base tooling exists inside the container before piping the script.
|
||||
pct exec "$ctid" -- sh -c "apk add --no-cache bash curl jq ca-certificates >/dev/null 2>&1"
|
||||
curl -fsSL "$SCRIPT_URL" \
|
||||
| pct exec "$ctid" -- env \
|
||||
SCRIPT_URL="$SCRIPT_URL" \
|
||||
OPENBAO_VERSION="$OPENBAO_VERSION" \
|
||||
OPENBAO_HOSTNAME="$HOSTNAME_LXC" \
|
||||
OPENBAO_LISTEN_ADDR="$OPENBAO_LISTEN_ADDR" \
|
||||
OPENBAO_API_ADDR="$OPENBAO_API_ADDR" \
|
||||
TS_AUTHKEY="$TS_AUTHKEY" \
|
||||
bash -s -- "$mode"
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# MODE: Proxmox host — create LXC + install
|
||||
# ============================================================
|
||||
create_lxc() {
|
||||
log_info "=== OpenBao — LXC creation ==="
|
||||
|
||||
if [[ -z "$TEMPLATE" ]]; then
|
||||
TEMPLATE=$(detect_latest_alpine_template)
|
||||
else
|
||||
log_info "Using user-provided template: $TEMPLATE"
|
||||
fi
|
||||
ensure_template_present "$TEMPLATE"
|
||||
|
||||
if [[ -z "$CTID" ]]; then
|
||||
CTID=$(allocate_ctid)
|
||||
log_info "Auto-selected CTID: $CTID"
|
||||
fi
|
||||
|
||||
log_info "Creating LXC ${CTID} (${HOSTNAME_LXC})..."
|
||||
pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" \
|
||||
--hostname "$HOSTNAME_LXC" \
|
||||
--cores "$CORES" \
|
||||
--memory "$RAM" \
|
||||
--rootfs "${STORAGE}:${DISK}" \
|
||||
--net0 "name=eth0,bridge=${BRIDGE},ip=dhcp" \
|
||||
--unprivileged 1 \
|
||||
--features "nesting=1" \
|
||||
--tags "infra-script,${LXC_TAG}" \
|
||||
--onboot 1 \
|
||||
--start 0
|
||||
|
||||
# Tailscale needs /dev/net/tun inside the unprivileged container.
|
||||
log_info "Adding /dev/net/tun passthrough for Tailscale..."
|
||||
cat >> "/etc/pve/lxc/${CTID}.conf" <<EOF
|
||||
lxc.cgroup2.devices.allow: c 10:200 rwm
|
||||
lxc.mount.entry: /dev/net dev/net none bind,create=dir
|
||||
lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file
|
||||
EOF
|
||||
|
||||
log_info "Starting LXC ${CTID}..."
|
||||
pct start "$CTID"
|
||||
# Wait for network to come up
|
||||
local tries=0
|
||||
until pct exec "$CTID" -- sh -c "ip -4 addr show eth0 | grep -q 'inet '" 2>/dev/null; do
|
||||
tries=$((tries + 1))
|
||||
if (( tries > 20 )); then
|
||||
log_error "LXC ${CTID} did not acquire an IP after 20s."
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
log_info "Running installer inside LXC ${CTID}..."
|
||||
exec_in_lxc "$CTID" "--install"
|
||||
|
||||
local ip
|
||||
ip=$(pct exec "$CTID" -- ip -4 addr show eth0 2>/dev/null | awk '/inet /{print $2}' | cut -d/ -f1 || true)
|
||||
|
||||
echo ""
|
||||
log_info "========================================="
|
||||
log_info "LXC ${CTID} created successfully!"
|
||||
log_info "========================================="
|
||||
echo ""
|
||||
echo " Hostname : ${HOSTNAME_LXC}"
|
||||
echo " IP : ${ip:-pending}"
|
||||
echo " API : http://${ip:-<ip>}:8200"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " pct enter ${CTID}"
|
||||
echo " export VAULT_ADDR=http://127.0.0.1:8200"
|
||||
echo " bao operator init # initialise & retrieve unseal keys + root token"
|
||||
echo " bao operator unseal # repeat with the unseal keys"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# MODE: Proxmox host — update existing LXC
|
||||
# ============================================================
|
||||
update_lxc() {
|
||||
local ctid="$1"
|
||||
log_info "=== OpenBao — updating existing LXC ${ctid} ==="
|
||||
|
||||
if ! pct status "$ctid" | grep -q running; then
|
||||
log_info "Starting LXC ${ctid}..."
|
||||
pct start "$ctid"
|
||||
sleep 3
|
||||
fi
|
||||
|
||||
log_info "Refreshing Alpine packages inside LXC ${ctid}..."
|
||||
pct exec "$ctid" -- sh -c "apk update >/dev/null && apk upgrade >/dev/null"
|
||||
|
||||
log_info "Upgrading bao binary inside LXC ${ctid}..."
|
||||
exec_in_lxc "$ctid" "--update"
|
||||
|
||||
log_info "Update of LXC ${ctid} complete."
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# MODE: inside LXC — fresh install of OpenBao
|
||||
# ============================================================
|
||||
install_inside_lxc() {
|
||||
log_info "=== OpenBao — installation ==="
|
||||
|
||||
log_info "Updating package index..."
|
||||
apk update >/dev/null
|
||||
apk upgrade >/dev/null
|
||||
|
||||
log_info "Installing dependencies..."
|
||||
apk add --no-cache bash curl jq ca-certificates gcompat openrc logrotate tailscale >/dev/null
|
||||
|
||||
log_info "Enabling tailscaled..."
|
||||
rc-update add tailscale default >/dev/null 2>&1 || true
|
||||
rc-service tailscale start >/dev/null 2>&1 || log_warn "tailscaled failed to start (is /dev/net/tun mapped into the LXC?)"
|
||||
|
||||
install_or_upgrade_bao
|
||||
|
||||
log_info "Creating ${BAO_USER} system user..."
|
||||
if ! id "$BAO_USER" >/dev/null 2>&1; then
|
||||
addgroup -S "$BAO_USER" 2>/dev/null || true
|
||||
adduser -S -D -H -h "$BAO_DATA_DIR" -s /sbin/nologin -G "$BAO_USER" "$BAO_USER"
|
||||
fi
|
||||
|
||||
log_info "Provisioning directories..."
|
||||
mkdir -p "$BAO_CONFIG_DIR" "$BAO_DATA_DIR/data"
|
||||
chown -R "${BAO_USER}:${BAO_USER}" "$BAO_DATA_DIR"
|
||||
chmod 750 "$BAO_DATA_DIR"
|
||||
|
||||
if [[ ! -f "${BAO_CONFIG_DIR}/config.hcl" ]]; then
|
||||
log_info "Writing default ${BAO_CONFIG_DIR}/config.hcl..."
|
||||
# OpenBao listens on loopback only; Tailscale (running in the same LXC)
|
||||
# acts as the reverse proxy and terminates TLS via tailnet certificates.
|
||||
# https://openbao.org/docs/configuration/
|
||||
cat > "${BAO_CONFIG_DIR}/config.hcl" <<EOF
|
||||
ui = true
|
||||
disable_mlock = true
|
||||
|
||||
storage "raft" {
|
||||
path = "${BAO_DATA_DIR}/data"
|
||||
node_id = "${HOSTNAME_LXC}"
|
||||
}
|
||||
|
||||
listener "tcp" {
|
||||
address = "${OPENBAO_LISTEN_ADDR}"
|
||||
tls_disable = 1
|
||||
}
|
||||
|
||||
api_addr = "${OPENBAO_API_ADDR}"
|
||||
cluster_addr = "http://127.0.0.1:8201"
|
||||
EOF
|
||||
chown root:"$BAO_USER" "${BAO_CONFIG_DIR}/config.hcl"
|
||||
chmod 640 "${BAO_CONFIG_DIR}/config.hcl"
|
||||
else
|
||||
log_info "Existing ${BAO_CONFIG_DIR}/config.hcl preserved."
|
||||
fi
|
||||
|
||||
log_info "Installing OpenRC service..."
|
||||
cat > /etc/init.d/openbao <<'EOF'
|
||||
#!/sbin/openrc-run
|
||||
|
||||
name="OpenBao"
|
||||
description="OpenBao secrets manager"
|
||||
command="/usr/local/bin/bao"
|
||||
command_args="server -config=/etc/openbao/config.hcl"
|
||||
command_user="openbao:openbao"
|
||||
command_background=true
|
||||
pidfile="/run/${RC_SVCNAME}.pid"
|
||||
directory="/var/lib/openbao"
|
||||
|
||||
output_log="/var/log/openbao.log"
|
||||
error_log="/var/log/openbao.log"
|
||||
|
||||
depend() {
|
||||
need net
|
||||
after net
|
||||
}
|
||||
|
||||
start_pre() {
|
||||
checkpath --directory --owner openbao:openbao --mode 0750 /var/lib/openbao
|
||||
checkpath --directory --owner openbao:openbao --mode 0750 /var/lib/openbao/data
|
||||
checkpath --file --owner openbao:openbao --mode 0644 /var/log/openbao.log
|
||||
}
|
||||
EOF
|
||||
chmod +x /etc/init.d/openbao
|
||||
rc-update add openbao default >/dev/null
|
||||
|
||||
cat > /etc/logrotate.d/openbao <<'EOF'
|
||||
/var/log/openbao.log {
|
||||
daily
|
||||
rotate 7
|
||||
compress
|
||||
missingok
|
||||
notifempty
|
||||
copytruncate
|
||||
}
|
||||
EOF
|
||||
ln -sf /usr/sbin/logrotate /etc/periodic/daily/logrotate 2>/dev/null || true
|
||||
|
||||
log_info "Starting openbao service..."
|
||||
rc-service openbao start || log_warn "openbao failed to start — inspect /var/log/openbao.log"
|
||||
|
||||
log_info "Enabling console auto-login on tty1..."
|
||||
# Alpine ships busybox getty by default; agetty (from util-linux) is what
|
||||
# supports --autologin.
|
||||
apk add --no-cache agetty >/dev/null 2>&1 || apk add --no-cache util-linux >/dev/null
|
||||
|
||||
# Replace any existing tty1 entry, then append our autologin line. Doing it
|
||||
# in two steps (delete + append) is more robust than an in-place sed against
|
||||
# a pattern that may drift across Alpine releases.
|
||||
sed -i '/^tty1::/d' /etc/inittab
|
||||
echo 'tty1::respawn:/sbin/agetty --autologin root --noclear 38400 tty1' >> /etc/inittab
|
||||
|
||||
# Tell PID 1 to re-read /etc/inittab so the change takes effect without a reboot.
|
||||
kill -HUP 1 2>/dev/null || true
|
||||
|
||||
# Kick any getty/agetty still attached to tty1 so init respawns it *now* with
|
||||
# the new line — otherwise the first web-console session lands on the stale
|
||||
# process and the operator has to type `exit` once before autologin kicks in.
|
||||
pkill -KILL -f '(getty|agetty).*tty1' 2>/dev/null || true
|
||||
|
||||
configure_tailscale_proxy
|
||||
|
||||
log_info "Configuring MOTD..."
|
||||
# /etc/profile.d/ runs for every interactive login shell — works for both
|
||||
# the auto-login tty and Tailscale SSH. Quoted heredoc: every variable is
|
||||
# resolved at login time, not at install time.
|
||||
cat > /etc/profile.d/00-openbao.sh <<'MOTD'
|
||||
TS_FQDN=$(tailscale status --json 2>/dev/null | awk -F'"' '
|
||||
/"Self"/ { in_self=1 }
|
||||
in_self && /"DNSName"/ { gsub(/\.$/, "", $4); print $4; exit }
|
||||
')
|
||||
[[ -z "$TS_FQDN" ]] && TS_FQDN="$(hostname).ts.net"
|
||||
|
||||
BAO_VERSION=$(cat /opt/openbao_version.txt 2>/dev/null || echo "unknown")
|
||||
|
||||
# `bao status` exit codes: 0 = unsealed, 2 = sealed, anything else = error.
|
||||
VAULT_ADDR=http://127.0.0.1:8200 /usr/local/bin/bao status >/dev/null 2>&1
|
||||
case $? in
|
||||
0) SEAL_STATE="unsealed" ;;
|
||||
2) SEAL_STATE="SEALED (run: bao operator unseal)" ;;
|
||||
*) SEAL_STATE="unreachable" ;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
echo " ___ ____ "
|
||||
echo " / _ \ _ __ ___ _ __ | __ ) __ _ ___ "
|
||||
echo "| | | | '_ \ / _ \ '_ \| _ \ / _\` |/ _ \\"
|
||||
echo "| |_| | |_) | __/ | | | |_) | (_| | (_) |"
|
||||
echo " \___/| .__/ \___|_| |_|____/ \__,_|\___/"
|
||||
echo " |_| "
|
||||
echo ""
|
||||
echo "OpenBao Secrets Manager (${BAO_VERSION})"
|
||||
echo "─────────────────────────────────────────"
|
||||
echo "Access:"
|
||||
echo " • API (local) : http://127.0.0.1:8200"
|
||||
echo " • Tailnet : https://${TS_FQDN}"
|
||||
echo " • Seal status : ${SEAL_STATE}"
|
||||
echo ""
|
||||
echo "Useful commands:"
|
||||
echo " export VAULT_ADDR=http://127.0.0.1:8200"
|
||||
echo " bao status"
|
||||
echo " bao operator init # first-time only"
|
||||
echo " bao operator unseal # after every restart"
|
||||
echo " rc-service openbao status"
|
||||
echo " tail -f /var/log/openbao.log"
|
||||
echo "─────────────────────────────────────────"
|
||||
echo ""
|
||||
MOTD
|
||||
chmod +x /etc/profile.d/00-openbao.sh
|
||||
|
||||
log_info "Cleaning up..."
|
||||
rm -rf /var/cache/apk/*
|
||||
|
||||
echo ""
|
||||
log_info "========================================="
|
||||
log_info "OpenBao installation complete!"
|
||||
log_info "========================================="
|
||||
echo ""
|
||||
echo "Initialise the server with:"
|
||||
echo " export VAULT_ADDR=http://127.0.0.1:8200"
|
||||
echo " bao operator init"
|
||||
echo " bao operator unseal # repeat with each unseal key share"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# MODE: inside LXC — update only
|
||||
# ============================================================
|
||||
update_inside_lxc() {
|
||||
log_info "=== OpenBao — update ==="
|
||||
apk update >/dev/null
|
||||
apk upgrade >/dev/null
|
||||
install_or_upgrade_bao
|
||||
configure_tailscale_proxy
|
||||
log_info "Update complete."
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# Main — dispatch on explicit mode flag or auto-detect context
|
||||
# ============================================================
|
||||
main() {
|
||||
case "${1:-}" in
|
||||
--install)
|
||||
install_inside_lxc
|
||||
return
|
||||
;;
|
||||
--update)
|
||||
update_inside_lxc
|
||||
return
|
||||
;;
|
||||
esac
|
||||
|
||||
if command -v pct >/dev/null 2>&1; then
|
||||
# Running on a Proxmox host
|
||||
require_root
|
||||
|
||||
local existing=""
|
||||
if existing=$(find_existing_lxc); then
|
||||
log_info "Found existing OpenBao LXC (CTID ${existing}, hostname/tag match) — switching to update mode."
|
||||
update_lxc "$existing"
|
||||
else
|
||||
create_lxc
|
||||
fi
|
||||
else
|
||||
# Inside a container (no Proxmox tooling)
|
||||
require_root
|
||||
if [[ -x /usr/local/bin/bao ]]; then
|
||||
update_inside_lxc
|
||||
else
|
||||
install_inside_lxc
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -1,62 +0,0 @@
|
||||
# Proxy Server
|
||||
|
||||
Deploys a secure reverse proxy with Tailscale + Nginx Proxy Manager.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/proxy/install.sh | bash
|
||||
```
|
||||
|
||||
## Components
|
||||
|
||||
- **Tailscale**: Private network access (SSH, admin panel)
|
||||
- **Nginx Proxy Manager**: Public reverse proxy (HTTP/HTTPS)
|
||||
- **UFW**: Firewall (only 80/443 exposed publicly)
|
||||
- **fail2ban** + **unattended-upgrades**: Basic hardening
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `PROXY_HOSTNAME` | `proxy` | Server hostname |
|
||||
| `TZ` | `Europe/Paris` | Timezone |
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
PROXY_HOSTNAME=myproxy TZ=America/New_York curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/proxy/install.sh | bash
|
||||
```
|
||||
|
||||
## What it does
|
||||
|
||||
1. Sets hostname
|
||||
2. Installs base packages (vim, fail2ban, unattended-upgrades, at)
|
||||
3. Installs and connects Tailscale (will prompt for authentication)
|
||||
4. Configures sysctl for exit-node capability
|
||||
5. Installs Docker
|
||||
6. Configures UFW (80/443 public, everything else via Tailscale only)
|
||||
7. Deploys Nginx Proxy Manager
|
||||
8. Exposes NPM admin panel via Tailscale serve
|
||||
9. Temporarily opens SSH port 22 for 5 minutes (safety net)
|
||||
|
||||
## SSH Safety Net
|
||||
|
||||
During installation, SSH port 22 is temporarily opened for 5 minutes to prevent lockout if you're connected via public IP. After 5 minutes, it will be automatically closed and only Tailscale SSH will work.
|
||||
|
||||
```bash
|
||||
# List scheduled jobs
|
||||
sudo atq
|
||||
|
||||
# Cancel the scheduled SSH closure (replace N with job number)
|
||||
sudo atrm N
|
||||
|
||||
# Manually close SSH port 22 if needed
|
||||
sudo ufw delete allow 22/tcp
|
||||
```
|
||||
|
||||
## Post-install
|
||||
|
||||
- Access NPM admin: `https://proxy.<your-tailnet>.ts.net`
|
||||
- Default credentials: `admin@example.com` / `changeme`
|
||||
- Optionally approve exit-node in Tailscale admin console
|
||||
@@ -1,360 +0,0 @@
|
||||
#!/bin/bash
|
||||
# install.sh - Automated deployment of Proxy Server with Tailscale + Traefik v3 + Fail2ban
|
||||
# Usage: curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/proxy/install.sh | bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Colors for logging
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
|
||||
# Pre-flight checks
|
||||
check_root() {
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
log_error "Do not run as root directly. Use a user with sudo privileges."
|
||||
exit 1
|
||||
fi
|
||||
if ! sudo -v; then
|
||||
log_error "User must have sudo privileges."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
check_debian() {
|
||||
if ! grep -qi debian /etc/os-release 2>/dev/null; then
|
||||
log_warn "This script is optimized for Debian. Continuing anyway..."
|
||||
fi
|
||||
}
|
||||
|
||||
# Configuration variables (can be overridden via environment)
|
||||
PROXY_HOSTNAME="${PROXY_HOSTNAME:-proxy}"
|
||||
TRAEFIK_DIR="$HOME/traefik"
|
||||
|
||||
# ACME_EMAIL is required for Let's Encrypt certificate issuance notifications.
|
||||
# Export it before running: export ACME_EMAIL=you@example.com
|
||||
ACME_EMAIL="${ACME_EMAIL:-}"
|
||||
|
||||
# Optional: pre-authorize Tailscale non-interactively (recommended for curl|bash).
|
||||
# Generate at https://login.tailscale.com/admin/settings/keys
|
||||
TS_AUTHKEY="${TS_AUTHKEY:-}"
|
||||
|
||||
main() {
|
||||
log_info "=== Proxy Server Deployment (Traefik v3) ==="
|
||||
|
||||
check_root
|
||||
check_debian
|
||||
|
||||
# Prompt for ACME email if not set. Only attempt interactive prompt when a
|
||||
# TTY is available — when invoked via `curl … | bash`, stdin is the pipe
|
||||
# and reading from /dev/tty may also fail (e.g. non-interactive runners).
|
||||
if [[ -z "$ACME_EMAIL" ]]; then
|
||||
if [[ -r /dev/tty ]]; then
|
||||
log_warn "ACME_EMAIL is not set in the environment."
|
||||
read -rp "Enter your ACME email address: " ACME_EMAIL < /dev/tty || true
|
||||
fi
|
||||
if [[ -z "$ACME_EMAIL" ]]; then
|
||||
log_error "ACME_EMAIL is required. Export it before running:"
|
||||
log_error " export ACME_EMAIL=you@example.com"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$(hostname)" != "$PROXY_HOSTNAME" ]]; then
|
||||
log_info "Setting hostname to: $PROXY_HOSTNAME"
|
||||
echo "$PROXY_HOSTNAME" | sudo tee /etc/hostname > /dev/null
|
||||
sudo hostnamectl set-hostname "$PROXY_HOSTNAME"
|
||||
else
|
||||
log_info "Hostname already set to $PROXY_HOSTNAME, skipping."
|
||||
fi
|
||||
|
||||
log_info "Installing base packages..."
|
||||
sudo apt update -qq
|
||||
sudo apt install -y -qq vim ca-certificates curl gnupg lsb-release fail2ban unattended-upgrades ufw ethtool networkd-dispatcher > /dev/null
|
||||
|
||||
log_info "Installing Tailscale..."
|
||||
curl -fsSL https://tailscale.com/install.sh | sh
|
||||
|
||||
log_info "Configuring sysctl for exit-node support..."
|
||||
echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/99-tailscale.conf > /dev/null
|
||||
echo 'net.ipv6.conf.all.forwarding = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf > /dev/null
|
||||
sudo sysctl -p /etc/sysctl.d/99-tailscale.conf > /dev/null
|
||||
|
||||
log_info "Configuring ethtool for Tailscale UDP GRO forwarding..."
|
||||
# Determine the default-route interface and disable rx-gro-list / enable
|
||||
# rx-udp-gro-forwarding to avoid the Tailscale throughput warning.
|
||||
NETDEV=$(ip -o route show default | awk '{print $5; exit}')
|
||||
if [[ -z "$NETDEV" ]]; then
|
||||
log_error "Could not determine default network interface."
|
||||
exit 1
|
||||
fi
|
||||
sudo ethtool -K "$NETDEV" rx-udp-gro-forwarding on rx-gro-list off
|
||||
# Persist across reboots via networkd-dispatcher
|
||||
sudo mkdir -p /etc/networkd-dispatcher/routable.d
|
||||
printf '#!/bin/sh\nethtool -K %s rx-udp-gro-forwarding on rx-gro-list off\n' "$NETDEV" \
|
||||
| sudo tee /etc/networkd-dispatcher/routable.d/50-tailscale > /dev/null
|
||||
sudo chmod 755 /etc/networkd-dispatcher/routable.d/50-tailscale
|
||||
|
||||
# Connect to Tailscale only if not already logged in (idempotent re-runs).
|
||||
if ! sudo tailscale status >/dev/null 2>&1; then
|
||||
log_info "Connecting to Tailscale..."
|
||||
if [[ -n "$TS_AUTHKEY" ]]; then
|
||||
sudo tailscale up --ssh --advertise-exit-node --authkey="$TS_AUTHKEY"
|
||||
else
|
||||
log_warn "TS_AUTHKEY not set — interactive browser auth required."
|
||||
sudo tailscale up --ssh --advertise-exit-node
|
||||
fi
|
||||
else
|
||||
log_info "Tailscale already connected, skipping."
|
||||
fi
|
||||
|
||||
log_info "Installing Docker..."
|
||||
sudo mkdir -m 0755 -p /etc/apt/keyrings
|
||||
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
sudo apt update -qq
|
||||
sudo apt install -y -qq docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin > /dev/null
|
||||
|
||||
log_info "Adding current user to docker group..."
|
||||
sudo usermod -aG docker "$USER"
|
||||
|
||||
log_info "Configuring UFW firewall..."
|
||||
# Idempotent: only reset if our marker rules are absent. This preserves any
|
||||
# rules added later by the operator on re-runs.
|
||||
if ! sudo ufw status | grep -q "tailscale0"; then
|
||||
sudo ufw --force reset > /dev/null
|
||||
sudo ufw default deny incoming > /dev/null
|
||||
sudo ufw default allow outgoing > /dev/null
|
||||
# Allow HTTP/HTTPS from the public internet (handled by Traefik)
|
||||
sudo ufw allow 80/tcp > /dev/null
|
||||
sudo ufw allow 443/tcp > /dev/null
|
||||
# Allow all traffic on Tailscale interface (dashboard, metrics, SSH, admin — everything internal)
|
||||
sudo ufw allow in on tailscale0 > /dev/null
|
||||
# Port 22 is intentionally NOT opened publicly; Tailscale SSH covers management access
|
||||
sudo ufw --force enable > /dev/null
|
||||
else
|
||||
log_info "UFW already configured, skipping reset."
|
||||
fi
|
||||
|
||||
log_info "Configuring Fail2ban for Traefik..."
|
||||
# Create the log file and fail2ban socket dir now so:
|
||||
# - fail2ban can open the log file when the jail loads.
|
||||
# - docker doesn't bind-mount /var/run/fail2ban as an empty dir if the
|
||||
# exporter container starts before fail2ban writes its socket.
|
||||
sudo mkdir -p /var/log/traefik /var/run/fail2ban
|
||||
sudo chown "$USER":"$USER" /var/log/traefik
|
||||
sudo touch /var/log/traefik/access.log
|
||||
|
||||
sudo tee /etc/fail2ban/filter.d/traefik.conf > /dev/null << 'EOF'
|
||||
[Definition]
|
||||
# Match JSON log lines where ClientHost is the offending IP and DownstreamStatus
|
||||
# is an auth/abuse status (401, 403, 429) or a server error (5xx).
|
||||
# Legitimate 404s on missing assets are excluded so dev traffic doesn't ban users.
|
||||
# Two patterns cover both possible field orderings in the JSON.
|
||||
failregex = ^.*"ClientHost":"<HOST>".*"DownstreamStatus":(401|403|429|5[0-9]{2})
|
||||
^.*"DownstreamStatus":(401|403|429|5[0-9]{2}).*"ClientHost":"<HOST>"
|
||||
ignoreregex =
|
||||
EOF
|
||||
|
||||
sudo tee /etc/fail2ban/jail.d/traefik.conf > /dev/null << 'EOF'
|
||||
[traefik-auth]
|
||||
enabled = true
|
||||
filter = traefik
|
||||
logpath = /var/log/traefik/access.log
|
||||
maxretry = 10
|
||||
findtime = 5m
|
||||
bantime = 1h
|
||||
action = iptables-multiport[name=traefik, port="80,443", protocol=tcp]
|
||||
EOF
|
||||
|
||||
sudo systemctl restart fail2ban
|
||||
|
||||
log_info "Creating Traefik stack under $TRAEFIK_DIR..."
|
||||
mkdir -p "$TRAEFIK_DIR/conf.d"
|
||||
|
||||
# acme.json must be 600 or Traefik refuses to use it
|
||||
touch "$TRAEFIK_DIR/acme.json"
|
||||
chmod 600 "$TRAEFIK_DIR/acme.json"
|
||||
|
||||
# --- docker-compose.yml ---
|
||||
# SECURITY: The dashboard/API entrypoint is bound to 127.0.0.1 ONLY.
|
||||
# Combined with `api.insecure: true` in traefik.yml, the dashboard has no
|
||||
# authentication — it is only reachable via the host loopback and exposed
|
||||
# selectively over the tailnet through `tailscale serve`. DO NOT change
|
||||
# this port binding to 0.0.0.0 or any non-loopback address.
|
||||
cat > "$TRAEFIK_DIR/docker-compose.yml" << 'EOF'
|
||||
services:
|
||||
traefik:
|
||||
image: traefik:v3
|
||||
container_name: traefik
|
||||
restart: unless-stopped
|
||||
dns:
|
||||
- 100.100.100.100
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
# MUST stay on 127.0.0.1: dashboard is unauthenticated (see traefik.yml).
|
||||
- "127.0.0.1:8080:8080"
|
||||
volumes:
|
||||
- ./traefik.yml:/etc/traefik/traefik.yml:ro
|
||||
- ./conf.d:/etc/traefik/conf.d:ro
|
||||
- ./acme.json:/acme.json
|
||||
- /var/log/traefik:/var/log/traefik
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
|
||||
fail2ban-exporter:
|
||||
image: registry.gitlab.com/hctrdev/fail2ban-prometheus-exporter:latest
|
||||
container_name: fail2ban-exporter
|
||||
restart: unless-stopped
|
||||
user: root
|
||||
ports:
|
||||
# Metrics reachable only via Tailscale (127.0.0.1 binding + UFW blocks public access)
|
||||
- "127.0.0.1:9191:9191"
|
||||
volumes:
|
||||
# Mount the directory, not the socket file: avoids Docker creating a directory
|
||||
# at the path when fail2ban is briefly down and recreating its socket.
|
||||
- /var/run/fail2ban:/var/run/fail2ban
|
||||
EOF
|
||||
|
||||
# --- traefik.yml (static config) ---
|
||||
# Unquoted EOF: ${ACME_EMAIL} must expand at write time into the static config.
|
||||
cat > "$TRAEFIK_DIR/traefik.yml" << EOF
|
||||
entryPoints:
|
||||
web:
|
||||
address: ":80"
|
||||
http:
|
||||
redirections:
|
||||
entryPoint:
|
||||
to: websecure
|
||||
scheme: https
|
||||
|
||||
websecure:
|
||||
address: ":443"
|
||||
|
||||
traefik:
|
||||
address: ":8080"
|
||||
|
||||
certificatesResolvers:
|
||||
letsencrypt:
|
||||
acme:
|
||||
email: "${ACME_EMAIL}"
|
||||
storage: /acme.json
|
||||
httpChallenge:
|
||||
entryPoint: web
|
||||
|
||||
providers:
|
||||
file:
|
||||
directory: /etc/traefik/conf.d
|
||||
watch: true
|
||||
|
||||
metrics:
|
||||
prometheus:
|
||||
addEntryPointsLabels: true
|
||||
addServicesLabels: true
|
||||
addRoutersLabels: true
|
||||
entryPoint: traefik
|
||||
|
||||
api:
|
||||
dashboard: true
|
||||
# insecure exposes the dashboard on the :8080 entrypoint without auth.
|
||||
# This is acceptable ONLY because docker-compose.yml binds 8080 to 127.0.0.1.
|
||||
# Public reach requires going through `tailscale serve` (tailnet-authenticated).
|
||||
insecure: true
|
||||
|
||||
accessLog:
|
||||
filePath: /var/log/traefik/access.log
|
||||
format: json
|
||||
EOF
|
||||
|
||||
# --- conf.d/gitea.yml (dynamic config) ---
|
||||
cat > "$TRAEFIK_DIR/conf.d/gitea.yml" << 'EOF'
|
||||
http:
|
||||
routers:
|
||||
gitea:
|
||||
rule: "Host(`gitea.arnodo.fr`)"
|
||||
entryPoints:
|
||||
- websecure
|
||||
service: gitea
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
|
||||
services:
|
||||
gitea:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://gitea.taila5ad8.ts.net:3000"
|
||||
EOF
|
||||
|
||||
log_info "Starting Traefik stack..."
|
||||
# Use sg to apply the docker group without requiring a re-login.
|
||||
# cd into the dir so paths inside the command don't break on spaces in $HOME.
|
||||
(cd "$TRAEFIK_DIR" && sg docker -c "docker compose up -d")
|
||||
|
||||
# Idempotent: only register the serve mapping if it isn't already present.
|
||||
# Use --json (stable contract) and capture stdout+stderr so any help/error
|
||||
# output on older tailscale builds doesn't leak to the user's terminal.
|
||||
if ! sudo tailscale serve status --json 2>&1 | grep -q '"127.0.0.1:8080"'; then
|
||||
log_info "Exposing Traefik dashboard via Tailscale serve..."
|
||||
sudo tailscale serve --bg http://localhost:8080
|
||||
else
|
||||
log_info "Tailscale serve already configured for dashboard, skipping."
|
||||
fi
|
||||
|
||||
log_info "Configuring MOTD..."
|
||||
# /etc/profile.d/ runs for every interactive login shell regardless of the SSH
|
||||
# implementation (works for both Tailscale SSH and regular OpenSSH).
|
||||
cat << 'MOTD' | sudo tee /etc/profile.d/00-proxy.sh > /dev/null
|
||||
TS_FQDN=$(tailscale status --json 2>/dev/null | awk -F'"' '
|
||||
/"Self"/ { in_self=1 }
|
||||
in_self && /"DNSName"/ { gsub(/\.$/, "", $4); print $4; exit }
|
||||
')
|
||||
[[ -z "$TS_FQDN" ]] && TS_FQDN="$(hostname).ts.net"
|
||||
|
||||
echo ""
|
||||
echo " ____ ____ _____ ____ __"
|
||||
echo "| _ \| _ \ / _ \ \/ /\ \ / /"
|
||||
echo "| |_) | |_) | | | \ / \ V /"
|
||||
echo "| __/| _ <| |_| / \ | |"
|
||||
echo "|_| |_| \_\\___/_/\_\ |_|"
|
||||
echo ""
|
||||
echo "Traefik v3 Reverse Proxy"
|
||||
echo "─────────────────────────────────────────"
|
||||
echo "Access:"
|
||||
echo " • Dashboard : https://${TS_FQDN} (Tailscale)"
|
||||
echo " • HTTP/HTTPS: Public ports 80/443"
|
||||
echo ""
|
||||
echo "Services:"
|
||||
docker ps --format ' • {{.Names}} : {{.Status}}' 2>/dev/null || echo " Docker not running"
|
||||
echo ""
|
||||
echo "Useful commands:"
|
||||
echo " cd ~/traefik && docker compose logs -f"
|
||||
echo " sudo tailscale serve status"
|
||||
echo "─────────────────────────────────────────"
|
||||
echo ""
|
||||
MOTD
|
||||
|
||||
TS_FQDN=$(tailscale status --json 2>/dev/null | awk -F'"' '
|
||||
/"Self"/ { in_self=1 }
|
||||
in_self && /"DNSName"/ { gsub(/\.$/, "", $4); print $4; exit }
|
||||
' || echo "${PROXY_HOSTNAME}.ts.net")
|
||||
|
||||
echo ""
|
||||
log_info "=========================================="
|
||||
log_info "Deployment complete!"
|
||||
log_info "=========================================="
|
||||
echo ""
|
||||
echo "Traefik dashboard : https://${TS_FQDN}"
|
||||
echo "Stack directory : $TRAEFIK_DIR"
|
||||
echo ""
|
||||
echo "Note: Approve exit-node in Tailscale admin console if needed."
|
||||
echo "Note: Fail2ban is running on the host; fail2ban-exporter exposes"
|
||||
echo " metrics on port 9191 (Tailscale-only, not public)."
|
||||
echo ""
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user