Compare commits

..

6 Commits

Author SHA1 Message Date
yukkop c439c1b948 fix: ssh extra debug
runner nix smoke / nix label and flake smoke (push) Failing after 57s
2026-09-26 19:11:41 +00:00
yukkop 0fe85c67d9 ci: staging server
runner nix smoke / nix label and flake smoke (push) Failing after 50s
2026-09-26 13:59:59 +00:00
yukkop 09eecf5969 feat: lfs
runner nix smoke / nix label and flake smoke (push) Failing after 1m59s
2026-09-24 23:15:43 +00:00
yukkop e444ea5936 fix: world-of-sosal
runner nix smoke / nix label and flake smoke (push) Failing after 59s
2026-09-23 19:26:19 +00:00
yukkop c917e4908c feat: zomboid backups
runner nix smoke / nix label and flake smoke (push) Failing after 54s
2026-09-22 17:15:18 +00:00
yukkop ef849b085f fix: wow-minecraft: finish configurations
runner nix smoke / nix label and flake smoke (push) Failing after 56s
2026-09-19 09:04:05 +00:00
17 changed files with 987 additions and 91 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ keys:
- &bfs-server age15yzgmsvl3ku2w863h6gw2vpmw37m9aruv6xrj4fue6n2jpm7pyuqk9xjmj - &bfs-server age15yzgmsvl3ku2w863h6gw2vpmw37m9aruv6xrj4fue6n2jpm7pyuqk9xjmj
- &bfs-pol-server age1fpytf05sg9n6ywpwkmn09lhpfvgtud9h75h76jhxha475zpnasqq952rpu - &bfs-pol-server age1fpytf05sg9n6ywpwkmn09lhpfvgtud9h75h76jhxha475zpnasqq952rpu
- &bfs-new-server age17yx98qk9gzgcf2q6zhhp05p6mmtrkgz66dvyk9gqclypvlr8rersxjy5v7 - &bfs-new-server age17yx98qk9gzgcf2q6zhhp05p6mmtrkgz66dvyk9gqclypvlr8rersxjy5v7
- &neuro-server age15yzgmsvl3ku2w863h6gw2vpmw37m9aruv6xrj4fue6n2jpm7pyuqk9xjmj - &neuro-server age1ak7heljpr0pjr4m0rcwxgn3sp0jjxw03lxyf33r8lcemqh2u2sgqx0aplq
- &games-server age15yzgmsvl3ku2w863h6gw2vpmw37m9aruv6xrj4fue6n2jpm7pyuqk9xjmj - &games-server age15yzgmsvl3ku2w863h6gw2vpmw37m9aruv6xrj4fue6n2jpm7pyuqk9xjmj
- &hectic-lab-server age13h8twnwvgxn04l5ywtru89a6psw5d0uckr2eghxsjp88a5augvsstq5ard - &hectic-lab-server age13h8twnwvgxn04l5ywtru89a6psw5d0uckr2eghxsjp88a5augvsstq5ard
- &umbriel-bfs age1jxntjca8q2vxvf2jaal4xyvm2ae6sh62fhv897694kuzawfrk5asj00zdt - &umbriel-bfs age1jxntjca8q2vxvf2jaal4xyvm2ae6sh62fhv897694kuzawfrk5asj00zdt
+2
View File
@@ -1,6 +1,8 @@
# Documentation # Documentation
- [Using the `hectic` Attic Cache](./attic-cache.md) - [Using the `hectic` Attic Cache](./attic-cache.md)
- [Minecraft incident log](./minecraft-incidents.md)
- [Project Zomboid backups](./project-zomboid-backups.md)
## Gitea runner labels ## Gitea runner labels
+101
View File
@@ -0,0 +1,101 @@
# Minecraft incident log
This file records only observed evidence, actions, and verification results.
An entity appearing in a stack trace is a trigger-path observation, not a
proven root cause.
## 2026-09-19 — WorldOfSosal crashes in Sable block-change handling
### Impact
- `minecraft-server-wowMineMap.service` terminates while a player is online.
- Public Minecraft endpoint is `store.hectic-lab.com:25568`.
- Server is intentionally stopped after the latest crash to prevent repeated
crash-save cycles while recovery is investigated.
### Observed evidence
All crash reports contain `sable@2.0.5` in
`LevelAccelerator.getBlockState`, followed by
`ArrayIndexOutOfBoundsException` where the requested section index exceeds
the world section array length of `24`.
| UTC timestamp | Crash report | Observed trigger path | Exception |
| --- | --- | --- | --- |
| 18:47:03 | `crash-2026-09-19_18.47.03-server.txt` | `EnderMan$EndermanTakeBlockGoal.tick` | index `38` / length `24` |
| 18:52:17 | `crash-2026-09-19_18.52.17-server.txt` | `GlowSquid.aiStep` → `RedStoneOreBlock.stepOn` | index `33` / length `24` |
| 19:14:46 | `crash-2026-09-19_19.14.46-server.txt` | `Skeleton.tick` → `RedStoneOreBlock.stepOn` | index `34` / length `24` |
Evidence locations on `neuro`:
```text
/srv/minecraft/wowMineMap/crash-reports/
/srv/minecraft/wowMineMap/logs/latest.log
```
### Actions performed
| UTC timestamp | Action | Result |
| --- | --- | --- |
| 17:51 | Archived current world before recovery | Archive checksum recorded |
| 18:08 | Set `randomTickSpeed=0` | Server started, but later crashed from an entity block change |
| 18:48 | Set `mobGriefing=false` | Prevented Enderman block pickup only; later crashes still occurred |
| 18:54 | Archived post-crash world | Archive checksum recorded |
| 19:00 | Moved Boss offline player NBT from `(3299.067, 142.630, 8613.742)` to `(3296, 500, 8608)` in `crafting_azeroth:azeroth` | Only `Pos` and `Dimension` changed; later crash still occurred |
| after 19:14 crash | Stopped `minecraft-server-wowMineMap.service` | Prevented further automatic crash/restart saves |
### Recovery artifacts
```text
/srv/minecraft/backups/wowMineMap-before-sable-recovery-20260919T175139Z.tar.zst
/srv/minecraft/backups/wowMineMap-after-sable-crashes-20260919T185445Z.tar.zst
/srv/minecraft/wowMineMap/world/playerdata/1c189af5-2713-3fa6-bcc4-893dfadedfa4.dat.before-relocation
```
### Conclusions supported by evidence
- Public proxy and reverse tunnel are not the failure point: server-list ping
succeeded before later in-world crashes.
- The failure is not limited to Endermen, random ticks, or one player
position.
- Sable's block-change callback is present in every captured crash.
### Not established
- Exact corrupt chunk, block, or mod data.
- Whether world data is corrupt, Sable itself is defective, or another mod is
supplying incompatible world state.
- Whether deleting any chunk, region, or Sable state would be safe.
### External research
No exact upstream match was found for Sable `2.0.5` on NeoForge `1.21.1` with
`LevelAccelerator.getBlockState` and a requested section index of `33`, `34`,
or `38` against a section array of length `24`.
Related but non-identical upstream reports:
- [Sable #776](https://github.com/ryanhcode/sable/issues/776) documents an
`ArrayIndexOutOfBoundsException` associated with unusual dimension height
bounds. This is relevant to section-coordinate handling, but is an older
version and different stack trace.
- [Sable #1087](https://github.com/ryanhcode/sable/issues/1087) documents a
`LevelAccelerator.getBlockState` recursion during block-shape processing.
The failure type differs.
- [Sable #820](https://github.com/ryanhcode/sable/issues/820) documents a
ticking-entity block-change crash. The reported downgrade to `1.1.3` helped
that distinct recursive-update failure; it is not evidence for this crash.
- [Sable #1223](https://github.com/ryanhcode/sable/issues/1223) documents a
different `ArrayIndexOutOfBoundsException` in voxel-neighborhood handling.
Its suggested Lithium setting only reduced crashes for some reporters and is
not a verified mitigation here.
Sable `2.0.4` and `2.0.5` release notes mention other block or contraption
crash fixes, but not this exception. No version upgrade or downgrade is
currently evidence-backed as a production fix.
### Next recovery step
Use a disposable full-world copy to test a supported Sable/physics integration
mitigation. Do not restart production, delete region files, or overwrite a
backup until that test gives reproducible evidence.
+55 -9
View File
@@ -1,7 +1,7 @@
# WorldOfSosal: Prism automatic updates # WorldOfSosal: Prism automatic updates
The published client entry points are: The published client entry points are:
- https://bfs.band/minecraft/ (BFS / Element host) - https://store.bfs.band/minecraft/ (BFS / Element host)
- https://store.hectic-lab.com/minecraft/world-of-sosal/ (hectic-lab) - https://store.hectic-lab.com/minecraft/world-of-sosal/ (hectic-lab)
Each site provides its own Prism ZIP with that site's update URL and matching Each site provides its own Prism ZIP with that site's update URL and matching
@@ -95,8 +95,8 @@ Both services and firewall rules are in Nix and start on boot. The SSH client
uses an explicit AES-CTR / HMAC-SHA256-ETM / curve25519 transport profile with uses an explicit AES-CTR / HMAC-SHA256-ETM / curve25519 transport profile with
IPQoS=none, tested on the neuro-to-lab route. The default profile stalled after IPQoS=none, tested on the neuro-to-lab route. The default profile stalled after
the handshake on this route. Both ends check peer liveness so stale listeners the handshake on this route. Both ends check peer liveness so stale listeners
are eventually released. Minecraft retains are eventually released. Minecraft initially used `online-mode=true`. It now uses offline mode at the
`online-mode=true`, requiring normal authenticated Minecraft accounts. owner's request; see the RCON and authentication section below.
For a temporary direct local tunnel, use: For a temporary direct local tunnel, use:
@@ -156,22 +156,22 @@ SHA-256 was verified before extraction.
## Independent BFS entry point (2026-09-19) ## Independent BFS entry point (2026-09-19)
- Server: `bfs.band:25568`; downloads: https://bfs.band/minecraft/. - Server: `wow.bfs.band`; downloads: https://store.bfs.band/minecraft/.
- BFS is `bfs.poland.xray` (91.198.166.181), the host of Element. - BFS is `bfs.poland.xray` (91.198.166.181), the host of Element.
- `minecraft-wow-tunnel-bfs` connects neuro directly to BFS. The BFS path does - `minecraft-wow-tunnel-bfs` connects neuro directly to BFS. The BFS path does
not transit hectic-lab; both tunnels have independent reconnecting services. not transit hectic-lab; both tunnels have independent reconnecting services.
- Shared proxy implementation: `nixos/module/generic/minecraft-public-relay.nix`. - Shared proxy implementation: `nixos/module/generic/minecraft-public-relay.nix`.
Host settings remain in `minecraft-wow-proxy.nix` (hectic-lab) and Host settings remain in `minecraft-wow-proxy.nix` (hectic-lab) and
`minecraft-wow.nix` (BFS). Only `/minecraft/` is added to the existing BFS `minecraft-wow.nix` (BFS). A dedicated HTTPS virtual host serves `store.bfs.band`. The legacy
nginx virtual host; Element/Matrix routes remain intact. `bfs.band/minecraft/` URLs remain available for already imported instances.
- Downloaded BFS ZIP seeds `bfs.band:25568` and uses the stable manifest - Downloaded BFS ZIP seeds `wow.bfs.band` and uses the stable manifest
`https://bfs.band/minecraft/world-of-sosal/current/pack.toml`. It does not `https://store.bfs.band/minecraft/world-of-sosal/current/pack.toml`. It does not
redirect installation metadata to hectic-lab. Upstream mod and Java/loader redirect installation metadata to hectic-lab. Upstream mod and Java/loader
downloads still use their original providers (e.g. Modrinth, GitHub, Mojang). downloads still use their original providers (e.g. Modrinth, GitHub, Mojang).
- Existing hectic-lab instances can be migrated without reinstalling mods: - Existing hectic-lab instances can be migrated without reinstalling mods:
in Edit / Settings / Custom commands, replace only the manifest URL in in Edit / Settings / Custom commands, replace only the manifest URL in
Pre-launch command with the BFS URL above. Change the multiplayer server Pre-launch command with the BFS URL above. Change the multiplayer server
address to bfs.band:25568. New users should import the ZIP from BFS. address to wow.bfs.band. New users should import the ZIP from BFS.
- `script/publish-prism-mirrors.py` builds host-specific ZIPs from one archive - `script/publish-prism-mirrors.py` builds host-specific ZIPs from one archive
and publishes both mirrors. It checks that the running neuro server's cached and publishes both mirrors. It checks that the running neuro server's cached
archive has the same SHA-256. Each host's switch is atomic; publication across archive has the same SHA-256. Each host's switch is atomic; publication across
@@ -183,3 +183,49 @@ Clean installation through the BFS manifest passed: all 141 client mods and
all overrides match the source archive. A second updater run performed no all overrides match the source archive. A second updater run performed no
downloads and preserved options.txt. The public BFS login protocol reached downloads and preserved options.txt. The public BFS login protocol reached
online authentication; the earlier full GUI login used hectic-lab. online authentication; the earlier full GUI login used hectic-lab.
## BFS DNS and dedicated download site (2026-09-19)
Porkbun DNS, TTL 600:
| Type | Name | Value |
| --- | --- | --- |
| A | store.bfs.band | 91.198.166.181 |
| A | wow.bfs.band | 91.198.166.181 |
| SRV | _minecraft._tcp.wow.bfs.band | 0 0 25568 wow.bfs.band |
Players enter `wow.bfs.band` without a port in Minecraft Java. In Porkbun,
SRV Priority is `0`, and Target is `0 25568 wow.bfs.band` (weight, port, host).
The root download URL https://store.bfs.band/ redirects to the WorldOfSosal page.
The NixOS virtual host obtains and renews its HTTPS certificate automatically.
The publication script now seeds this update URL and the port-free game address.
Existing BFS instances retain working legacy update URLs; switching their
pre-launch manifest to the new store host is optional. Root bfs.band remains
the existing Element entry point.
## RCON and authentication (2026-09-19)
The WoW server now has `online-mode=false`. Account authentication is disabled;
player names can be impersonated, and offline UUIDs differ from online UUIDs.
Existing inventory/permissions may require a separate UUID migration.
RCON listens on TCP 25575 on neuro; its port is not opened in the firewall or
forwarded through the public Minecraft relays. The server-specific automatic
firewall is disabled and only game port 25567 is explicitly permitted.
A random password is stored in SOPS as `minecraft/rcon-password`, injected into
server.properties at startup with mode 0600, and is absent from the Nix store.
Start a local-only SSH tunnel and leave it running:
```sh
ssh -NT -L 127.0.0.1:25575:127.0.0.1:25575 -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=3 neuro
```
Retrieve the password in another terminal (do not paste it into logs):
```sh
ssh neuro cat /run/secrets/minecraft/rcon-password
```
Configure the RCON client with host `127.0.0.1`, port `25575`, and that password.
There is no RCON username. These changes apply to wowMineMap only.
+96
View File
@@ -0,0 +1,96 @@
# Project Zomboid backups
`hectic.services."project-zomboid".backup` creates local backups without stopping
or pausing the server. The default schedule is every 30 minutes. Each run:
1. sends the local RCON `save` command and waits for the configured save grace
period;
2. rsyncs `Zomboid/Saves/Multiplayer/<serverName>` and non-secret server
settings (`SandboxVars`, spawn-points, and spawn-regions) from
`Zomboid/Server` into a private staging tree;
3. waits five seconds and repeats the rsync to narrow the live-write window;
4. publishes a timestamped `tar.zst` archive; and
5. deletes local archives older than `backup.retentionDays`.
The service lock prevents overlapping runs. Missing save or server-config paths
skip the run through systemd `ConditionPathExists` checks.
## Consistency and secrets
This is a best-effort backup. It does not stop Project Zomboid and does not use
an atomic filesystem snapshot. The RCON save command flushes the world before
copying, and the second rsync narrows the remaining live-write window, but
neither makes the filesystem copy an atomic snapshot.
Archives do not include the generated server INI, `admin-password`,
host-generated password files, or the S3 credentials file. The server INI is
generated again during service startup; provision secret-backed values separately
after a restore.
## hectic-lab
hectic-lab runs the timer every 30 minutes and keeps local archives for 14 days:
```text
/var/lib/project-zomboid/backups/archive/
```
Check it with:
```sh
systemctl list-timers project-zomboid-backup.timer
systemctl status project-zomboid-backup.service
journalctl -u project-zomboid-backup.service
```
RCON is enabled on localhost port `27015`; the firewall does not expose this
port. The password is generated at
`/var/lib/project-zomboid/rcon-password` with mode `0600`. The server also uses
`SaveWorldEveryMinutes=15` as a periodic persistence fallback.
## Optional S3 upload
S3 upload is disabled by default. Enabling it requires `bucket`, `endpoint`,
`region`, and an absolute runtime `credentialsFile` outside `/nix/store`. The
endpoint must use HTTPS. systemd reads the environment file without executing
it; this host keeps it owned by `project-zomboid` with mode `0400`:
```sh
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...
```
Set `backup.s3.prefix` to choose the object-key prefix and
`backup.s3.remoteRetentionDays` to prune old archives from that prefix. Remote
deletion runs only after a successful upload and only matches this server's
archive name prefix. Configure bucket lifecycle expiration/versioning too when
available; it remains the stronger recovery and cleanup control.
## Restore
Restoring must be done while the server is stopped so it cannot modify files
during extraction:
The versioned helper creates a fresh current-state backup, stops the timer and
server, validates archive paths, restores the save, and starts both services:
```sh
sudo ./docs/project-zomboid-restore.sh \
/var/lib/project-zomboid/backups/archive/<archive>.tar.zst
```
It writes a rollback archive named
`project-zomboid-<serverName>-pre-restore-<timestamp>.tar.zst` before changing
the save.
```sh
systemctl stop project-zomboid.service
tar --zstd --no-same-owner --no-same-permissions \
-xf /var/lib/project-zomboid/backups/archive/<archive>.tar.zst \
-C /var/lib/project-zomboid
chown -R project-zomboid:project-zomboid /var/lib/project-zomboid/Zomboid
systemctl start project-zomboid.service
```
Re-provision password files and secret-backed INI values before starting.
Verify the restored save and server name before allowing players to reconnect.
+143
View File
@@ -0,0 +1,143 @@
#!/bin/sh
set -eu
SERVER_NAME=${SERVER_NAME:-servertest}
DATA_DIR=${DATA_DIR:-/var/lib/project-zomboid}
ARCHIVE=${1:-}
usage() {
printf '%s\n' "Usage: $0 /path/to/project-zomboid-${SERVER_NAME}-<timestamp>.tar.zst"
printf '%s\n' "Environment: SERVER_NAME, DATA_DIR"
}
if [ "$(id -u)" -ne 0 ]; then
printf '%s\n' 'Run as root.' >&2
exit 1
fi
if [ -z "$ARCHIVE" ]; then
usage >&2
exit 2
fi
if [ ! -r "$ARCHIVE" ]; then
printf 'Backup archive is not readable: %s\n' "$ARCHIVE" >&2
exit 1
fi
ARCHIVE_DIR="$DATA_DIR/backups/archive"
SAVE_DIR="$DATA_DIR/Zomboid/Saves/Multiplayer/$SERVER_NAME"
SERVER_DIR="$DATA_DIR/Zomboid/Server"
TMP_LIST=$(mktemp)
ROLLBACK_ARCHIVE=''
SERVER_STOPPED=0
RESTORE_SUCCEEDED=0
cleanup() {
rm -f "$TMP_LIST"
}
on_exit() {
status=$?
if [ "$status" -ne 0 ] && [ "$SERVER_STOPPED" -eq 1 ] \
&& [ "$RESTORE_SUCCEEDED" -eq 0 ] && [ -n "$ROLLBACK_ARCHIVE" ]; then
set +e
rm -rf "$SAVE_DIR"
rm -f \
"$SERVER_DIR/${SERVER_NAME}_SandboxVars.lua" \
"$SERVER_DIR/${SERVER_NAME}_spawnpoints.lua" \
"$SERVER_DIR/${SERVER_NAME}_spawnregions.lua"
tar --zstd --no-same-owner --no-same-permissions \
-xpf "$ROLLBACK_ARCHIVE" -C "$DATA_DIR"
chown -R project-zomboid:project-zomboid "$SAVE_DIR" "$SERVER_DIR"
systemctl start project-zomboid.service
systemctl start project-zomboid-backup.timer
printf '%s\n' "Restore failed; current state restored from $ROLLBACK_ARCHIVE" >&2
fi
cleanup
exit "$status"
}
trap on_exit EXIT
if ! tar --zstd -tf "$ARCHIVE" >"$TMP_LIST"; then
printf 'Archive integrity check failed: %s\n' "$ARCHIVE" >&2
exit 1
fi
while IFS= read -r member; do
case "$member" in
Zomboid/*) ;;
*)
printf 'Unsafe archive member: %s\n' "$member" >&2
exit 1
;;
esac
case "$member" in
/*|*../*)
printf 'Path traversal member: %s\n' "$member" >&2
exit 1
;;
esac
done <"$TMP_LIST"
if ! systemctl start project-zomboid-backup.service; then
printf '%s\n' 'Could not create fresh backup of current state.' >&2
exit 1
fi
CURRENT_ARCHIVE=$(find "$ARCHIVE_DIR" -maxdepth 1 -type f \
-name "project-zomboid-$SERVER_NAME-*.tar.zst" \
-printf '%T@ %p\n' | sort -nr | awk 'NR == 1 {sub(/^[^ ]* /, ""); print}')
if [ -z "$CURRENT_ARCHIVE" ]; then
printf '%s\n' 'Fresh current-state backup was not found.' >&2
exit 1
fi
stamp=$(date -u +%Y%m%dT%H%M%SZ)
ROLLBACK_ARCHIVE="$ARCHIVE_DIR/project-zomboid-$SERVER_NAME-pre-restore-$stamp.tar.zst"
cp --reflink=auto "$CURRENT_ARCHIVE" "$ROLLBACK_ARCHIVE" 2>/dev/null \
|| cp "$CURRENT_ARCHIVE" "$ROLLBACK_ARCHIVE"
chmod 0600 "$ROLLBACK_ARCHIVE"
chown project-zomboid:project-zomboid "$ROLLBACK_ARCHIVE"
systemctl stop project-zomboid-backup.timer
systemctl stop project-zomboid.service
SERVER_STOPPED=1
if [ "$(systemctl show project-zomboid --property=ActiveState --value)" != inactive ]; then
printf '%s\n' 'Project Zomboid did not stop; refusing to restore.' >&2
exit 1
fi
rm -rf "$SAVE_DIR"
rm -f \
"$SERVER_DIR/${SERVER_NAME}_SandboxVars.lua" \
"$SERVER_DIR/${SERVER_NAME}_spawnpoints.lua" \
"$SERVER_DIR/${SERVER_NAME}_spawnregions.lua"
tar --zstd --no-same-owner --no-same-permissions \
-xpf "$ARCHIVE" -C "$DATA_DIR"
chown -R project-zomboid:project-zomboid "$SAVE_DIR" "$SERVER_DIR"
systemctl start project-zomboid.service
started=0
for _ in $(seq 1 90); do
if [ "$(systemctl show project-zomboid --property=ActiveState --value)" = active ] \
&& [ "$(systemctl show project-zomboid --property=SubState --value)" = running ]; then
started=1
break
fi
sleep 2
done
if [ "$started" -ne 1 ]; then
printf 'Restore completed, but service did not become healthy. Rollback: %s\n' \
"$ROLLBACK_ARCHIVE" >&2
exit 1
fi
systemctl start project-zomboid-backup.timer
RESTORE_SUCCEEDED=1
printf 'Restore completed.\n'
printf 'Rollback archive: %s\n' "$ROLLBACK_ARCHIVE"
@@ -37,6 +37,162 @@
) cfg.sandboxProperties; ) cfg.sandboxProperties;
zomboidDir = "${cfg.dataDir}/Zomboid"; zomboidDir = "${cfg.dataDir}/Zomboid";
adminPasswordFile = "${cfg.dataDir}/admin-password"; adminPasswordFile = "${cfg.dataDir}/admin-password";
rconPasswordFile = cfg.rcon.passwordFile;
backupCfg = cfg.backup;
s3CredentialsFile = if backupCfg.s3.credentialsFile == null then "" else backupCfg.s3.credentialsFile;
s3Bucket = if backupCfg.s3.bucket == null then "" else backupCfg.s3.bucket;
s3Endpoint = if backupCfg.s3.endpoint == null then "" else backupCfg.s3.endpoint;
s3Region = if backupCfg.s3.region == null then "" else backupCfg.s3.region;
saveDir = "${zomboidDir}/Saves/Multiplayer/${cfg.serverName}";
serverConfigDir = "${zomboidDir}/Server";
backupScript = pkgs.writeShellScript "project-zomboid-backup" ''
set -eu
staging_dir=${lib.escapeShellArg backupCfg.stagingDir}
archive_dir=${lib.escapeShellArg backupCfg.archiveDir}
lock_file="$archive_dir/.backup.lock"
${pkgs.coreutils}/bin/install -d -m 0700 \
"$staging_dir/Zomboid/Saves/Multiplayer/${cfg.serverName}" \
"$staging_dir/Zomboid/Server" \
"$archive_dir"
exec 9>"$lock_file"
if ! ${pkgs.util-linux}/bin/flock -n 9; then
${pkgs.coreutils}/bin/printf '%s\n' 'Project Zomboid backup already running; skipping.' >&2
exit 0
fi
${lib.optionalString cfg.rcon.enable ''
rcon_password="$(${pkgs.coreutils}/bin/cat ${lib.escapeShellArg rconPasswordFile})"
if [ -z "$rcon_password" ]; then
${pkgs.coreutils}/bin/printf '%s\n' 'Project Zomboid RCON password file is empty.' >&2
exit 1
fi
${pkgs.rcon}/bin/rcon \
--host 127.0.0.1 \
--port ${toString cfg.rcon.port} \
--password "$rcon_password" \
save
${pkgs.coreutils}/bin/sleep ${toString backupCfg.saveWaitSeconds}
''}
sync_staging() {
${pkgs.rsync}/bin/rsync -a --delete \
${lib.escapeShellArg "${saveDir}/"} \
"$staging_dir/Zomboid/Saves/Multiplayer/${cfg.serverName}/"
${pkgs.rsync}/bin/rsync -a --delete --delete-excluded \
--include=${lib.escapeShellArg "/${cfg.serverName}_SandboxVars.lua"} \
--include=${lib.escapeShellArg "/${cfg.serverName}_spawnpoints.lua"} \
--include=${lib.escapeShellArg "/${cfg.serverName}_spawnregions.lua"} \
--exclude='*' \
${lib.escapeShellArg "${serverConfigDir}/"} \
"$staging_dir/Zomboid/Server/"
}
# Second pass narrows, but cannot eliminate, live-save inconsistency.
sync_staging
${pkgs.coreutils}/bin/sleep 5
sync_staging
timestamp="$(${pkgs.coreutils}/bin/date -u +%Y%m%dT%H%M%SZ)"
archive_name="project-zomboid-${cfg.serverName}-$timestamp.tar.zst"
archive_tmp="$archive_dir/.$archive_name.tmp"
archive="$archive_dir/$archive_name"
trap '${pkgs.coreutils}/bin/rm -f "$archive_tmp"' EXIT
${pkgs.gnutar}/bin/tar \
--use-compress-program=${lib.escapeShellArg "${pkgs.zstd}/bin/zstd -T0"} \
-C "$staging_dir" -cf "$archive_tmp" Zomboid
${pkgs.coreutils}/bin/chmod 0600 "$archive_tmp"
${pkgs.coreutils}/bin/mv "$archive_tmp" "$archive"
trap - EXIT
${pkgs.findutils}/bin/find "$archive_dir" -maxdepth 1 -type f \
-name ${lib.escapeShellArg "project-zomboid-${cfg.serverName}-*.tar.zst"} \
-mmin +${toString (backupCfg.retentionDays * 1440)} -delete
${lib.optionalString backupCfg.s3.enable ''
if [ -z "''${AWS_ACCESS_KEY_ID:-}" ] || [ -z "''${AWS_SECRET_ACCESS_KEY:-}" ]; then
${pkgs.coreutils}/bin/printf '%s\n' \
'AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY missing from Project Zomboid S3 credentials file.' >&2
exit 1
fi
s3_bucket=${lib.escapeShellArg s3Bucket}
s3_prefix=${lib.escapeShellArg backupCfg.s3.prefix}
s3_key="''${s3_prefix:+$s3_prefix/}$archive_name"
${pkgs.awscli2}/bin/aws s3 cp "$archive" \
"s3://$s3_bucket/$s3_key" \
--endpoint-url ${lib.escapeShellArg s3Endpoint} \
--region ${lib.escapeShellArg s3Region} \
--cli-connect-timeout 30 \
--cli-read-timeout 300 \
--only-show-errors
remote_prefix="$s3_prefix"
if [ -n "$remote_prefix" ]; then
remote_prefix="$remote_prefix/"
fi
archive_prefix=${lib.escapeShellArg "project-zomboid-${cfg.serverName}-"}
remote_list="$staging_dir/.remote-objects.json"
remote_delete_dir="$staging_dir/.remote-delete"
${pkgs.awscli2}/bin/aws s3api list-objects-v2 \
--bucket "$s3_bucket" \
--prefix "$remote_prefix" \
--endpoint-url ${lib.escapeShellArg s3Endpoint} \
--region ${lib.escapeShellArg s3Region} \
--output json > "$remote_list"
${pkgs.python3}/bin/python3 - "$remote_list" "$remote_delete_dir" \
"$(( $(${pkgs.coreutils}/bin/date +%s) - ${toString (backupCfg.s3.remoteRetentionDays * 86400)} ))" \
"$remote_prefix$archive_prefix" <<'PY'
import datetime
import json
import os
import re
import sys
objects_path, delete_dir, cutoff, key_prefix = sys.argv[1:]
cutoff = int(cutoff)
archive_pattern = re.compile(
re.escape(key_prefix) + r"\d{8}T\d{6}Z\.tar\.zst\Z"
)
with open(objects_path, encoding="utf-8") as stream:
objects = json.load(stream).get("Contents", [])
old_keys = []
for item in objects:
key = item.get("Key", "")
if not archive_pattern.fullmatch(key):
continue
modified = datetime.datetime.fromisoformat(
item["LastModified"].replace("Z", "+00:00")
)
if int(modified.timestamp()) < cutoff:
old_keys.append(key)
os.makedirs(delete_dir, exist_ok=True)
for batch_number in range(0, len(old_keys), 1000):
batch = old_keys[batch_number:batch_number + 1000]
manifest_path = os.path.join(
delete_dir, f"batch-{batch_number // 1000:04d}.json"
)
with open(manifest_path, "w", encoding="utf-8") as stream:
json.dump(
{"Objects": [{"Key": key} for key in batch], "Quiet": True},
stream,
)
PY
for remote_manifest in "$remote_delete_dir"/*.json; do
[ -f "$remote_manifest" ] || continue
${pkgs.awscli2}/bin/aws s3api delete-objects \
--bucket "$s3_bucket" \
--delete "file://$remote_manifest" \
--endpoint-url ${lib.escapeShellArg s3Endpoint} \
--region ${lib.escapeShellArg s3Region} \
--only-show-errors
done
${pkgs.coreutils}/bin/rm -rf "$remote_list" "$remote_delete_dir"
''}
'';
startScript = pkgs.writeShellScript "project-zomboid-start" '' startScript = pkgs.writeShellScript "project-zomboid-start" ''
admin_password=$(${pkgs.coreutils}/bin/cat ${lib.escapeShellArg adminPasswordFile}) admin_password=$(${pkgs.coreutils}/bin/cat ${lib.escapeShellArg adminPasswordFile})
exec ${pkgs.steam-run}/bin/steam-run \ exec ${pkgs.steam-run}/bin/steam-run \
@@ -131,9 +287,149 @@ in {
default = true; default = true;
description = "Open the Project Zomboid UDP ports in the firewall."; description = "Open the Project Zomboid UDP ports in the firewall.";
}; };
rcon = {
enable = lib.mkEnableOption "local RCON for Project Zomboid automation";
port = lib.mkOption {
type = lib.types.port;
default = 27015;
description = "RCON TCP port; not opened in the firewall by this module.";
};
passwordFile = lib.mkOption {
type = lib.types.path;
default = "${cfg.dataDir}/rcon-password";
description = "Runtime file containing the generated RCON password.";
};
};
backup = {
enable = lib.mkEnableOption "no-stop Project Zomboid backups";
onCalendar = lib.mkOption {
type = lib.types.str;
default = "*:0/30";
description = "systemd calendar expression controlling backup frequency.";
};
stagingDir = lib.mkOption {
type = lib.types.path;
default = "${cfg.dataDir}/backups/staging";
description = "Local directory containing the two-pass rsync staging tree.";
};
archiveDir = lib.mkOption {
type = lib.types.path;
default = "${cfg.dataDir}/backups/archive";
description = "Local directory containing timestamped tar.zst archives.";
};
retentionDays = lib.mkOption {
type = lib.types.ints.positive;
default = 14;
description = "Delete local archives older than this many days.";
};
saveWaitSeconds = lib.mkOption {
type = lib.types.ints.positive;
default = 10;
description = "Seconds to wait after the RCON save command before rsync.";
};
s3 = {
enable = lib.mkEnableOption "uploading Project Zomboid backups to S3-compatible storage";
credentialsFile = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
Runtime env file containing AWS_ACCESS_KEY_ID and
AWS_SECRET_ACCESS_KEY. Required when S3 upload is enabled.
'';
};
bucket = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "S3 bucket receiving backup archives.";
};
endpoint = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "S3-compatible endpoint URL.";
};
region = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "S3 region passed to awscli2.";
};
prefix = lib.mkOption {
type = lib.types.str;
default = "project-zomboid";
description = "Optional object key prefix within the S3 bucket.";
};
remoteRetentionDays = lib.mkOption {
type = lib.types.ints.positive;
default = 14;
description = "Delete uploaded archives older than this many days.";
};
};
};
}; };
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
assertions = [
{
assertion = !cfg.rcon.enable || (
lib.hasPrefix "/" cfg.rcon.passwordFile
&& !lib.hasPrefix "/nix/store/" cfg.rcon.passwordFile
);
message = "hectic.services.project-zomboid.rcon.passwordFile must be a runtime path outside /nix/store.";
}
{
assertion = !backupCfg.s3.enable || backupCfg.enable;
message = "hectic.services.project-zomboid.backup must be enabled before S3 upload.";
}
{
assertion = !backupCfg.s3.enable || backupCfg.s3.credentialsFile != null;
message = "hectic.services.project-zomboid.backup.s3.credentialsFile is required when S3 upload is enabled.";
}
{
assertion = !backupCfg.s3.enable || backupCfg.s3.bucket != null;
message = "hectic.services.project-zomboid.backup.s3.bucket is required when S3 upload is enabled.";
}
{
assertion = !backupCfg.s3.enable || backupCfg.s3.endpoint != null;
message = "hectic.services.project-zomboid.backup.s3.endpoint is required when S3 upload is enabled.";
}
{
assertion = !backupCfg.s3.enable || backupCfg.s3.region != null;
message = "hectic.services.project-zomboid.backup.s3.region is required when S3 upload is enabled.";
}
{
assertion =
!backupCfg.s3.enable
|| backupCfg.s3.credentialsFile == null
|| (
lib.hasPrefix "/" backupCfg.s3.credentialsFile
&& !lib.hasPrefix "/nix/store/" backupCfg.s3.credentialsFile
);
message = "hectic.services.project-zomboid.backup.s3.credentialsFile must be a runtime path outside /nix/store.";
}
{
assertion =
!backupCfg.s3.enable
|| backupCfg.s3.endpoint == null
|| lib.hasPrefix "https://" backupCfg.s3.endpoint;
message = "hectic.services.project-zomboid.backup.s3.endpoint must use HTTPS.";
}
];
users.groups.project-zomboid = { }; users.groups.project-zomboid = { };
users.users.project-zomboid = { users.users.project-zomboid = {
isSystemUser = true; isSystemUser = true;
@@ -145,6 +441,11 @@ in {
systemd.tmpfiles.rules = [ systemd.tmpfiles.rules = [
"d ${cfg.dataDir} 0750 project-zomboid project-zomboid - -" "d ${cfg.dataDir} 0750 project-zomboid project-zomboid - -"
"d ${cfg.installDir} 0750 project-zomboid project-zomboid - -" "d ${cfg.installDir} 0750 project-zomboid project-zomboid - -"
] ++ lib.optionals backupCfg.enable [
"d ${cfg.dataDir}/backups 0700 project-zomboid project-zomboid - -"
"Z ${cfg.dataDir}/backups 0700 project-zomboid project-zomboid - -"
"d ${backupCfg.stagingDir} 0700 project-zomboid project-zomboid - -"
"d ${backupCfg.archiveDir} 0700 project-zomboid project-zomboid - -"
]; ];
systemd.services.project-zomboid = { systemd.services.project-zomboid = {
@@ -160,6 +461,22 @@ in {
umask 077 umask 077
${pkgs.openssl}/bin/openssl rand -base64 32 > ${lib.escapeShellArg adminPasswordFile} ${pkgs.openssl}/bin/openssl rand -base64 32 > ${lib.escapeShellArg adminPasswordFile}
fi fi
${lib.optionalString cfg.rcon.enable ''
if [ ! -s ${lib.escapeShellArg rconPasswordFile} ]; then
umask 077
${pkgs.openssl}/bin/openssl rand -hex 32 > ${lib.escapeShellArg rconPasswordFile}
else
rcon_password=$(${pkgs.coreutils}/bin/cat ${lib.escapeShellArg rconPasswordFile})
case "$rcon_password" in
*[!0123456789abcdefABCDEF]*)
umask 077
${pkgs.openssl}/bin/openssl rand -hex 32 > ${lib.escapeShellArg rconPasswordFile}
;;
esac
fi
${pkgs.coreutils}/bin/chown project-zomboid:project-zomboid ${lib.escapeShellArg rconPasswordFile}
${pkgs.coreutils}/bin/chmod 0600 ${lib.escapeShellArg rconPasswordFile}
''}
${pkgs.steamcmd}/bin/steamcmd \ ${pkgs.steamcmd}/bin/steamcmd \
+force_install_dir ${lib.escapeShellArg cfg.installDir} \ +force_install_dir ${lib.escapeShellArg cfg.installDir} \
+login anonymous \ +login anonymous \
@@ -176,6 +493,12 @@ in {
) configLines} ) configLines}
${lib.optionalString (cfg.serverPropertiesFile != null) ${lib.optionalString (cfg.serverPropertiesFile != null)
"${pkgs.coreutils}/bin/cat ${lib.escapeShellArg cfg.serverPropertiesFile};"} "${pkgs.coreutils}/bin/cat ${lib.escapeShellArg cfg.serverPropertiesFile};"}
${lib.optionalString cfg.rcon.enable ''
${pkgs.coreutils}/bin/printf '%s\n' ${lib.escapeShellArg "RCONPort=${toString cfg.rcon.port}"};
${pkgs.coreutils}/bin/printf '%s' 'RCONPassword=';
${pkgs.coreutils}/bin/cat ${lib.escapeShellArg rconPasswordFile};
${pkgs.coreutils}/bin/printf '\n';
''}
} > ${lib.escapeShellArg "${zomboidDir}/Server/${cfg.serverName}.ini"} } > ${lib.escapeShellArg "${zomboidDir}/Server/${cfg.serverName}.ini"}
${lib.optionalString (cfg.sandboxProperties != { }) '' ${lib.optionalString (cfg.sandboxProperties != { }) ''
{ {
@@ -186,6 +509,10 @@ in {
${pkgs.coreutils}/bin/printf '%s\n' '};'; ${pkgs.coreutils}/bin/printf '%s\n' '};';
} > ${lib.escapeShellArg "${zomboidDir}/Server/${cfg.serverName}_SandboxVars.lua"} } > ${lib.escapeShellArg "${zomboidDir}/Server/${cfg.serverName}_SandboxVars.lua"}
''} ''}
${lib.optionalString (cfg.sandboxProperties == { }) ''
${pkgs.coreutils}/bin/rm -f \
${lib.escapeShellArg "${zomboidDir}/Server/${cfg.serverName}_SandboxVars.lua"}
''}
''; '';
serviceConfig = { serviceConfig = {
@@ -205,6 +532,34 @@ in {
}; };
}; };
systemd.services.project-zomboid-backup = lib.mkIf backupCfg.enable {
description = "No-stop Project Zomboid backup";
after = [ "project-zomboid.service" ];
unitConfig.ConditionPathExists = [
saveDir
serverConfigDir
];
serviceConfig = {
Type = "oneshot";
User = "project-zomboid";
Group = "project-zomboid";
ExecStart = backupScript;
TimeoutStartSec = "30min";
UMask = "0077";
} // lib.optionalAttrs backupCfg.s3.enable {
EnvironmentFile = s3CredentialsFile;
};
};
systemd.timers.project-zomboid-backup = lib.mkIf backupCfg.enable {
description = "Run Project Zomboid backup";
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = backupCfg.onCalendar;
Persistent = true;
};
};
networking.firewall.allowedUDPPorts = lib.mkIf cfg.openFirewall [ networking.firewall.allowedUDPPorts = lib.mkIf cfg.openFirewall [
cfg.port cfg.port
cfg.udpPort cfg.udpPort
@@ -6,6 +6,20 @@
publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKNWWegOVTOF3EOmam32iP7sMybULMTxsXuC+cEGITQ8 minecraft-wow-relay"; publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKNWWegOVTOF3EOmam32iP7sMybULMTxsXuC+cEGITQ8 minecraft-wow-relay";
}; };
systemd.tmpfiles.rules = [ "d /var/www/store/minecraft/world-of-sosal 0755 root root -" ]; systemd.tmpfiles.rules = [ "d /var/www/store/minecraft/world-of-sosal 0755 root root -" ];
services.nginx.virtualHosts."store.bfs.band" = {
enableACME = true;
forceSSL = true;
root = "/var/www/store";
locations."= /".return = "302 /minecraft/world-of-sosal/";
locations."= /minecraft".return = "302 /minecraft/world-of-sosal/";
locations."= /minecraft/".return = "302 /minecraft/world-of-sosal/";
locations."/".extraConfig = ''
autoindex off;
add_header Cache-Control "no-cache";
try_files $uri $uri/ =404;
'';
};
# Keep old pack URLs working for already imported Prism instances.
services.nginx.virtualHosts."bfs.band".locations = { services.nginx.virtualHosts."bfs.band".locations = {
"= /minecraft".return = "302 /minecraft/world-of-sosal/"; "= /minecraft".return = "302 /minecraft/world-of-sosal/";
"= /minecraft/".return = "302 /minecraft/world-of-sosal/"; "= /minecraft/".return = "302 /minecraft/world-of-sosal/";
@@ -17,7 +17,7 @@ let
UsePAM yes UsePAM yes
AuthenticationMethods publickey AuthenticationMethods publickey
AuthorizedKeysFile %h/.ssh/authorized_keys /etc/ssh/authorized_keys.d/%u AuthorizedKeysFile %h/.ssh/authorized_keys /etc/ssh/authorized_keys.d/%u
LogLevel DEBUG3 LogLevel INFO
VersionAddendum none VersionAddendum none
HostKeyAlgorithms rsa-sha2-512,rsa-sha2-256,ssh-ed25519 HostKeyAlgorithms rsa-sha2-512,rsa-sha2-256,ssh-ed25519
@@ -65,7 +65,8 @@ in
Type = "simple"; Type = "simple";
StateDirectory = "experimental-sshd"; StateDirectory = "experimental-sshd";
RuntimeDirectory = "experimental-sshd"; RuntimeDirectory = "experimental-sshd";
ExecStart = "${pkgs.openssh}/bin/sshd -D -e -f /etc/ssh/experimental-sshd_config"; ExecStart = "${pkgs.openssh}/bin/sshd -D -f /etc/ssh/experimental-sshd_config";
StandardError = "journal";
}; };
preStart = '' preStart = ''
${pkgs.openssh}/bin/sshd -t -f /etc/ssh/experimental-sshd_config ${pkgs.openssh}/bin/sshd -t -f /etc/ssh/experimental-sshd_config
+86 -50
View File
@@ -14,7 +14,7 @@ with builtins;
with lib; with lib;
let let
domain = "hectic-lab.com"; domain = "hectic-lab.com";
sshPort = 22; giteaSshPort = 22223;
mailUserNames = [ mailUserNames = [
"security" "security"
"founders" "founders"
@@ -103,11 +103,25 @@ in {
}; };
services."project-zomboid" = { services."project-zomboid" = {
enable = true; enable = true;
memory = "3g"; memory = "4g";
serverName = "servertest"; serverName = "servertest";
serverPropertiesFile = /var/lib/project-zomboid/server-password.ini; serverPropertiesFile = /var/lib/project-zomboid/server-password.ini;
rcon.enable = true;
backup = {
enable = true;
onCalendar = "*:0/30";
retentionDays = 14;
s3 = {
enable = true;
bucket = "backup-hectic-lab";
endpoint = "https://hel1.your-objectstorage.com";
region = "hel1";
credentialsFile = "/var/lib/project-zomboid/s3-credentials";
};
};
serverProperties = { serverProperties = {
Map = "vehicle_interior_arcadia75;Muldraugh, KY"; Map = "Muldraugh, KY";
SaveWorldEveryMinutes = 15;
DoLuaChecksum = false; DoLuaChecksum = false;
Public = true; Public = true;
AntiCheatSafety = 4; AntiCheatSafety = 4;
@@ -127,55 +141,43 @@ in {
AntiCheatNoClip = 4; AntiCheatNoClip = 4;
AntiCheatServerCustomization = 4; AntiCheatServerCustomization = 4;
}; };
sandboxProperties = {
StartMonth = 12;
StartDay = 1;
WaterShut = 3;
WaterShutModifier = 60;
ElecShut = 3;
ElecShutModifier = 60;
MinutesPerPage = 0.5;
ZombieLore = {
Transmission = 4;
Mortality = 7;
};
};
workshopItems = [ workshopItems = [
"3773972040" # Arcadia RV Interiors B42.20
"2210760610" # Cryogenic Winter +Easy/Hard Modes
"3676456221" # Lua Digital Watch Framework "3676456221" # Lua Digital Watch Framework
"3600401184" # Realistic Temperature Mod "3600401184" # Realistic Temperature Mod
"3387824513" # Material Weight Reducer
"3413150945" # More Damaged Objects
"3512708849" # Shotgun Trajectory
"3401576145" # Firearm Models: Redux
"3401134276" # Vanilla Gear Expanded
"2956146279" # Rain Cleans Blood
"3693258802" # Tactical Hold
"3394588830" # Simple Flashlight on Belt
"2684285534" # Spongie's Clothing
"2812326159" # Spongie's Open Jackets
]; ];
mods = [ mods = [
"\\ArcadiaRVInterior_B42_MP"
"\\ArcadiaRVInterior_B42_Vanilla"
"\\CryogenicWinter2NormalMode"
"\\LuaDigitalWatchUI" "\\LuaDigitalWatchUI"
"\\RC_RealisticColdMod" "\\RC_RealisticColdMod"
"\\Material Weight Reducer"
"\\Ammunition Weight Reducer"
"\\MoreDamagedObjects"
"\\ShotgunTrajectory"
"\\FMR"
"\\VanillaGearExpanded"
"\\RainCleansBlood"
"\\TacHold Complete"
"\\LightOnBelt"
"\\SpnCloth"
"\\SpnOpenClothBase"
"\\SpnOpenCloth"
]; ];
sandboxProperties = {
StartMonth = 12;
StartDay = 1;
WaterShut = 3;
WaterShutModifier = 150;
ElecShut = 3;
ElecShutModifier = 150;
MinutesPerPage = 0.5;
Zombies = 4;
ZombieConfig = {
PopulationMultiplier = 1.3;
PopulationStartMultiplier = 1.0;
PopulationPeakMultiplier = 1.0;
RespawnHours = 0.0;
RespawnUnseenHours = 0.0;
RespawnMultiplier = 0.0;
RedistributeHours = 0.0;
};
ZombieLore = {
Transmission = 4;
Mortality = 7;
Speed = 2;
SprinterPercentage = 0;
Strength = 2;
Cognition = 2;
DoorOpeningPercentage = 10;
};
}; };
};
services.p4d = { services.p4d = {
enable = true; enable = true;
package = pkgs.p4d; package = pkgs.p4d;
@@ -253,8 +255,8 @@ in {
key = "init-postgresql"; key = "init-postgresql";
}; };
"atticd/environment" = {}; "atticd/environment" = {};
"immich/storage-box" = {}; "immich/storage-box" = {};
"wg-bfs/private-key" = {}; "wg-bfs/private-key" = {};
"gitea-runner/org-registration-token" = { "gitea-runner/org-registration-token" = {
sopsFile = flake + "/sus/gitea-runners.yaml"; sopsFile = flake + "/sus/gitea-runners.yaml";
key = "gitea/hectic-lab/org-runner-registration-token"; key = "gitea/hectic-lab/org-runner-registration-token";
@@ -265,12 +267,37 @@ in {
"jwt-secret" "jwt-secret"
"s3-access-key" "s3-access-key"
"s3-secret-key" "s3-secret-key"
]); ]) // {
"project-zomboid/s3-access-key" = {
key = "ente/s3-access-key";
owner = "project-zomboid";
group = "project-zomboid";
};
"project-zomboid/s3-secret-key" = {
key = "ente/s3-secret-key";
owner = "project-zomboid";
group = "project-zomboid";
};
};
}; };
systemd.services.project-zomboid.preStart = lib.mkBefore '' systemd.services.project-zomboid.preStart = lib.mkBefore ''
password_file=${lib.escapeShellArg "/var/lib/project-zomboid/server-password"} password_file=${lib.escapeShellArg "/var/lib/project-zomboid/server-password"}
properties_file=${lib.escapeShellArg "/var/lib/project-zomboid/server-password.ini"} properties_file=${lib.escapeShellArg "/var/lib/project-zomboid/server-password.ini"}
s3_credentials_file=${lib.escapeShellArg "/var/lib/project-zomboid/s3-credentials"}
s3_credentials_tmp="$(${pkgs.coreutils}/bin/mktemp "''${s3_credentials_file}.XXXXXX")"
trap '${pkgs.coreutils}/bin/rm -f "$s3_credentials_tmp"' EXIT
{
${pkgs.coreutils}/bin/printf 'AWS_ACCESS_KEY_ID='
${pkgs.coreutils}/bin/cat ${lib.escapeShellArg config.sops.secrets."project-zomboid/s3-access-key".path}
${pkgs.coreutils}/bin/printf '\n'
${pkgs.coreutils}/bin/printf 'AWS_SECRET_ACCESS_KEY='
${pkgs.coreutils}/bin/cat ${lib.escapeShellArg config.sops.secrets."project-zomboid/s3-secret-key".path}
${pkgs.coreutils}/bin/printf '\n'
} > "$s3_credentials_tmp"
${pkgs.coreutils}/bin/chmod 0400 "$s3_credentials_tmp"
${pkgs.coreutils}/bin/mv -f "$s3_credentials_tmp" "$s3_credentials_file"
if [ ! -s "$password_file" ] || ! ${pkgs.gnugrep}/bin/grep -Eq '^[0-9a-f]{48}$' "$password_file"; then if [ ! -s "$password_file" ] || ! ${pkgs.gnugrep}/bin/grep -Eq '^[0-9a-f]{48}$' "$password_file"; then
umask 077 umask 077
@@ -315,7 +342,7 @@ in {
]; ];
}; };
services.openssh.ports = [ sshPort ]; services.openssh.ports = [ giteaSshPort ];
services.mailserver = { services.mailserver = {
enable = true; enable = true;
@@ -337,7 +364,7 @@ in {
networking.firewall = { networking.firewall = {
allowedTCPPorts = [ allowedTCPPorts = [
sshPort # ssh giteaSshPort # gitea ssh
80 80
443 443
3306 # mysql 3306 # mysql
@@ -507,6 +534,9 @@ in {
extraConfig = '' extraConfig = ''
proxy_pass http://127.0.0.1:11011/; proxy_pass http://127.0.0.1:11011/;
proxy_redirect off; proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
''; '';
}; };
}; };
@@ -516,15 +546,21 @@ in {
gitea = { gitea = {
enable = true; enable = true;
package = pkgs.hectic.gitea-heatmap; package = pkgs.hectic.gitea-heatmap;
settings.service.DISABLE_REGISTRATION = false; # Keep LFS storage limited to accounts provisioned by administrators.
settings.service.DISABLE_REGISTRATION = true;
settings.session.COOKIE_SECURE = true;
settings.actions.ENABLED = true; settings.actions.ENABLED = true;
# Long CUDA builds must not hit Gitea's default three-hour task watchdog. # Long CUDA builds must not hit Gitea's default three-hour task watchdog.
settings.actions.ENDLESS_TASK_TIMEOUT = "8h"; settings.actions.ENDLESS_TASK_TIMEOUT = "8h";
settings.server = { settings.server = {
HTTP_ADDR = "127.0.0.1";
HTTP_PORT = 11011; HTTP_PORT = 11011;
SSH_PORT = sshPort; ROOT_URL = "https://gitea.${domain}/";
SSH_PORT = giteaSshPort;
SSH_DOMAIN = "hectic-lab.com"; SSH_DOMAIN = "hectic-lab.com";
}; };
lfs.enable = true;
settings.lfs.LFS_MAX_FILE_SIZE = 536870912;
database = { database = {
createDatabase = true; createDatabase = true;
type = "postgres"; type = "postgres";
+5 -3
View File
@@ -17,10 +17,12 @@ in self.lib.nixpkgs-lib.nixosSystem {
]; ];
config.allowUnfreePredicate = pkg: config.allowUnfreePredicate = pkg:
self.lib.cudaUnfreePredicate pkg || builtins.elem (self.lib.nixpkgs-lib.getName pkg) [ self.lib.cudaUnfreePredicate pkg || builtins.elem (self.lib.nixpkgs-lib.getName pkg) [
"minecraft-server" "minecraft-server"
"neoforge" "neoforge"
"steamcmd"
"steam-unwrapped"
"nvidia-x11" "nvidia-x11"
]; ];
# jitsi-meet depends on libolm which is marked insecure (CVE-2024-4519x) # jitsi-meet depends on libolm which is marked insecure (CVE-2024-4519x)
config.permittedInsecurePackages = [ config.permittedInsecurePackages = [
@@ -13,10 +13,10 @@
serverName = "wowMineMap"; serverName = "wowMineMap";
remoteHost = "u664722.your-storagebox.de"; remoteHost = "u664722.your-storagebox.de";
remoteUser = "u664722"; remoteUser = "u664722";
remotePath = "minecraft/pack/WorldOfSosal.mrpack"; remotePath = "minecraft/pack/WorldOfSosal-v3.mrpack";
archiveName = "WorldOfSosal.mrpack"; archiveName = "WorldOfSosal.mrpack";
cacheDir = "/var/lib/minecraft-modpacks/worldOfSosal"; cacheDir = "/var/lib/minecraft-modpacks/worldOfSosal";
archiveSha256 = "f8c18acb9208e4592725632ae50dab4f9c308483b34fd43a6507c74fdbf8169f"; archiveSha256 = "f97cf251b14f40590e97e7b39e8a8ec43dacfce6da1b02357d15e0eee10d3ade";
expectedDependencies = { expectedDependencies = {
minecraft = "1.21.1"; minecraft = "1.21.1";
neoforge = "21.1.250"; neoforge = "21.1.250";
+23 -2
View File
@@ -22,9 +22,28 @@
hostPublicKey = "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA5EB5p/5Hp3hGW1oHok+PIOH9Pbn7cnUiGmUEBrCVjnAw+HrKyN8bYVV0dIGllswYXwkG/+bgiBlE6IVIBAq+JwVWu1Sss3KarHY3OvFJUXZoZyRRg/Gc/+LRCE7lyKpwWQ70dbelGRyyJFH36eNv6ySXoUYtGkwlU5IVaHPApOxe4LHPZa/qhSRbPo2hwoh0orCtgejRebNtW5nlx00DNFgsvn8Svz2cIYLxsPVzKgUxs8Zxsxgn+Q/UvR7uq4AbAhyBMLxv7DjJ1pc7PJocuTno2Rw9uMZi1gkjbnmiOh6TTXIEWbnroyIhwc8555uto9melEUmWNQ+C+PwAK+MPw=="; hostPublicKey = "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA5EB5p/5Hp3hGW1oHok+PIOH9Pbn7cnUiGmUEBrCVjnAw+HrKyN8bYVV0dIGllswYXwkG/+bgiBlE6IVIBAq+JwVWu1Sss3KarHY3OvFJUXZoZyRRg/Gc/+LRCE7lyKpwWQ70dbelGRyyJFH36eNv6ySXoUYtGkwlU5IVaHPApOxe4LHPZa/qhSRbPo2hwoh0orCtgejRebNtW5nlx00DNFgsvn8Svz2cIYLxsPVzKgUxs8Zxsxgn+Q/UvR7uq4AbAhyBMLxv7DjJ1pc7PJocuTno2Rw9uMZi1gkjbnmiOh6TTXIEWbnroyIhwc8555uto9melEUmWNQ+C+PwAK+MPw==";
}; };
sops.secrets."minecraft/rcon-password" = {
sopsFile = ../../../../sus/neuro-minecraft.yaml;
owner = "minecraft";
group = "minecraft";
mode = "0400";
restartUnits = [ "minecraft-server-wowMineMap.service" ];
};
# The module's automatic firewall would also expose RCON.
networking.firewall.allowedTCPPorts = [ 25567 ];
services.minecraft-servers.servers.wowMineMap = { services.minecraft-servers.servers.wowMineMap = {
openFirewall = false;
extraStartPre = ''
chmod 600 server.properties
{
printf '\nrcon.password='
cat ${config.sops.secrets."minecraft/rcon-password".path}
printf '\n'
} >> server.properties
'';
enable = true; enable = true;
jvmOpts = "-Xmx8G -Xms2G"; jvmOpts = "-Xmx24G -Xms2G";
# WorldOfSosal client and server use the same pinned NeoForge. # WorldOfSosal client and server use the same pinned NeoForge.
package = pkgs.minecraftServers.neoforge-1_21_1.override ( package = pkgs.minecraftServers.neoforge-1_21_1.override (
builtins.fromJSON (builtins.readFile ./neoforge-21.1.250.json) builtins.fromJSON (builtins.readFile ./neoforge-21.1.250.json)
@@ -33,7 +52,9 @@
serverProperties = { serverProperties = {
server-port = 25567; server-port = 25567;
difficulty = "hard"; difficulty = "hard";
online-mode = true; online-mode = false;
enable-rcon = true;
"rcon.port" = 25575;
view-distance = 12; view-distance = 12;
simulation-distance = 8; simulation-distance = 8;
motd = "WorldOfSosal — World of Warcraft"; motd = "WorldOfSosal — World of Warcraft";
+65 -1
View File
@@ -139,7 +139,7 @@ in {
}; };
services.nginx = { services.nginx = {
enable = true; enable = false;
virtualHosts."bfs.band" = let virtualHosts."bfs.band" = let
site = pkgs.runCommand "bfs-band-site" {} '' site = pkgs.runCommand "bfs-band-site" {} ''
mkdir -p $out mkdir -p $out
@@ -224,6 +224,70 @@ in {
archetype.dev.enable = true; archetype.dev.enable = true;
}; };
hectic.services."project-zomboid" = {
enable = true;
memory = "8g";
serverName = "servertest";
serverPropertiesFile = /var/lib/project-zomboid/server-password.ini;
serverProperties = {
Map = "Muldraugh, KY";
DoLuaChecksum = false;
Public = true;
AntiCheatSafety = 4;
AntiCheatMovement = 4;
AntiCheatSpeed = 4;
AntiCheatHit = 4;
AntiCheatPacket = 4;
AntiCheatPacketException = 4;
AntiCheatPermission = 4;
AntiCheatXP = 4;
AntiCheatFire = 4;
AntiCheatSafeHouse = 4;
AntiCheatRecipe = 4;
AntiCheatPlayer = 4;
AntiCheatChecksum = 4;
AntiCheatItem = 4;
AntiCheatNoClip = 4;
AntiCheatServerCustomization = 4;
};
workshopItems = [
"3676456221" # Lua Digital Watch Framework
"3600401184" # Realistic Temperature Mod
];
mods = [
"\\LuaDigitalWatchUI"
"\\RC_RealisticColdMod"
];
sandboxProperties = {
Zombies = 6;
ZombieConfig = {
PopulationMultiplier = 0.0;
PopulationStartMultiplier = 0.0;
PopulationPeakMultiplier = 0.0;
RespawnHours = 0.0;
RespawnUnseenHours = 0.0;
RespawnMultiplier = 0.0;
RedistributeHours = 0.0;
};
};
};
systemd.services.project-zomboid.preStart = lib.mkBefore ''
password_file=${lib.escapeShellArg "/var/lib/project-zomboid/server-password"}
properties_file=${lib.escapeShellArg "/var/lib/project-zomboid/server-password.ini"}
if [ ! -s "$password_file" ] || ! ${pkgs.gnugrep}/bin/grep -Eq '^[0-9a-f]{48}$' "$password_file"; then
umask 077
${pkgs.openssl}/bin/openssl rand -hex 24 > "$password_file"
fi
${pkgs.coreutils}/bin/chmod 0600 "$password_file"
properties_file_tmp="$( ${pkgs.coreutils}/bin/mktemp "$(dirname "$properties_file")/.server-password.ini.XXXXXX")"
${pkgs.coreutils}/bin/printf 'Password=%s\n' "$(<"$password_file")" > "$properties_file_tmp"
${pkgs.coreutils}/bin/chmod 0600 "$properties_file_tmp"
${pkgs.coreutils}/bin/mv "$properties_file_tmp" "$properties_file"
'';
sops = { sops = {
gnupg.sshKeyPaths = [ ]; gnupg.sshKeyPaths = [ ];
age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ]; age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
+14
View File
@@ -6,6 +6,20 @@ in final: prev: (
legacyPackages = self.legacyPackages.${prev.stdenv.hostPlatform.system}; legacyPackages = self.legacyPackages.${prev.stdenv.hostPlatform.system};
in { in {
hectic = packages // legacyPackages; hectic = packages // legacyPackages;
p4d = if final.stdenv.hostPlatform.system == "x86_64-linux" then prev.p4d.overrideAttrs (_: {
version = "2023.1/2797325";
src = final.fetchurl {
url = "https://ftp.perforce.com/pub/perforce/r23.1/bin.linux26x86_64/helix-core-server.tgz";
hash = "sha256-O8znAlq2XjrixG0FA4cfkgcI9t/w9QMHV0spUjYKl48=";
};
}) else prev.p4d;
p4 = prev.p4.overrideAttrs (_: {
version = "2024.1/3006289";
src = final.fetchurl {
url = "https://ftp.perforce.com/pub/perforce/r24.1/bin.tools/p4source.tgz";
hash = "sha256-z3I3cikbbSrmS7dUMMKi6edPnZk2BYAmdO+pfYRJUVQ=";
};
});
postgresql_17 = prev.postgresql_17 // {pkgs = prev.postgresql_17.pkgs // { postgresql_17 = prev.postgresql_17 // {pkgs = prev.postgresql_17.pkgs // {
http = packages.pg-17-ext-http; http = packages.pg-17-ext-http;
pg_smtp_client = packages.pg-17-ext-smtp-client; pg_smtp_client = packages.pg-17-ext-smtp-client;
+1 -1
View File
@@ -27,7 +27,7 @@ def main():
scripts = Path(__file__).resolve().parent scripts = Path(__file__).resolve().parent
mirrors = [ mirrors = [
('hectic-lab', 'https://store.hectic-lab.com/minecraft/world-of-sosal/', 'store.hectic-lab.com:25568'), ('hectic-lab', 'https://store.hectic-lab.com/minecraft/world-of-sosal/', 'store.hectic-lab.com:25568'),
('bfs.poland.xray', 'https://bfs.band/minecraft/world-of-sosal/', 'bfs.band:25568'), ('bfs.poland.xray', 'https://store.bfs.band/minecraft/world-of-sosal/', 'wow.bfs.band'),
] ]
with tempfile.TemporaryDirectory(prefix='prism-mirrors-') as temporary: with tempfile.TemporaryDirectory(prefix='prism-mirrors-') as temporary:
root = Path(temporary) root = Path(temporary)
+21 -20
View File
@@ -1,37 +1,38 @@
minecraft: minecraft:
storage-box-key: ENC[AES256_GCM,data:+TGUsDgspg+zvPRqnooKK+XpSqZQIxqjW1Se0FHbdFrn1bndW1ImiW7YMAZP01/k6hI5vngKxjq/AiGtFqarNFd/wL18w7Rx9MiIspLHIt9IQmeRG9d3i9Rp0tIRQ4dtgMV3VMd/VVdmFm8Vt2E+yw1KFJMY+vhTN4tpQeTLsMVXpo0eySINgEgY9Fri85wglP87YefRN7LEUQIYofjwh/QH2ll2aYVIb1u1Djkxfljf1OBkfiQOM5dgm4NUIEiv5EiVBE+gFZJDo5+VNW1NkNz8xMyQhydJGUCJitdnb3W8nUiOz+TDkxiCg82t11cIXKiQUThc0KOKCv827zkMn4gPieUd2rmr8gao61e8tpJGEJS7Qv7gPl+Bt7vPLRnNKo3sEtRtUHoqkRam0Jr3eOnn2F8Ubnj3sBn0sYbERKkjV4m1qhorDRjDxxR/J22FR8gZvtdYMq2y0n98DHKBTrtKCLddiwMfyXoxCLi8p8PF+IzZTBD2oKa6hQmTUZB134yYJIadGYzpr2+vM8GY,iv:+ljR+UiWoh+fRLTNNSQwiNo6DDxhWeE3UNUvvVjghMI=,tag:n4an1nQXdBm+eCuUJvnQfw==,type:str] storage-box-key: ENC[AES256_GCM,data:Bf4hPWsFuS9u8DXeAYgvjQA7uCi7OHbq/C1yvhZsBmaKHBV7vfNCZoQrF0rE/Fo/ZAwfiirhxmn4FpNzakL8OzD99P0VPdC+BR/Fl2AA67l56UyTtHnIeir+BiWNlZuOOmhGCI1lrR8m0HW/HuY7BAK3wY9qyHDMyjti8fQBAE79axCnmpFR/cL3Fl+NV65IQwKVddhldTSW2kJhPAithjHHugObMJppn5jMPMGqu/MP9EsufnIc68F9X9tzFJEF4Ry7ukgTI3M8dtEX79jcssROXZAKl6oSiYG2CPmr8DGmbGyvkzy5m3MT9q/ZLrIZbNDPnWmDUVg+FFpqGlv4uLsWn/JIpuXev7t573MckjmKrVuuDldpArpVlKd3fgw2faxYD/iltH0cckfYSSqObndRWQcvN4ioJMUCXBIg/QlU111EcrqhwpaTkQIAXSZUxjUhlNEa+mfndxwp8TvV890zISwKRLXjv6AtrWy6m+9W2gm9ESbGRqDka4Ltzet0gcauY4Iwj6uzNukVfiZS,iv:33onrZNu3cMq5ukxuL8qZM2XeG1Qo+iaRFCy8Gy6U6k=,tag:jgKRJtp6mNZem7NrhDf1AA==,type:str]
storage-box-pack-key: ENC[AES256_GCM,data:rzWpejUCTkdkmdwJlveRRAzr1DhDHgBrjMDF0M21fMS9LEL057P/8yCEwjZQNe81tMNZVC+NfgEvAoxYpMkv7jpKk4FH92Sr1VWmvv4UTgusYzrebvSt2evnzNEjkxhhsi81kpC6Rt386/3Z7SdsQ86RUNj+tJs/qxLo4ekk63vWXVw5i1idpTfr2jMiwG4/mjQaw02kkvUtqFJgQdPiMhHjbXR51uoAQQ7AL0vcOvpGtqYJQJqItcJohaJtTBI7bRj3TqEI88SU3NWIQs9vfWm39HDDyAJyyqNnCSDPZLFp3khbgdQYS8PJGmFCXynVQM27UJhASR+OFL5B5Irx4W6ggkYiJWIYFkNs1hk0l7QpE+yNcRVIkbZEf9cSfUVhOBuUQ6lrUI7bpyD7t+6P/sY4Ok58/6wBt5+9EPyRJsBb0rksf1/wVMuj3oOHDU3X6D1XE+yf1xHK6a0r6UdmUfJXBejNOFHXKg/8uW+4g7iIpu0JxQKkvNyh87UAzXU9U7ORQ48PLS93YGXdd5Dq,iv:a5n3kn/dKoJgaqFam0MUYc5bfOiFQMkWZf6IK+OPvqY=,tag:2enSZwMTeg24OF4il0LyhQ==,type:str] storage-box-pack-key: ENC[AES256_GCM,data:srm0+uu1ejWMdlxsIl4X6Fv2c/KBo4pVKcmtun+jqbsa1yifKBDT0hBul9dD/Vs6CojXZcDKTTs21B7zTfJuVmWX/z67qzY7aO0zNGtptgJsEW2OGwQjPWTXq6XhbIHGAGDutzluvdSpRCcKh9vKg2fPKHnhuntWGnDm/ujJKMgd5DqvapRNKLL4SoGo/6pedEVXJvEV9N37FvSoBIE2kj0KXxlX8AigjOtq7y+7Mk0p25FQY7uL1Ix94IK5Zi1VFt6gCbbntbPNeS08k+UUw+kDJ7QFwJrHIa7KzQ+3sL1eyKr5GbiWtDCzFGKSLth8qP8S2fgUQIII0L4Ak6NOSyJA3v1c1A8tl9kWiqZ/j0CQeFr++2PUi+lRI4tSo8stlV7MGA9lIO8Uwl0XTfdCkiEkBdZXj6UKyoDyS+hjTMGgh9evnCiEj0xETupA84CoRQEWXrb7wQmiw4evSJrYohiu+3iilNikgS9EJxCvXtoXemGTcqKCcXYvKRIPHmouLpeHYtLm2fpjKapvCGfV,iv:AkqaETKUQ7k6uH/nWV4KfxdGoBexH9r4ZiKDUZ/s4+0=,tag:3srUBlg2NvvFRq0BCoVvmg==,type:str]
wow-tunnel-key: ENC[AES256_GCM,data:8aQZZd8c3/DAFkNOoxSTBK+sf1dL2ygAokS94NH/5Dh137Zil9S2MDMiUvEXHHUjvxSPN9YqFeYZfewFu0kaoKy6hwQiJPxMCS4hefzBZtO0uiSIJ7WfLwwMUMwICOvc423ynfK4IuZiu4m9NI3crDl6h2Xfz8PmR9potpssnuLVf88hZeXhZznQhI5NjuZjI7a6RoVNKT+ZaRyNk4T56mhP3pRtGkOS8UU9gElWN+gBUaZWm8NqyBsa7vpLLStdwHalzvQVcj5NDjYcaaH8rCeqODda+8NPJC0+w2XiYWRHXU24c4N1vRkVp9Ioo4P0COpNnG4U1aJcbAF9G255Dy7QNx/251GzG2lK8G3n4YC8XlQB1coEO/WnfkUzPXbI+Un89kDaXg3ZopLypVFv/ApOXhOz9i/vcsD95fCmBnWFzZb/mMx07JZ9L0CeDqAYUMzJzIsxE0cIIKlX0hTJIQpsQ1FZsR4TN4X3XtdeYZkZB1c5m4EURwkc2GhstZmusxlUhvBHxq4UNTvWOq4U82pueMfBf92u3Rgl,iv:4RBfsx51NHcRI+szo+JeJgb30ClnuLSO6Wfql4MTlz4=,tag:V+KH9NAooe3BftWrt22YXQ==,type:str] wow-tunnel-key: ENC[AES256_GCM,data:/2QeyAd+2+9VLeSEdZP5vh9sBqrfakxx0ZqNxzTr0QKT7d0s5jQpMMXOC763cjYFeEnvwQDpE4gL1lkoSSQ543vC0A0kA3a3AUUUtCQxTQqtrESnulferbp2jVXBXZrjYuIyH803YZ1iQD9rFaVEVQEtGR2lqkSqccMMvu+oSnJroXCJyuortN+xCri1xdPb+/tpqoffzpGet7TUUnAXAUatn9XaD18tj4q2ID9HfNLQdzwUjrFiEJx3yOFcn5PC1AE3REx6ZLIHF0ZDdVILfP/Q+uNa5l//Zrj9Xm2/+dgx7h7kGCwkS/Da8ow5EGpvcgAEsbX4cRgVLQvmnr2vaBbbArVyZpriMEyzEcwf68kl/KiZKgfEFuxPjXeawF/eLisMpwbqtFa1owljbzkBBTgBrCIvt9m29jiuj1EsNDdxvUsM95ATVr6eqDL04JgzPrt7niwzO5WeqBfugeOvNFlx3NCzKd7L0mG2qDPXTmRuOvRu3/LfctScW+zT9QCmN9IsxNkqFPLzhsF4JFD1HfPr3JLVsKYALJ5c,iv:k0y53lW8JLoQ8+Botf0EFzqSkTrvzaKB6fBbGTcdaLc=,tag:z4unb+jPbvKSlMZbORHYeA==,type:str]
rcon-password: ENC[AES256_GCM,data:wzlMscGtdXF/Cssvi2qeNFgVtYVgCtm9nhX1rCVZ5KcnuyUmejjxsR6N/VAf4iTT,iv:BGdWotcNYNVL+Yi3Wd+5vYHw+l31Gt34MbTXsPxJEuM=,tag:VLWkYWLjjef8mvKYA0JgDA==,type:str]
sops: sops:
age: age:
- recipient: age1r25zdeqq8nac6dgca9en28r57ffyz9u9d8z5yc25gc8xqz747vaqmdtk0h - recipient: age1r25zdeqq8nac6dgca9en28r57ffyz9u9d8z5yc25gc8xqz747vaqmdtk0h
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSArTUllbGhrL2F4SGVOMDl5 YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB4NkhmaG5DeUIyZkZabHls
RkJHZFprY1FIQ2hmRE1PTVkxbGhZWitIYW5ZCk1ueXRYY3REVFdpVXBCY1dXcmFD bWM0Y2dmc2p0MkhtQnR4dUJWWjhkRzB6TnhNCnczdHpUenNtVEdVWit6UUFVQ0k5
QlBEZFVLRWc0MmtiNnpyQWZaMDhFaGcKLS0tIHl0T1pTanIwbG14ZDk0UjdrYVVR cTJhWnJ6Yi9mN0M2K1FiUHpqMjhwRm8KLS0tIFZCOTdybFpCY0huNUxtSVc3ZzQ5
MDRoSzNQdXdsVWk5SjZaYlBkZWJpaDQKkVU1p0KTR2aVyeNDBg9jUfeTFCY+BS7U Ump2dXJyRS94elJYcGo0Y2svWCtkSGcKxnsHCGzvUYbgmEro9YkrvrOGJo7NYRRQ
hqIEwIErcWTg35f7kl3I2Mrw69gEU7jXlc0I0/uKGJuZAwZ3hksmmQ== HzvXpMkoO64LHALsamQq0awmdpOPjXAnJ9UzgcXrLzgbyZPDyVSWng==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1ak7heljpr0pjr4m0rcwxgn3sp0jjxw03lxyf33r8lcemqh2u2sgqx0aplq - recipient: age1ak7heljpr0pjr4m0rcwxgn3sp0jjxw03lxyf33r8lcemqh2u2sgqx0aplq
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBhN2FOSlh6VWl1NEJ1dnd6 YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB2ay9QbytUL3o1SjdPdTRV
cVVTOGM5amREUnZWK2ljZHRyV1dyRlVBS2hrCnhPb2liMEpVSDNLdVlrcjBpeG01 aVN0RGlTaWFpeU9Obkd2MkJoQWJFUy9rd2xzCkQ2N3NwYXZwSXdkWUNNbldNSElS
RTdCdGxnSHZFKzUweHdMNTdjNlVQZWMKLS0tIFYwUXZoaUNjYWdUaW1nTFk2TEJG MzZGdG83QWpsd2VBV1lLTXVwWk5tSUkKLS0tIFQvM3dzZVI2L050OXIxVEtFdVF4
NGY5U0tlREpiRUR5QU81cVFRL2NXU1UK6KVnn5WZ+vDMwrS1TkuET3TG5AxYbiPd WWZseFVKV09RZmViQ1JEaDNFaUpTMkkKfUeGHjNCOfIeqxAxxNGUsFZ/yBCyzE8r
MigYj9t0tDtytiiQZ6sbJVqXewqbKwZS6pRaGXuNTu4s2cUTSJ4ABg== +XeGgTZLRyfABCHi7uPkNYFMryMc4veGUdavTcaMNYUcNfEI/g3FtA==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
- recipient: age1ev53mzse6rg4ffwtcwtq4e93c7x7s4d0eyu89jrsahrke8r4yamsseu8h4 - recipient: age1ev53mzse6rg4ffwtcwtq4e93c7x7s4d0eyu89jrsahrke8r4yamsseu8h4
enc: | enc: |
-----BEGIN AGE ENCRYPTED FILE----- -----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBQTmd5bVlleEtocDZPaHZy YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBNY2FTRGZwQ2ZRSjVyYlpa
aUY1ZTdBeHhXZmZsOXFCMTRoWlNraC9OTEFnCkNhV3NMS3lVeWVlSkFnOXdDbUFq WldKdmRKVS9JbHNScEJXbmVKbHRDZFU1NFZBClE5MitXSHc0ZHBHSnVPeHg3V0ZZ
QUxxRWloUDNOQk05QUpVUGwycHRMZUkKLS0tIEdyMDJnaTB3M2VJR1hSYzhReUlz SFFXWWlwS2VMb0czSW93UllpZjhMSzgKLS0tIE5CL0FTOEhoNkhlMVdsS3pQbXNQ
WVlKekVEVm5kRGNCdWNqRjhYMkMyQlEKCBkbxQ2LZ56redETBA1NcL6CTAv/FBdP WWVLZDVCODJja2RqdTdZK3BmQzBXeDAK5Eqxf+HgCMhKLHtW0jRe6nDKHaSsGX/V
x2xBwsbHdxO0PlmEvgC4x/1CtFTFj8NDhNyY7sf21ETMEIyOX9k8oA== nu5r7ceh5XHG1Mq3K9eA1oBa+GFP3305ch5ja6iTICj76ZXjmRnffg==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
lastmodified: "2026-09-18T20:09:44Z" lastmodified: "2026-09-19T08:48:54Z"
mac: ENC[AES256_GCM,data:9g87oKlLkbvqS8anS4FD/icqoClklhJENoRbKIFr38YNLPwtLLRmTPLai6Q2Z4KLeLgf0KTB+uS+Z4s0/4UuLZPU9cFXDKittkoRGG7amEY2+n9MDfuoWch47mSCCxem3gP5GV5RNKGrEXuyCESzElCO+Q1ZnsF4WSkOsxsK2MU=,iv:n2nYrw0dHmt3NfRL22Ga0eO23iYn9o/HjyCbQhJQCpc=,tag:zTQRKr0rQD5XYl2tTH7WaA==,type:str] mac: ENC[AES256_GCM,data:vGdJ6avRiDu2PU/115cxCQNE5/Oj2K9QRLAxvPRp9UdYPrvx/sh2Q2cy5rGRL9pn09yZPB8LLtxFA9jDsZuSt8WIktIllkFfMb/ng3lEki/KSyd4YSlbA3wBcQ7wsWdfZ510BuRhbwCLnkIZpZYJ91mnaaU1ykmPzM5MHRj3IfQ=,iv:x/5jXo9ySW+3qqciS+bIDAwXo9O9j/I2JqcJp2BOLJQ=,tag:1yH4ZZReaP/dOuPyW0XBSw==,type:str]
unencrypted_suffix: _unencrypted unencrypted_suffix: _unencrypted
version: 3.10.2 version: 3.10.2