Compare commits

...
Sign in to create a new pull request.

2 commits

Author SHA1 Message Date
Hernâni Marques
4f74662202
pepsi: host-net highports + ensure mon-* users 2026-10-29 07:55:14 +01:00
Hernâni Marques
8370ad2e33
tor: CachyOS CTR CookieAuth same FP (TODO=704) 2026-10-21 03:32:38 +02:00
10 changed files with 491 additions and 7 deletions

View file

@ -0,0 +1,13 @@
# Castopod stays off
Rechecked 2026-10-21 after the stop note. Web unit container-koopa-castopod.service is disabled, inactive, dead. Container koopa-castopod is exited, finished 2026-10-21 22:48:06 +0200. Nothing listens on :9020. MariaDB and Redis user units stay loaded and active. No second stop. Caddy vhost unchanged.
Log review found no sign of a successful remote code execution. Overlay file /tmp/fw-cp.php (1448 bytes, mode 600, sha256 7bb279bffa12dbb81787183108a2641b78a6657b069cbef7967950d80e13b5bc, mtime 2026-09-07 03:25:59 +0200) is a local database script for cp_users, outside the docroot, not a web shell. /tmp/os-dry.out is an apt dry-run. Five theme files differ by one branding line from 2026-07-13. Media volume has no scripts. Access logs run 2026-10-17 through the stop and show no 2xx on a non-index script. Critical lines on 2026-09-07 match other days.
Limits: access logs start 2026-10-17, so HTTP from 2026-09-07 is absent. A memory-only or already rotated request is not ruled out. Whether the local script ran is not in the logs. MariaDB was not opened.
Stays down until an image ships PHP newer than 8.4.18. Hub tag :1 from 2026-02-24 is not that image. Parent 537, slug castopod-restart-after-php-fix.
Restore only then:
systemctl --user enable --now container-koopa-castopod.service

View file

@ -0,0 +1,23 @@
# Castopod PHP stopped
Checked 2026-10-21. Host has no php-fpm and no php package. The only PHP process was Castopod FrankenPHP in container koopa-castopod.
| Item | Value |
|------|--------|
| image | docker.io/castopod/castopod:1 |
| repo digest | sha256:4e4f0440520f45257bfeac7be4347defd20048b4efef8f53d73ec9ed3a4f7966 |
| amd64 digest | sha256:757f3c2a96b31b81753c23bc092dfcf05039d9df4d13cc12c428bb752fce882b |
| image created | 2026-02-24 |
| hub tags 1 / latest / 1.15.5 | same digest, pushed 2026-02-24 |
| PHP | 8.4.18 (12 Feb 2026) |
| FrankenPHP | v2.10.2 |
| listen | 0.0.0.0:9020 -> 8080 |
| public | https://castopod.hacktivism.ch/ |
Stable Hub tags have not moved since 2026-02-24. develop last push 2026-06-05 and is not a stable security update. php.net lists 8.4.26 (24 Sep 2026) and 8.4.27 (22 Oct 2026) after 8.4.18, with a banner to update for critical remote code execution. Pulling :1 repeats the same image. FrankenPHP ships its own PHP binary.
Stopped and disabled user unit container-koopa-castopod.service. ExecStop is `podman stop -t 30 koopa-castopod`. Container status after stop: Exited (0). :9020 not listening. MariaDB and Redis units left running. Caddy vhost unchanged.
Bring the web container back only when a newer image exists:
systemctl --user enable --now container-koopa-castopod.service

View file

