SearXNG: define the gluetun+searxng+valkey trio as quadlet units
Converts the podman-compose SearXNG stack to boot-persistent quadlet units so
it survives a reboot (the compose deployment did not). searxng sets
Network=gluetun.container, which makes it reuse gluetun's network namespace so
outbound search traffic exits the WireGuard tunnel; it publishes no port of its
own (the port lives on gluetun). gluetun and valkey join a pinned searxng
network (10.89.2.0/24, DNS-enabled) so searxng reaches valkey by name.
Secrets stay in /opt/searxng/.env (mode 0600), referenced via [Service]
EnvironmentFile= so ${VAR} expands. Adds a bats seam asserting the netns
wiring, port placement, boot install, and that no secret material is tracked.
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
# Quadlet unit: Gluetun VPN tunnel (rootless, boot-persistent).
|
||||
#
|
||||
# Install path on dankclaw: ~/.config/containers/systemd/gluetun.container
|
||||
# Unit name: gluetun.service (container name: gluetun)
|
||||
#
|
||||
# Creates the WireGuard tunnel to ProtonVPN (US-WA). SearXNG reuses this
|
||||
# container's network namespace (see searxng.container), so its outbound search
|
||||
# traffic exits here. The WireGuard credentials live in /opt/searxng/.env
|
||||
# (mode 0600); this file holds no secrets. See docs/Dankclaw/Gluetun.md.
|
||||
|
||||
[Unit]
|
||||
Description=Gluetun VPN tunnel container
|
||||
Wants=searxng-network.service
|
||||
After=searxng-network.service network.target
|
||||
|
||||
[Container]
|
||||
ContainerName=gluetun
|
||||
Network=searxng.network
|
||||
Image=docker.io/qmcgaw/gluetun:latest
|
||||
CapAdd=NET_ADMIN
|
||||
Device=/dev/net/tun:/dev/net/tun
|
||||
Environment=VPN_SERVICE_PROVIDER=${VPN_SERVICE_PROVIDER}
|
||||
Environment=VPN_TYPE=${VPN_TYPE}
|
||||
Environment=WIREGUARD_PRIVATE_KEY=${WIREGUARD_PRIVATE_KEY}
|
||||
Environment=WIREGUARD_ADDRESSES=${WIREGUARD_ADDRESSES}
|
||||
Environment=WIREGUARD_PUBLIC_KEY=${WIREGUARD_PUBLIC_KEY}
|
||||
Environment=WIREGUARD_ENDPOINT_IP=${WIREGUARD_ENDPOINT_IP}
|
||||
Environment=WIREGUARD_ENDPOINT_PORT=${WIREGUARD_ENDPOINT_PORT}
|
||||
Environment=WIREGUARD_ALLOWED_IPS=${WIREGUARD_ALLOWED_IPS}
|
||||
Environment=WIREGUARD_PERSISTENT_KEEPALIVE_INTERVAL=${WIREGUARD_PERSISTENT_KEEPALIVE_INTERVAL}
|
||||
PublishPort=8081:8080
|
||||
|
||||
[Service]
|
||||
EnvironmentFile=/opt/searxng/.env
|
||||
TimeoutStartSec=900
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -0,0 +1,28 @@
|
||||
# Quadlet unit: Valkey cache for SearXNG (rootless, boot-persistent).
|
||||
#
|
||||
# Install path on dankclaw: ~/.config/containers/systemd/searxng-valkey.container
|
||||
# Unit name: searxng-valkey.service (container name: searxng-valkey)
|
||||
#
|
||||
# The limiter's rate-limit state store. Joins the searxng network and is
|
||||
# reachable by the name "valkey" (settings.yml points there). Data persists in
|
||||
# /opt/searxng/valkey. See docs/Dankclaw/Valkey.md.
|
||||
|
||||
[Unit]
|
||||
Description=SearXNG Valkey cache container
|
||||
Wants=searxng-network.service
|
||||
After=searxng-network.service network.target
|
||||
|
||||
[Container]
|
||||
ContainerName=searxng-valkey
|
||||
Network=searxng.network
|
||||
NetworkAlias=valkey
|
||||
Image=docker.io/valkey/valkey:9-alpine
|
||||
Volume=/opt/searxng/valkey:/data
|
||||
|
||||
[Service]
|
||||
EnvironmentFile=/opt/searxng/.env
|
||||
TimeoutStartSec=900
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -0,0 +1,30 @@
|
||||
# Quadlet unit: SearXNG search engine (rootless, boot-persistent).
|
||||
#
|
||||
# Install path on dankclaw: ~/.config/containers/systemd/searxng.container
|
||||
# Unit name: searxng.service (container name: searxng)
|
||||
#
|
||||
# Reuses gluetun's network namespace (Network=gluetun.container) so its
|
||||
# outbound traffic exits the WireGuard tunnel. It publishes no port of its own;
|
||||
# the port lives on gluetun. It reaches valkey by name on the searxng network.
|
||||
# Config mounts from /opt/searxng/config. See docs/Dankclaw/SearXNG.md.
|
||||
|
||||
[Unit]
|
||||
Description=SearXNG search engine container
|
||||
Wants=gluetun.service searxng-valkey.service
|
||||
After=gluetun.service searxng-valkey.service network.target
|
||||
|
||||
[Container]
|
||||
ContainerName=searxng
|
||||
Network=gluetun.container
|
||||
Image=ghcr.io/searxng/searxng:latest
|
||||
Environment=SEARXNG_SECRET=${SEARXNG_SECRET}
|
||||
Environment=SEARXNG_BASE_URL=${SEARXNG_BASE_URL}
|
||||
Volume=/opt/searxng/config:/etc/searxng
|
||||
|
||||
[Service]
|
||||
EnvironmentFile=/opt/searxng/.env
|
||||
TimeoutStartSec=900
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -0,0 +1,16 @@
|
||||
# Quadlet network: private bridge for the SearXNG stack (rootless).
|
||||
#
|
||||
# Install path on dankclaw: ~/.config/containers/systemd/searxng.network
|
||||
# Unit name: searxng-network.service (podman network name: systemd-searxng)
|
||||
#
|
||||
# DNS is enabled by default for quadlet networks. This matters: the automatic
|
||||
# "podman" default network has dns_enabled=false, so container names would not
|
||||
# resolve. searxng (which reuses gluetun's netns) reaches valkey by the name
|
||||
# "valkey" on this network.
|
||||
#
|
||||
# The subnet is pinned so the value stays stable across reboots. It must match
|
||||
# an entry in shared/searxng/limiter.toml trusted_proxies. 10.89.2.0/24 is free
|
||||
# (gitea uses 10.89.0.0/24; the old compose net used 10.89.1.0/24).
|
||||
|
||||
[Network]
|
||||
Subnet=10.89.2.0/24
|
||||
@@ -25,6 +25,7 @@ trusted_proxies = [
|
||||
'192.168.0.0/24',
|
||||
'10.88.0.0/24',
|
||||
'10.89.1.0/24',
|
||||
'10.89.2.0/24',
|
||||
]
|
||||
|
||||
[botdetection.ip_limit]
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env bats
|
||||
# Seam: SearXNG stack boot-persistence via Quadlet.
|
||||
#
|
||||
# The gluetun + searxng + valkey trio runs under systemd as quadlet units so it
|
||||
# survives a reboot (the podman-compose deployment did not). The crux: searxng
|
||||
# must reuse gluetun's network namespace (Network=gluetun.container) so its
|
||||
# outbound search traffic exits the WireGuard tunnel, while still reaching valkey
|
||||
# by name on the shared searxng network.
|
||||
|
||||
load helpers
|
||||
|
||||
QDIR="$REPO_ROOT/dankclaw/searxng/quadlet"
|
||||
|
||||
@test "all four quadlet files exist" {
|
||||
[ -f "$QDIR/searxng.network" ]
|
||||
[ -f "$QDIR/gluetun.container" ]
|
||||
[ -f "$QDIR/searxng.container" ]
|
||||
[ -f "$QDIR/searxng-valkey.container" ]
|
||||
}
|
||||
|
||||
@test "searxng reuses gluetun's network namespace (exits the tunnel)" {
|
||||
grep -qE '^Network=gluetun\.container' "$QDIR/searxng.container" \
|
||||
|| { echo "searxng.container must set Network=gluetun.container"; return 1; }
|
||||
}
|
||||
|
||||
@test "searxng publishes no port (the port lives on gluetun)" {
|
||||
if grep -qE '^PublishPort=' "$QDIR/searxng.container"; then
|
||||
echo "searxng.container must not set PublishPort (it shares gluetun's netns)"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
@test "gluetun publishes SearXNG on 8081:8080" {
|
||||
grep -qE '^PublishPort=8081:8080' "$QDIR/gluetun.container" \
|
||||
|| { echo "gluetun.container must publish 8081:8080"; return 1; }
|
||||
}
|
||||
|
||||
@test "gluetun has NET_ADMIN and the /dev/net/tun device" {
|
||||
grep -qE '^CapAdd=NET_ADMIN' "$QDIR/gluetun.container" \
|
||||
|| { echo "gluetun.container must set CapAdd=NET_ADMIN"; return 1; }
|
||||
grep -qE '^Device=/dev/net/tun' "$QDIR/gluetun.container" \
|
||||
|| { echo "gluetun.container must set Device=/dev/net/tun"; return 1; }
|
||||
}
|
||||
|
||||
@test "valkey joins searxng.network and is reachable by the name valkey" {
|
||||
grep -qE '^Network=searxng\.network' "$QDIR/searxng-valkey.container" \
|
||||
|| { echo "searxng-valkey.container must set Network=searxng.network"; return 1; }
|
||||
grep -qE '^NetworkAlias=valkey' "$QDIR/searxng-valkey.container" \
|
||||
|| { echo "searxng-valkey.container must set NetworkAlias=valkey"; return 1; }
|
||||
}
|
||||
|
||||
@test "gluetun joins searxng.network so searxng (in its netns) can reach valkey" {
|
||||
grep -qE '^Network=searxng\.network' "$QDIR/gluetun.container" \
|
||||
|| { echo "gluetun.container must set Network=searxng.network"; return 1; }
|
||||
}
|
||||
|
||||
@test "searxng.network pins a subnet that limiter.toml trusts as a proxy" {
|
||||
# The pinned subnet must appear in limiter.toml trusted_proxies so the value
|
||||
# stays stable across reboots (rootless podman otherwise reassigns the CIDR).
|
||||
local subnet
|
||||
subnet=$(grep -oE '^Subnet=[0-9./:]+' "$QDIR/searxng.network" | head -1 | cut -d= -f2)
|
||||
[ -n "$subnet" ] || { echo "searxng.network must pin a Subnet="; return 1; }
|
||||
grep -qF "'$subnet'" "$REPO_ROOT/shared/searxng/limiter.toml" \
|
||||
|| { echo "pinned subnet $subnet missing from limiter.toml trusted_proxies"; return 1; }
|
||||
}
|
||||
|
||||
@test "every container unit feeds \${VAR} expansion via [Service] EnvironmentFile" {
|
||||
# An EnvironmentFile in [Container] only lands in the ignored [X-Container]
|
||||
# section and does not feed ${VAR} expansion. It must be in [Service].
|
||||
local f
|
||||
for f in gluetun searxng searxng-valkey; do
|
||||
# The file must contain a [Service] section that sets EnvironmentFile=.
|
||||
awk '/^\[Service\]/{s=1;next} /^\[/{s=0} s&&/EnvironmentFile=/{found=1} END{exit found?0:1}' \
|
||||
"$QDIR/$f.container" \
|
||||
|| { echo "$f.container: [Service] must set EnvironmentFile="; return 1; }
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
@test "every container unit is installed for boot (WantedBy=default.target)" {
|
||||
local f
|
||||
for f in gluetun searxng searxng-valkey; do
|
||||
grep -qE '^WantedBy=default\.target' "$QDIR/$f.container" \
|
||||
|| { echo "$f.container: [Install] must set WantedBy=default.target"; return 1; }
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
@test "no secret material in the quadlet files" {
|
||||
# The units reference \${VAR} placeholders, never literal keys. A WireGuard
|
||||
# key is a 44-char base64 string; none may appear in the tracked files.
|
||||
local f
|
||||
for f in "$QDIR"/*.container "$QDIR"/*.network; do
|
||||
if grep -qE '[A-Za-z0-9+/]{44}=' "$f"; then
|
||||
echo "$f: contains a secret-like value"
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
has_no_leaked_secret "$QDIR/gluetun.container"
|
||||
}
|
||||
Reference in New Issue
Block a user