@ -0,0 +1,41 @@
# TODO=704 Tor CachyOS CTR + CookieAuth (koopa)
Relay was down (stale PIDFile / start-timeout on forking unit), keys intact. Recreated **same identity** on CachyOS container; CookieAuth for ControlPort; host tor disabled.
## Stand after APPLY (2026-10-24 ~21:47–21:51 Europe/Zurich)
| Item | Value |
|------|--------|
| CTR | `koopa-tor-relay` Up, image `localhost/koopa-tor-relay:latest` (=`:cachyos`) |
| Tor | 0.4.9.14 (CachyOS/Arch extra) |
| Unit | `container-koopa-tor-relay.service` Type=simple, active |
| Host tor | `tor.service` disabled + failed/inactive |
| FP | `02C7EECCDF7814FBA2DB1CBAED000D06E8A32EE8` (keys mtime 2024-11-14) |
| ORPort | 8080 |
| ControlPort | 127.0.0.1:9051 CookieAuthentication=1, CookieAuthFile=/var/lib/tor/control_auth_cookie |
| Bootstrap | PROGRESS=100 done; status/circuit-established=1 |
| Unauth ControlPort | 514 Authentication required |
| Cookie AUTH | 250 OK |
## Steps
1. Dry-run `/tmp/tor.sh` on koopa → STATUS=dry-run-ok; NEXT=`sudo APPLY=1 /tmp/tor.sh` (host tor was still enabled).
2. Human: `systemctl disable --now tor.service`, then `sudo APPLY=1 /tmp/tor.sh`.
3. APPLY: wrote CachyOS Containerfile, pulled `docker.io/cachyos/cachyos:latest`, build (cache hit), `podman create --replace --network host --user 1000:1000 --userns keep-id`, torrc bak `torrc.bak-cookie-20261024-214720`, CookieAuth on.
4. First APPLY hit dbus fail under sudo (`$DBUS_SESSION_BUS_ADDRESS` / `$XDG_RUNTIME_DIR` unset). Unit rewrite + enable finished with XDG/DBUS set; script path re-exec as SUDO_USER for later runs.
5. Verify from magikoopa SSH: unit active, CTR Up, FP match, circuit-established=1, unauth 514.
## Live paths
- `~/koopa-tor-relay/{torrc,data,log,build/Containerfile}`
- `~/.config/systemd/user/container-koopa-tor-relay.service`
- Driver: `/tmp/tor.sh` (TARGET=koopa); upgrade later: `~/upgrade-containers.sh --apply --rebuild-local --only koopa-tor-relay`
## Akte mirrors
- `configs/tor/torrc`
- `configs/tor/build/Containerfile`
- `configs/systemd/user/container-koopa-tor-relay.service`
- `host/tor/README.md` updated (was stale host `tor.service`)
Sibling: firecuda brew tor CookieAuth same day (FP `52BB94DDC1292F950CF728708AC48523E018A718`, MyFamily both ways). Parent TODO=704.

View file

@ -0,0 +1,21 @@
# container-koopa-tor-relay.service — regenerated by tor.sh 2026-10-24T21:47:24+02:00
# Prefer simple over podman-generate-systemd forking+PIDFile (stale CID storm).
[Unit]
Description=Podman koopa-tor-relay
Wants=network-online.target
After=network-online.target
RequiresMountsFor=%t/containers
[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=always
RestartSec=5
TimeoutStartSec=120
TimeoutStopSec=70
ExecStart=/usr/bin/podman start --attach koopa-tor-relay
ExecStop=/usr/bin/podman stop -t 10 koopa-tor-relay
Type=simple
[Install]
WantedBy=default.target

View file

@ -0,0 +1,24 @@
# koopa-tor-relay — Tor non-exit relay on CachyOS (Arch extra/tor)
# Same pattern as koopa-meet-web:cachyos. Rootless: tor user -> uid/gid 1000.
# Recreate: --user 1000:1000 --userns=keep-id (see README / tor.sh)
ARG CACHYOS_IMAGE=docker.io/cachyos/cachyos:latest
FROM ${CACHYOS_IMAGE}
ENV TZ=Europe/Zurich
RUN pacman -Syu --noconfirm --needed tor nyx ca-certificates \
&& pacman -Scc --noconfirm \
&& rm -rf /var/cache/pacman/pkg/* /tmp/* \
&& mkdir -p /var/lib/tor /var/log/tor \
&& if id tor >/dev/null 2>&1; then \
groupmod -g 1000 tor 2>/dev/null || true; \
usermod -u 1000 -g 1000 tor 2>/dev/null || true; \
elif id debian-tor >/dev/null 2>&1; then \
groupmod -g 1000 debian-tor; usermod -u 1000 -g 1000 debian-tor; \
else \
groupadd -g 1000 tor; useradd -u 1000 -g 1000 -d /var/lib/tor -s /usr/bin/nologin tor; \
fi \
&& chown -R 1000:1000 /var/lib/tor /var/log/tor \
&& chmod 700 /var/lib/tor
EXPOSE 8080 9051
USER 1000:1000
ENTRYPOINT ["/usr/bin/tor"]
CMD ["-f", "/etc/tor/torrc", "--runasdaemon", "0"]

18
configs/tor/torrc Normal file
View file

@ -0,0 +1,18 @@
# KoopaRelay — same policy as live /etc/tor/torrc + admin-log configs/tor/torrc
# Identity: keys in DataDirectory (migrate from host /var/lib/tor)
Log notice file /var/log/tor/notices.log
Nickname KoopaRelay
ContactInfo info+koopa@hacktivism.ch
# dual-stack: bind all addresses on 8080 (IPv4 + IPv6), same as host tor
ORPort 8080
ControlPort 127.0.0.1:9051
CookieAuthentication 1
CookieAuthFile /var/lib/tor/control_auth_cookie
ExitRelay 0
SocksPort 0
MyFamily 52BB94DDC1292F950CF728708AC48523E018A718
BandwidthRate 2000 MBytes
BandwidthBurst 2000 MBytes
RelayBandwidthRate 2000 MBytes
RelayBandwidthBurst 2000 MBytes
DataDirectory /var/lib/tor

View file

@ -1,13 +1,30 @@
# Tor on koopa # Tor on koopa
**Runtime:** host **`tor.service`** (not a podman container). **Runtime:** rootless podman container **`koopa-tor-relay`** (CachyOS base, Arch `extra/tor`), managed by user unit **`container-koopa-tor-relay.service`** (`Type=simple`, `podman start --attach`).
**Ports:** ORPort **8080** · ControlPort **9051** (loopback). **Host `tor.service`:** disabled / must stay off (port clash on ORPort 8080 + ControlPort 9051).
**Non-exit** relay (`ExitRelay 0`). **Ports:** ORPort **8080** · ControlPort **127.0.0.1:9051** (CookieAuthentication + CookieAuthFile).
**Non-exit** relay (`ExitRelay 0`). Nickname **KoopaRelay**.
**Fingerprint (stable keys under `~/koopa-tor-relay/data/keys/`):** `02C7EECCDF7814FBA2DB1CBAED000D06E8A32EE8`
**Family:** firecuda FireCudaRelay `52BB94DDC1292F950CF728708AC48523E018A718`
Live tree: `~/koopa-tor-relay/` (`torrc`, `data/`, `log/`, `build/Containerfile`).
Mirrors in this repo: `configs/tor/torrc`, `configs/tor/build/Containerfile`, `configs/systemd/user/container-koopa-tor-relay.service`.
```bash ```bash
systemctl status tor # status
ss -lntp | grep -E '8080|9051' systemctl --user status container-koopa-tor-relay.service
podman ps --filter name=koopa-tor-relay
# ControlPort cookie auth (hex)
COOKIE=$(xxd -p -c 256 ~/koopa-tor-relay/data/control_auth_cookie)
printf 'AUTHENTICATE %s\r\nGETINFO fingerprint\r\nGETINFO status/circuit-established\r\nQUIT\r\n' "$COOKIE" | nc -w 3 127.0.0.1 9051
``` ```
login-health: WARN if `tor.service` not active. Apply / recreate (after host tor off):
Legacy `koopa-tor-relay` / `configs/tor/` removed from live inventory (git history keeps them).
```bash
sudo APPLY=1 /tmp/tor.sh
# later upgrades:
~/upgrade-containers.sh --apply --rebuild-local --only koopa-tor-relay
```
login-health: expect user unit active + CTR Up; host `tor.service` inactive. Parent TODO=704.

View file

@ -0,0 +1,57 @@
#!/usr/bin/env bash
# Idempotent: ensure mon-* OS users + Maildir inside koopa-pepsi.
# Needed after every container recreate: /home is not in the image and is
# not a volume by default → useradd state vanishes while DB login_assignment
# survives.
#
# UIDs fixed 1000–1004 (match login_assignment / prior live).
# STATUS= only; no secrets. Run on koopa as hernani (rootless podman exec).
set -euo pipefail
CTR="${PEPSI_CTR:-koopa-pepsi}"
USERS=(
"mon-smtp:1000"
"mon-maildir:1001"
"mon-dkim:1002"
"mon-bounce:1003"
"mon-local:1004"
)
echo "STATUS=start CTR=$CTR HOST=$(hostname -s)"
if ! podman container exists "$CTR" 2>/dev/null; then
echo "STATUS=missing_ctr CTR=$CTR"
exit 2
fi
if [[ "$(podman inspect -f '{{.State.Running}}' "$CTR" 2>/dev/null || echo false)" != "true" ]]; then
echo "STATUS=ctr_not_running CTR=$CTR"
exit 3
fi
created=0
present=0
for entry in "${USERS[@]}"; do
u="${entry%%:*}"
uid="${entry##*:}"
if podman exec "$CTR" getent passwd "$u" >/dev/null 2>&1; then
echo "USER=$u STATE=present"
present=$((present + 1))
else
podman exec -u root "$CTR" useradd -u "$uid" -U -m -d "/home/$u" -s /usr/sbin/nologin "$u"
echo "USER=$u STATE=created UID=$uid"
created=$((created + 1))
fi
# Maildir skeleton (cur/new/tmp) — safe if already present
podman exec -u root "$CTR" bash -c "
set -e
home=/home/$u
mkdir -p \"\$home/Maildir\"/{cur,new,tmp}
chown -R $u:$u \"\$home\"
chmod 700 \"\$home\"
"
echo "MAILDIR=$u OK=1"
done
echo "STATUS=done CREATED=$created PRESENT_BEFORE=$present EXPECT=${#USERS[@]}"
podman exec "$CTR" getent passwd | grep '^mon-' || true
exit 0

View file

@ -0,0 +1,66 @@
#!/usr/bin/env bash
# Open rootless pepsi high-ports on koopa (firewalld).
# Compose map (LAN bind 192.168.100.95):
# 1025→25 1465→465 1587→587 1993→993 18443→443
# Freigabe: human this session. Run on koopa as root / sudo.
set -euo pipefail
PORTS=(1025/tcp 1465/tcp 1587/tcp 1993/tcp 18443/tcp)
ZONE="${FIREWALLD_ZONE:-}"
if [[ ${EUID:-$(id -u)} -ne 0 ]]; then
echo "STATUS=need_root HINT='sudo $0'"
exit 2
fi
if ! command -v firewall-cmd >/dev/null 2>&1; then
echo "STATUS=no_firewalld"
exit 3
fi
if ! firewall-cmd --state >/dev/null 2>&1; then
echo "STATUS=firewalld_down"
exit 4
fi
if [[ -z "$ZONE" ]]; then
# Prefer the zone that owns the LAN NIC; else default.
ZONE=$(firewall-cmd --get-zone-of-interface=eno1 2>/dev/null || true)
if [[ -z "$ZONE" ]]; then
ZONE=$(firewall-cmd --get-default-zone)
fi
fi
echo "STATUS=start ZONE=$ZONE HOST=$(hostname -s)"
added=0
present=0
for p in "${PORTS[@]}"; do
if firewall-cmd --zone="$ZONE" --query-port="$p" >/dev/null 2>&1; then
echo "PORT=$p STATE=present"
present=$((present + 1))
else
firewall-cmd --zone="$ZONE" --add-port="$p" --permanent
echo "PORT=$p STATE=added_permanent"
added=$((added + 1))
fi
done
firewall-cmd --reload
echo "STATUS=reloaded"
ok=0
for p in "${PORTS[@]}"; do
if firewall-cmd --zone="$ZONE" --query-port="$p" >/dev/null 2>&1; then
echo "VERIFY=$p OK=1"
ok=$((ok + 1))
else
echo "VERIFY=$p OK=0"
fi
done
echo "STATUS=done ZONE=$ZONE ADDED=$added PRESENT_BEFORE=$present VERIFY_OK=$ok EXPECT=${#PORTS[@]}"
if [[ "$ok" -eq "${#PORTS[@]}" ]]; then
exit 0
fi
exit 1

View file

@ -0,0 +1,204 @@
#!/usr/bin/env bash
# Switch koopa-pepsi to network_mode:host on high ports so SMTP client IP
# is preserved (rootlessport SNAT otherwise forces 10.89.16.4 → SPF fail).
#
# Vecigate stays pure DNAT (25/465/587 → 1025/1465/1587). No mail on router.
# DB stays bridged; publish 127.0.0.1:15432→5432 for host-net pepsi.
#
# Freigabe: human this session (Script hinlegen + apply+restart).
# Run on koopa as hernani (rootless podman). Never echoes DB secrets.
set -euo pipefail
PEPSI_DIR="${PEPSI_DIR:-$HOME/koopa-pepsi}"
TS="$(date +%Y%m%d-%H%M%S)"
BAK="${PEPSI_DIR}/bak-hostnet-${TS}"
COMPOSE_BIN=""
echo "STATUS=start HOST=$(hostname -s) DIR=$PEPSI_DIR"
if [[ ! -d "$PEPSI_DIR" ]]; then
echo "STATUS=missing_dir PATH=$PEPSI_DIR"
exit 2
fi
cd "$PEPSI_DIR"
if command -v podman-compose >/dev/null 2>&1; then
COMPOSE_BIN="podman-compose"
elif podman compose version >/dev/null 2>&1; then
COMPOSE_BIN="podman compose"
else
echo "STATUS=no_compose"
exit 3
fi
if [[ ! -f compose.yml || ! -f conf/pepsi.conf ]]; then
echo "STATUS=missing_files"
exit 4
fi
already=0
if grep -q 'network_mode:[[:space:]]*host' compose.yml \
&& grep -qE '^PORT = 1025$' conf/pepsi.conf \
&& grep -qE '@127\.0\.0\.1:15432/' conf/pepsi.conf; then
already=1
echo "STATUS=already_hostnet HINT=skip_patch"
fi
if [[ "$already" -eq 0 ]]; then
mkdir -p "$BAK"
cp -a compose.yml conf/pepsi.conf "$BAK/"
echo "STATUS=bak PATH=$BAK"
python3 - <<'PY'
from pathlib import Path
import re
import sys
root = Path(".")
compose_path = root / "compose.yml"
conf_path = root / "conf" / "pepsi.conf"
compose = compose_path.read_text()
conf = conf_path.read_text()
# Split services crudely on "\n pepsi:" / "\n db:"
def patch_compose(text: str) -> str:
# Retarget env DB URLs first (password stays ${VAR})
text = text.replace("@db:5432/", "@127.0.0.1:15432/")
# Ensure db publishes 127.0.0.1:15432:5432
if "15432:5432" not in text:
text = re.sub(
r"(container_name: koopa-pepsi-db\n)",
r'\1 ports:\n - "127.0.0.1:15432:5432"\n',
text,
count=1,
)
# pepsi service: add network_mode host; remove its ports: block
# Match from " pepsi:" until next top-level service or "volumes:"
m = re.search(r"(?ms)^ pepsi:\n(.*?)(?=^volumes:|^ [a-z])", text)
if not m:
print("STATUS=pepsi_service_not_found")
sys.exit(5)
body = m.group(1)
# drop ports block inside pepsi
body2 = re.sub(r"(?m)^ ports:\n(?: [^\n]*\n)+", "", body)
if "network_mode:" not in body2:
body2 = re.sub(
r"(container_name: koopa-pepsi\n)",
r"\1 network_mode: host\n",
body2,
count=1,
)
text = text[: m.start(1)] + body2 + text[m.end(1) :]
return text
def set_port(text: str, section: str, port: int) -> str:
pat = rf"(\[{re.escape(section)}\]\n(?:.*\n)*?PORT = )\d+"
new, n = re.subn(pat, rf"\g<1>{port}", text, count=1)
if n != 1:
print(f"STATUS=port_patch_fail SECTION={section} N={n}")
sys.exit(6)
return new
compose = patch_compose(compose)
conf = conf.replace("@db:5432/", "@127.0.0.1:15432/")
conf = set_port(conf, "pepsi-ingress-listener-mx", 1025)
conf = set_port(conf, "pepsi-ingress-listener-submissions", 1465)
conf = set_port(conf, "pepsi-ingress-listener-submission", 1587)
conf = set_port(conf, "pepsi-httpd-listener-https", 18443)
compose_path.write_text(compose)
conf_path.write_text(conf)
# sanity STATUS (no secrets)
checks = [
("network_mode_host", bool(re.search(r"(?m)^ network_mode:\s*host\s*$", compose))),
("db_pub_15432", "15432:5432" in compose),
("no_db_hostname", "@db:5432/" not in compose and "@db:5432/" not in conf),
("port_1025", bool(re.search(r"(?m)^PORT = 1025\s*$", conf))),
("port_1465", bool(re.search(r"(?m)^PORT = 1465\s*$", conf))),
("port_1587", bool(re.search(r"(?m)^PORT = 1587\s*$", conf))),
("port_18443", bool(re.search(r"(?m)^PORT = 18443\s*$", conf))),
("pepsi_ports_gone", "192.168.100.95:1025" not in compose),
]
for k, v in checks:
print(f"CHECK={k} OK={int(v)}")
if not all(v for _, v in checks):
print("STATUS=patch_sanity_fail")
sys.exit(7)
print("STATUS=patched")
PY
fi
echo "STATUS=compose_up BIN=$COMPOSE_BIN"
# shellcheck disable=SC2086
$COMPOSE_BIN up -d --force-recreate
sleep 4
echo "STATUS=verify"
ss_out="$(ss -lntp 2>/dev/null | grep -E ':1025|:1465|:1587|:18443|:15432' || true)"
# redact pid/user clutter lightly
echo "$ss_out" | sed -E 's/users:\(\("([^"]+)".*/users=(("\1"…)/' || true
rootless_hit=0
if echo "$ss_out" | grep -q rootlessport; then
rootless_hit=1
fi
db_pub=0
if echo "$ss_out" | grep -qE '127\.0\.0\.1:15432'; then
db_pub=1
fi
mx_ok=0
if echo "$ss_out" | grep -qE ':1025\b'; then
mx_ok=1
fi
podman ps --format '{{.Names}} {{.Status}}' | grep pepsi || true
if command -v pg_isready >/dev/null 2>&1; then
if pg_isready -h 127.0.0.1 -p 15432 -d pepsi >/dev/null 2>&1; then
echo "STATUS=db_ready HOST=127.0.0.1 PORT=15432"
else
echo "STATUS=db_not_ready HOST=127.0.0.1 PORT=15432"
fi
else
if timeout 2 bash -c 'echo >/dev/tcp/127.0.0.1/15432' 2>/dev/null; then
echo "STATUS=db_tcp_open HOST=127.0.0.1 PORT=15432"
else
echo "STATUS=db_tcp_closed HOST=127.0.0.1 PORT=15432"
fi
fi
# recent log lines (strip any postgres URLs)
podman logs --tail 40 koopa-pepsi 2>/dev/null \
| grep -Ei 'listen|error|fatal|spf|client|1025|postgres|ready' \
| sed -E 's#postgres://[^[:space:]]+#postgres://*#g' \
| tail -12 || true
echo "STATUS=done MX_1025=$mx_ok DB_PUB=$db_pub ROOTLESSPORT_ON_HIGH=$rootless_hit BAK=${BAK:-none}"
echo "HINT=next ensure-mon-users.sh after recreate; then taler-mail send --expect-pong"
echo "HINT=Vecigate DNAT unchanged (no mail on router); /home ephemeral unless volume+ensure"
# Optional: run ensure if sibling script present (same dir or PEPSI_DIR/scripts)
ENSURE=""
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
for cand in "$here/ensure-mon-users.sh" "$PEPSI_DIR/scripts/ensure-mon-users.sh" "$PEPSI_DIR/ensure-mon-users.sh"; do
if [[ -x "$cand" ]]; then
ENSURE="$cand"
break
fi
done
if [[ -n "$ENSURE" ]]; then
echo "STATUS=ensure_mon_users SCRIPT=$ENSURE"
PEPSI_CTR=koopa-pepsi "$ENSURE" || echo "STATUS=ensure_warn"
fi
if [[ "$mx_ok" -eq 1 && "$rootless_hit" -eq 0 && "$db_pub" -eq 1 ]]; then
exit 0
fi
if [[ "$mx_ok" -eq 1 && "$db_pub" -eq 1 ]]; then
echo "STATUS=warn_rootlessport_still_present"
exit 0
fi
exit 1