Compare commits

...

26 Commits

Author SHA1 Message Date
Bastian de Byl
b4ebc4bad7 feat: increase act_runner capacity for parallel job execution
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 13:51:24 -04:00
Bastian de Byl
d5e473304a fix: use python_env as guard for ESP-IDF install task
The tools directory can exist without the Python venv being created,
causing install.sh to be skipped on re-runs. Check for python_env
instead, which is the actual output we need.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 12:43:14 -04:00
Bastian de Byl
5deb2e6e48 feat: add SSH key and known_hosts for gitea-runner
Generate ed25519 deploy key and add git.skudak.com/git.debyl.io host
keys to known_hosts so the runner can clone SSH submodules in CI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 17:53:01 -04:00
Bastian de Byl
1c478e6ab5 fix: add ESP-IDF to git safe.directory before submodule init
Root-owned /opt/esp-idf triggers git dubious ownership check when
running submodule update. Add safe.directory config beforehand.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 17:10:25 -04:00
Bastian de Byl
dbd898cb2f feat: support multiple Gitea instances for actions runner
The gitea-actions role now uses a `gitea_runners` list instead of a
single `gitea_instance_url`. Each instance gets its own config, systemd
service, working directory, and cache. Migrates from the old single
`act_runner.service` to per-instance `act_runner-{name}.service`.

Adds git.skudak.com alongside git.debyl.io as runner targets.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 17:04:23 -04:00
Bastian de Byl
43fbcf59a5 add n8n workflow automation and fix cloud backup rsync
- Add n8n container (n8nio/n8n:2.11.3) with Caddy reverse proxy at n8n.debyl.io
- Add --exclude .ssh to cloud backup rsync to prevent overwriting
  authorized_keys on TrueNAS backup targets

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 12:12:19 -04:00
Bastian de Byl
f23fc62ada fix: move cloud backup keys and scripts out of container volume paths
SSH keys moved to /etc/ssh/backup_keys/ (ssh_home_t) and backup scripts
to /usr/local/bin/ (bin_t) to fix SELinux denials - container_file_t
context blocked rsync from exec'ing ssh. Also fixes skudak key path
mismatch (was truenas_skudak, key deployed as truenas_skudak-cloud).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 14:45:03 -05:00
Bastian de Byl
d4b01468ba chore: update vault variables 2026-03-05 14:00:16 -05:00
Bastian de Byl
8fd220a16e noticket - update zomboid b42revamp modpack to collection 3672556207
Replaces old 168-mod collection (3636931465) with new 385-mod collection.
Cleaned BBCode artifacts from mod IDs, updated map folders for 32 maps.
LogCabin retained for player connect/disconnect logging.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 13:59:33 -05:00
Bastian de Byl
3637b3ba23 noticket - remove karrio, update gregtime, fix caddy duplicate redirect
Remove Karrio shipping platform (containers, config, vault secrets,
Caddy site block). Bump gregtime 3.4.1 -> 3.4.3. Remove duplicate
home.debyl.io redirect in Caddyfile. Update zomboid b42revamp mod list.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 17:40:00 -05:00
Bastian de Byl
9f95585daa noticket - updated gregtime 2026-02-17 14:21:02 -05:00
Bastian de Byl
495943b837 feat: add ollama and searxng, migrate to debyl.io hostname
- Add ollama role for local LLM inference (install, service, models)
- Add searxng container for private search
- Migrate hostname from home.bdebyl.net to home.debyl.io
  (inventory, awsddns, zomboid entrypoint, home_server_name)
- Update vault with new secrets

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 15:13:25 -05:00
Bastian de Byl
3eb6938b62 feat: switch FISTO to dolphin-mistral with dolphin-phi fallback
Benchmarked uncensored models for the gregtime FISTO bot. dolphin-mistral
produces the best uncensored creative content, dolphin-phi is faster fallback.
Added OLLAMA_NUM_PREDICT env var (300) and bumped image to 3.3.0.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 14:56:52 -05:00
Bastian de Byl
d10cd49cf0 refactor: use variables for graylog stack image versions
Move hardcoded image versions to variables defined in main.yml for
easier version management in one place.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 12:35:51 -05:00
Bastian de Byl
61692b36a2 refactor: reorganize fluent-bit and geoip out of containers
- Move fluent-bit to common role (systemd service, not a container)
- Move geoip to podman/tasks/data/ (data prep, not a container)
- Remove debyltech tag from geoip (not a debyltech service)
- Fix check_mode for fetch subuid task to enable dry-run mode

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 12:34:43 -05:00
Bastian de Byl
9d562c7188 feat: smart zomboid traffic filtering with packet-size detection
Replace per-IP hashlimit with smarter filtering that distinguishes
legitimate players from scanner bots based on packet behavior:
- Players send varied packet sizes (53, 37, 1472 bytes)
- Scanners only send 53-byte query packets

New firewall rule chain:
- Priority 2: Mark + ACCEPT non-query packets (verifies player)
- Priority 3: ACCEPT queries from verified IPs (1 hour TTL)
- Priority 4: LOG rate-limited queries from unverified IPs
- Priority 5: DROP rate-limited queries (2 burst, then 1/hour)

Also includes:
- Fail2ban zomboid jail with tighter thresholds (5 retries/4h, 1w ban)
- Graylog streams for zomboid-connections, zomboid-ratelimit, fail2ban
- GeoIP pipeline enrichment for zomboid traffic
- Fluent-bit inputs for ratelimit logs and fail2ban events
- Remove Legendary Katana mod (Workshop 3418366499) - removed from Steam
- Bump Immich to v2.5.0
- Fix fulfillr config (nil → null)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 15:09:26 -05:00
Bastian de Byl
33eceff1fe feat: add personal uptime kuma instance at uptime.debyl.io
- Add uptime-kuma-personal container on port 3002
- Add Caddy config for uptime.debyl.io with IP restriction
- Update both uptime-kuma instances to 2.0.2
- Rename debyltech tag from uptime-kuma to uptime-debyltech

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 08:04:33 -05:00
Bastian de Byl
bc26fcd1f9 chore: fluent-bit zomboid, zomboid stats, home assistant, gregbot 2026-01-24 17:08:05 -05:00
Bastian de Byl
045eb0b5a7 chore: update fulfillr 2026-01-23 12:07:08 -05:00
Bastian de Byl
9a95eecfd5 chore: zomboid stats for gregtime, updates 2026-01-23 12:02:57 -05:00
Bastian de Byl
a59dc7a050 chore: bump gregtime to 2.0.9
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 23:16:51 -05:00
Bastian de Byl
2b4844b211 feat: add fulfillr outreach email configuration
- Update street2 address to Unit 95
- Add outreach config with DynamoDB tables and SES settings

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 23:11:58 -05:00
Bastian de Byl
86e1b88d5a chore: bump image versions
- fulfillr: 20260109.0522 -> 20260123.0109
- gregtime: 1.9.0 -> 2.0.8

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 23:11:57 -05:00
Bastian de Byl
9e04727b0e feat: update zomboid b42revamp server name and mods
- Rename b42revamp server from "zomboidb42revamp" to "gregboid"
- Remove mod 3238830225 from workshop items
- Replace Real Firearms with B42RainsFirearmsAndGunPartsExpanded4213
- Remove 2788256295/ammomaker mod

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 23:11:56 -05:00
Bastian de Byl
2c7704b6f9 feat: add zomboid world reset via systemd path unit
Deploy systemd path unit that watches for trigger file from Discord
bot and executes world reset script to delete saves and restart server.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 23:11:54 -05:00
Bastian de Byl
c2d117bd95 feat: add systemd timer for zomboid container stats
Deploy systemd timer that writes zomboid container stats to
zomboid-stats.json every 30 seconds for gregtime to read.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 23:10:05 -05:00
62 changed files with 1310 additions and 189 deletions

View File

@@ -9,7 +9,7 @@ This is a home infrastructure deployment repository using Ansible for automated
## Development Commands
### Core Commands
- `make` or `make lint` - Run linting (yamllint + ansible-lint) on all YAML files
- `make` or `make lint` - Run yamllint on all YAML files. Output may only show "Running yamllint..." and "Done." with no errors listed — this means linting passed. Do NOT run yamllint or ansible-lint manually; `make lint` is the only lint step needed.
- `make deploy` - Deploy all configurations to the home server
- `make deploy TAGS=sometag` - Deploy only specific tagged tasks
- `make deploy TARGET=specific-host` - Deploy to specific host instead of all
@@ -96,9 +96,22 @@ Tasks are tagged by service/component for selective deployment:
## Target Environment
- Single target host: `home.bdebyl.net`
- Single target host: `home.debyl.io`
- OS: Fedora (ansible_user: fedora)
- Container runtime: Podman
- Web server: Caddy with automatic HTTPS and built-in security (replaced nginx + ModSecurity)
- All services accessible via HTTPS with automatic certificate renewal
- ~~CI/CD: Drone CI infrastructure completely decommissioned~~
- ~~CI/CD: Drone CI infrastructure completely decommissioned~~
### Remote SSH Commands for Service Users
The `podman` user (and other service users) have `/bin/nologin` as their shell. To run commands as these users via SSH:
- **One-off commands**: `sudo -H -u podman bash -c 'command here'`
- **Interactive shell**: `sudo -H -u podman bash -c 'cd; bash'`
- **systemctl --user** requires `XDG_RUNTIME_DIR`:
```bash
sudo -H -u podman bash -c 'export XDG_RUNTIME_DIR=/run/user/$(id -u); systemctl --user <action> <service>'
```
Podman is a user-specific (rootless) container runtime, not a system service like Docker. The user context matters for all podman and systemctl --user operations. The default SSH user (`fedora`) has sudo access and can run commands directly.

View File

@@ -70,6 +70,7 @@ vault: ${ANSIBLE_VAULT} ${VAULT_FILE}
lint: ${LINT_YAML} ${SKIP_FILE}
@printf "Running yamllint...\n"
-@${LINT_YAML} ${YAML_FILES}
@printf "Done.\n"
# Git-crypt management
git-crypt-backup:

View File

@@ -8,6 +8,8 @@
- role: podman
# SSL certificates are now handled automatically by Caddy
# - role: ssl # REMOVED - Caddy handles all certificate management
- role: ollama
tags: ollama
- role: github-actions
- role: graylog-config
tags: graylog-config

View File

@@ -1,5 +1,5 @@
---
all:
hosts:
home.bdebyl.net:
home.debyl.io:
ansible_user: fedora

View File

@@ -12,7 +12,8 @@ deps:
python-docker,
]
fail2ban_jails: [sshd.local]
fail2ban_jails: [sshd.local, zomboid.local]
fail2ban_filters: [zomboid.conf]
services:
- crond

View File

@@ -0,0 +1,5 @@
[Definition]
# Match ZOMBOID_RATELIMIT firewall log entries
# Example: ZOMBOID_RATELIMIT: IN=eth0 OUT= MAC=... SRC=1.2.3.4 DST=...
failregex = ZOMBOID_RATELIMIT:.*SRC=<HOST>
ignoreregex =

View File

@@ -0,0 +1,9 @@
[zomboid]
enabled = true
filter = zomboid
banaction = iptables-allports
backend = systemd
maxretry = 5
findtime = 4h
bantime = 1w
ignoreip = 127.0.0.1/32 192.168.1.0/24

View File

@@ -10,3 +10,9 @@
ansible.builtin.service:
name: fail2ban
state: restarted
- name: restart fluent-bit
become: true
ansible.builtin.systemd:
name: fluent-bit
state: restarted

View File

@@ -2,25 +2,6 @@
# Fluent Bit - Log forwarder from journald to Graylog GELF
# Deployed as systemd service (not container) for direct journal access
# Clean up old container deployment if it exists
- name: stop and remove fluent-bit container if exists
become: true
become_user: "{{ podman_user }}"
containers.podman.podman_container:
name: fluent-bit
state: absent
ignore_errors: true
- name: disable old fluent-bit container systemd service
become: true
become_user: "{{ podman_user }}"
ansible.builtin.systemd:
name: fluent-bit
enabled: false
state: stopped
scope: user
ignore_errors: true
- name: install fluent-bit package
become: true
ansible.builtin.dnf:

View File

@@ -3,6 +3,9 @@
- import_tasks: security.yml
- import_tasks: service.yml
- import_tasks: fluent-bit.yml
tags: fluent-bit, graylog
- name: create the docker group
become: true
ansible.builtin.group:

View File

@@ -21,6 +21,16 @@
notify: restart_sshd
tags: security
- name: setup fail2ban filters
become: true
ansible.builtin.copy:
src: files/fail2ban/filters/{{ item }}
dest: /etc/fail2ban/filter.d/{{ item }}
mode: 0644
loop: "{{ fail2ban_filters }}"
notify: restart_fail2ban
tags: security
- name: setup fail2ban jails
become: true
ansible.builtin.copy:

View File

@@ -0,0 +1,155 @@
[SERVICE]
Flush 5
Daemon Off
Log_Level info
Parsers_File parsers.conf
# =============================================================================
# INPUT: Podman container logs
# =============================================================================
# Container logs come from conmon process with CONTAINER_NAME field
[INPUT]
Name systemd
Tag podman.*
Systemd_Filter _COMM=conmon
Read_From_Tail On
Strip_Underscores On
# =============================================================================
# INPUT: SSH logs for security monitoring
# =============================================================================
[INPUT]
Name systemd
Tag ssh.*
Systemd_Filter _SYSTEMD_UNIT=sshd.service
Read_From_Tail On
Strip_Underscores On
# =============================================================================
# INPUT: Kernel firewall logs for Zomboid connections
# =============================================================================
# Captures ZOMBOID_CONN firewall events with source IP for player correlation
[INPUT]
Name systemd
Tag firewall.zomboid
Systemd_Filter _TRANSPORT=kernel
Read_From_Tail On
Strip_Underscores On
# =============================================================================
# INPUT: Kernel firewall logs for Zomboid rate limiting
# =============================================================================
# Captures ZOMBOID_RATELIMIT firewall events for fail2ban monitoring
[INPUT]
Name systemd
Tag firewall.zomboid.ratelimit
Systemd_Filter _TRANSPORT=kernel
Read_From_Tail On
Strip_Underscores On
# =============================================================================
# INPUT: Fail2ban actions (ban/unban events)
# =============================================================================
[INPUT]
Name systemd
Tag fail2ban.*
Systemd_Filter _SYSTEMD_UNIT=fail2ban.service
Read_From_Tail On
Strip_Underscores On
# =============================================================================
# INPUT: Caddy access logs (JSON format)
# =============================================================================
{% for log_name in caddy_log_names %}
[INPUT]
Name tail
Tag caddy.{{ log_name }}
Path {{ caddy_log_path }}/{{ log_name }}.log
Parser caddy_json
Read_From_Head False
Refresh_Interval 5
DB /var/lib/fluent-bit/caddy_{{ log_name }}.db
{% endfor %}
# =============================================================================
# FILTERS: Add metadata for Graylog categorization
# =============================================================================
# Exclude Graylog stack containers to prevent feedback loop
[FILTER]
Name grep
Match podman.*
Exclude CONTAINER_NAME ^graylog
[FILTER]
Name record_modifier
Match podman.*
Record host {{ ansible_hostname }}
Record source podman
Record log_type container
[FILTER]
Name record_modifier
Match ssh.*
Record host {{ ansible_hostname }}
Record source sshd
Record log_type security
# Copy msg to MESSAGE for caddy logs (GELF requires MESSAGE)
[FILTER]
Name modify
Match caddy.*
Copy msg MESSAGE
[FILTER]
Name record_modifier
Match caddy.*
Record host {{ ansible_hostname }}
Record source caddy
Record log_type access
# Filter kernel logs to only keep ZOMBOID_CONN messages
[FILTER]
Name grep
Match firewall.zomboid
Regex MESSAGE ZOMBOID_CONN
[FILTER]
Name record_modifier
Match firewall.zomboid
Record host {{ ansible_hostname }}
Record source firewall
Record log_type zomboid_connection
# Filter kernel logs to only keep ZOMBOID_RATELIMIT messages
[FILTER]
Name grep
Match firewall.zomboid.ratelimit
Regex MESSAGE ZOMBOID_RATELIMIT
[FILTER]
Name record_modifier
Match firewall.zomboid.ratelimit
Record host {{ ansible_hostname }}
Record source firewall
Record log_type zomboid_ratelimit
# Fail2ban ban/unban events
[FILTER]
Name record_modifier
Match fail2ban.*
Record host {{ ansible_hostname }}
Record source fail2ban
Record log_type security
# =============================================================================
# OUTPUT: All logs to Graylog GELF UDP
# =============================================================================
# Graylog needs a GELF UDP input configured on port 12203
[OUTPUT]
Name gelf
Match *
Host 127.0.0.1
Port 12202
Mode tcp
Gelf_Short_Message_Key MESSAGE
Gelf_Host_Key host

View File

@@ -0,0 +1,24 @@
[PARSER]
Name caddy_json
Format json
Time_Key ts
Time_Format %s.%L
# Generic JSON parser for nested message fields
[PARSER]
Name json
Format json
# Parse ZOMBOID_CONN firewall logs to extract source IP
# Example: ZOMBOID_CONN: IN=enp0s31f6 OUT= MAC=... SRC=45.5.113.90 DST=192.168.1.10 ...
[PARSER]
Name zomboid_firewall
Format regex
Regex ZOMBOID_CONN:.*SRC=(?<src_ip>[0-9.]+).*DST=(?<dst_ip>[0-9.]+).*DPT=(?<dst_port>[0-9]+)
# Parse ZOMBOID_RATELIMIT firewall logs to extract source IP
# Example: ZOMBOID_RATELIMIT: IN=enp0s31f6 OUT= MAC=... SRC=45.5.113.90 DST=192.168.1.10 ...
[PARSER]
Name zomboid_ratelimit
Format regex
Regex ZOMBOID_RATELIMIT:.*SRC=(?<src_ip>[0-9.]+).*DST=(?<dst_ip>[0-9.]+).*DPT=(?<dst_port>[0-9]+)

View File

@@ -3,7 +3,17 @@ gitea_runner_user: gitea-runner
gitea_runner_home: /home/gitea-runner
gitea_runner_version: "0.2.13"
gitea_runner_arch: linux-amd64
gitea_instance_url: https://git.debyl.io
gitea_runner_capacity: 4
# Multiple Gitea instances to run actions runners for
gitea_runners:
- name: debyl
instance_url: https://git.debyl.io
- name: skudak
instance_url: https://git.skudak.com
# Old single-instance format (replaced by gitea_runners list above):
# gitea_instance_url: https://git.debyl.io
# Paths
act_runner_bin: /usr/local/bin/act_runner

View File

@@ -1,10 +1,11 @@
---
- name: restart act_runner
- name: restart act_runner services
become: true
ansible.builtin.systemd:
name: act_runner
name: "act_runner-{{ item.name }}"
state: restarted
daemon_reload: true
loop: "{{ gitea_runners }}"
- name: restart podman socket
become: true

View File

@@ -35,6 +35,13 @@
when: not esp_idf_dir.stat.exists
tags: gitea-actions
- name: add ESP-IDF to git safe.directory
become: true
ansible.builtin.command:
cmd: git config --global --add safe.directory {{ esp_idf_path }}
changed_when: false
tags: gitea-actions
- name: ensure ESP-IDF submodules are initialized
become: true
ansible.builtin.command:
@@ -79,7 +86,7 @@
export IDF_TOOLS_PATH="{{ gitea_runner_home }}/.espressif"
{{ esp_idf_path }}/install.sh esp32
args:
creates: "{{ gitea_runner_home }}/.espressif/tools"
creates: "{{ gitea_runner_home }}/.espressif/python_env"
environment:
HOME: "{{ gitea_runner_home }}"
tags: gitea-actions

View File

@@ -15,31 +15,36 @@
mode: "0755"
tags: gitea-actions
- name: create act_runner working directory
- name: create per-runner working directory
become: true
ansible.builtin.file:
path: "{{ act_runner_work_dir }}"
path: "{{ act_runner_work_dir }}/{{ item.name }}"
state: directory
owner: "{{ gitea_runner_user }}"
group: "{{ gitea_runner_user }}"
mode: "0755"
loop: "{{ gitea_runners }}"
tags: gitea-actions
- name: create act_runner cache directory
- name: create per-runner cache directory
become: true
ansible.builtin.file:
path: "{{ act_runner_work_dir }}/cache"
path: "{{ act_runner_work_dir }}/{{ item.name }}/cache"
state: directory
owner: "{{ gitea_runner_user }}"
group: "{{ gitea_runner_user }}"
mode: "0755"
loop: "{{ gitea_runners }}"
tags: gitea-actions
- name: deploy act_runner configuration
- name: deploy per-runner configuration
become: true
ansible.builtin.template:
src: config.yaml.j2
dest: "{{ act_runner_config_dir }}/config.yaml"
dest: "{{ act_runner_config_dir }}/config-{{ item.name }}.yaml"
mode: "0644"
notify: restart act_runner
vars:
runner_name: "{{ item.name }}"
loop: "{{ gitea_runners }}"
notify: restart act_runner services
tags: gitea-actions

View File

@@ -1,17 +1,37 @@
---
- name: deploy act_runner systemd service
become: true
ansible.builtin.template:
src: act_runner.service.j2
dest: /etc/systemd/system/act_runner.service
mode: "0644"
notify: restart act_runner
tags: gitea-actions
- name: enable act_runner service
- name: stop and disable legacy act_runner service
become: true
ansible.builtin.systemd:
name: act_runner
state: stopped
enabled: false
failed_when: false
tags: gitea-actions
- name: remove legacy act_runner service file
become: true
ansible.builtin.file:
path: /etc/systemd/system/act_runner.service
state: absent
tags: gitea-actions
- name: deploy per-runner systemd service
become: true
ansible.builtin.template:
src: act_runner.service.j2
dest: "/etc/systemd/system/act_runner-{{ item.name }}.service"
mode: "0644"
vars:
runner_name: "{{ item.name }}"
loop: "{{ gitea_runners }}"
notify: restart act_runner services
tags: gitea-actions
- name: enable per-runner services
become: true
ansible.builtin.systemd:
name: "act_runner-{{ item.name }}"
daemon_reload: true
enabled: true
loop: "{{ gitea_runners }}"
tags: gitea-actions

View File

@@ -32,3 +32,42 @@
state: directory
mode: "0755"
tags: gitea-actions
- name: create .ssh directory
become: true
ansible.builtin.file:
path: "{{ gitea_runner_home }}/.ssh"
state: directory
owner: "{{ gitea_runner_user }}"
group: "{{ gitea_runner_user }}"
mode: "0700"
tags: gitea-actions
- name: generate SSH key for gitea-runner
become: true
become_user: "{{ gitea_runner_user }}"
ansible.builtin.command:
cmd: ssh-keygen -t ed25519 -f {{ gitea_runner_home }}/.ssh/id_ed25519 -N "" -C "gitea-runner@galactica"
creates: "{{ gitea_runner_home }}/.ssh/id_ed25519"
tags: gitea-actions
- name: add Gitea SSH host keys to known_hosts
become: true
become_user: "{{ gitea_runner_user }}"
ansible.builtin.shell:
cmd: ssh-keyscan -p 2222 {{ item }} >> {{ gitea_runner_home }}/.ssh/known_hosts 2>/dev/null
args:
creates: "{{ gitea_runner_home }}/.ssh/known_hosts"
loop:
- git.skudak.com
- git.debyl.io
tags: gitea-actions
- name: set known_hosts permissions
become: true
ansible.builtin.file:
path: "{{ gitea_runner_home }}/.ssh/known_hosts"
owner: "{{ gitea_runner_user }}"
group: "{{ gitea_runner_user }}"
mode: "0644"
tags: gitea-actions

View File

@@ -1,11 +1,11 @@
[Unit]
Description=Gitea Actions runner
Description=Gitea Actions runner ({{ runner_name }})
Documentation=https://gitea.com/gitea/act_runner
After=network.target podman.socket
[Service]
ExecStart={{ act_runner_bin }} daemon --config {{ act_runner_config_dir }}/config.yaml
WorkingDirectory={{ act_runner_work_dir }}
ExecStart={{ act_runner_bin }} daemon --config {{ act_runner_config_dir }}/config-{{ runner_name }}.yaml
WorkingDirectory={{ act_runner_work_dir }}/{{ runner_name }}
TimeoutSec=0
RestartSec=10
Restart=always

View File

@@ -2,8 +2,8 @@ log:
level: info
runner:
file: {{ act_runner_work_dir }}/.runner
capacity: 1
file: {{ act_runner_work_dir }}/{{ runner_name }}/.runner
capacity: {{ gitea_runner_capacity | default(4) }}
timeout: 3h
insecure: false
fetch_timeout: 5s
@@ -15,7 +15,7 @@ runner:
cache:
enabled: true
dir: {{ act_runner_work_dir }}/cache
dir: {{ act_runner_work_dir }}/{{ runner_name }}/cache
container:
network: host
@@ -27,4 +27,4 @@ container:
force_pull: false
host:
workdir_parent: {{ act_runner_work_dir }}/workdir
workdir_parent: {{ act_runner_work_dir }}/{{ runner_name }}/workdir

View File

@@ -56,6 +56,30 @@ graylog_streams:
type: 1
inverted: false
- title: "zomboid-connections"
description: "Zomboid game server connection logs"
rules:
- field: "log_type"
value: "zomboid_connection"
type: 1
inverted: false
- title: "zomboid-ratelimit"
description: "Zomboid rate-limited connection attempts"
rules:
- field: "log_type"
value: "zomboid_ratelimit"
type: 1
inverted: false
- title: "fail2ban-actions"
description: "Fail2ban ban and unban events"
rules:
- field: "source"
value: "fail2ban"
type: 1
inverted: false
# Pipeline definitions
graylog_pipelines:
- title: "GeoIP Enrichment"
@@ -65,6 +89,7 @@ graylog_pipelines:
match: "EITHER"
rules:
- "geoip_caddy_access"
- "geoip_zomboid"
- title: "Debyltech Event Classification"
description: "Categorize debyltech-api events"
@@ -98,6 +123,20 @@ graylog_pipeline_rules:
set_field("geo_coordinates", geo["coordinates"]);
end
- title: "geoip_zomboid"
description: "GeoIP lookup for Zomboid connection logs"
source: |
rule "GeoIP for Zomboid"
when
has_field("src_ip")
then
let ip = to_string($message.src_ip);
let geo = lookup("geoip-lookup", ip);
set_field("geo_country", geo["country"].iso_code);
set_field("geo_city", geo["city"].names.en);
set_field("geo_coordinates", geo["coordinates"]);
end
- title: "classify_order_events"
description: "Classify order events"
source: |
@@ -164,6 +203,8 @@ graylog_pipeline_connections:
streams:
- "caddy-access"
- "caddy-fulfillr"
- "zomboid-connections"
- "zomboid-ratelimit"
- pipeline: "Debyltech Event Classification"
streams:

View File

@@ -0,0 +1,6 @@
---
ollama_models:
- dolphin-phi
- dolphin-mistral
ollama_host: "127.0.0.1"
ollama_port: 11434

View File

@@ -0,0 +1,8 @@
---
- name: restart ollama
become: true
ansible.builtin.systemd:
name: ollama
state: restarted
daemon_reload: true
tags: ollama

View File

@@ -0,0 +1,3 @@
---
dependencies:
- role: common

View File

@@ -0,0 +1,11 @@
---
- name: check if ollama is already installed
ansible.builtin.stat:
path: /usr/local/bin/ollama
register: ollama_binary
- name: install ollama via official install script
become: true
ansible.builtin.shell: |
curl -fsSL https://ollama.com/install.sh | sh
when: not ollama_binary.stat.exists

View File

@@ -0,0 +1,9 @@
---
- import_tasks: install.yml
tags: ollama
- import_tasks: service.yml
tags: ollama
- import_tasks: models.yml
tags: ollama

View File

@@ -0,0 +1,10 @@
---
- name: pull ollama models
become: true
ansible.builtin.command: ollama pull {{ item }}
loop: "{{ ollama_models }}"
register: result
retries: 3
delay: 10
until: result is not failed
changed_when: "'pulling' in result.stderr or 'pulling' in result.stdout"

View File

@@ -0,0 +1,23 @@
---
- name: create ollama systemd override directory
become: true
ansible.builtin.file:
path: /etc/systemd/system/ollama.service.d
state: directory
mode: 0755
- name: template ollama environment override
become: true
ansible.builtin.template:
src: ollama.env.j2
dest: /etc/systemd/system/ollama.service.d/override.conf
mode: 0644
notify: restart ollama
- name: enable and start ollama service
become: true
ansible.builtin.systemd:
name: ollama
enabled: true
state: started
daemon_reload: true

View File

@@ -0,0 +1,4 @@
[Service]
Environment="OLLAMA_HOST={{ ollama_host }}:{{ ollama_port }}"
Environment="OLLAMA_NUM_PARALLEL=1"
Environment="OLLAMA_MAX_LOADED_MODELS=1"

File diff suppressed because one or more lines are too long

View File

@@ -633,3 +633,38 @@
entity_id: 81c486d682afcc94e98e377475cc92fc
domain: light
mode: single
- id: '1768862300896'
alias: Bedroom On
description: ''
triggers:
- type: turned_on
device_id: afb9734fe9b187ab6881a64d24e1c2f5
entity_id: 27efa149b9ebb388e7c21ba89e671b42
domain: switch
trigger: device
conditions: []
actions:
- action: light.turn_on
metadata: {}
data:
brightness_pct: 100
target:
area_id: bedroom
mode: single
- id: '1768862339192'
alias: Bedroom Off
description: ''
triggers:
- type: turned_off
device_id: afb9734fe9b187ab6881a64d24e1c2f5
entity_id: 27efa149b9ebb388e7c21ba89e671b42
domain: switch
trigger: device
conditions: []
actions:
- action: light.turn_off
metadata: {}
data: {}
target:
area_id: bedroom
mode: single

View File

@@ -42,11 +42,3 @@
scope: user
tags:
- zomboid
- name: restart fluent-bit
become: true
ansible.builtin.systemd:
name: fluent-bit
state: restarted
tags:
- fluent-bit

View File

@@ -4,7 +4,7 @@
container_name: awsddns
container_image: "{{ image }}"
- name: create home.bdebyl.net awsddns server container
- name: create home.debyl.io awsddns server container
become: true
become_user: "{{ podman_user }}"
diff: false

View File

@@ -0,0 +1,59 @@
---
- name: create backup SSH key directory
become: true
ansible.builtin.file:
path: /etc/ssh/backup_keys
state: directory
owner: root
group: root
mode: 0700
- name: deploy {{ backup_name }} backup SSH key
become: true
ansible.builtin.copy:
content: "{{ ssh_key_content }}"
dest: "{{ ssh_key_path }}"
owner: root
group: root
mode: 0600
setype: ssh_home_t
- name: template {{ backup_name }} backup script
become: true
ansible.builtin.template:
src: nextcloud/cloud-backup.sh.j2
dest: "{{ script_path }}"
owner: root
group: root
mode: 0755
setype: bin_t
- name: template {{ backup_name }} backup systemd service
become: true
ansible.builtin.template:
src: nextcloud/cloud-backup.service.j2
dest: "/etc/systemd/system/{{ backup_name }}-backup.service"
owner: root
group: root
mode: 0644
vars:
instance_name: "{{ backup_name }}"
- name: template {{ backup_name }} backup systemd timer
become: true
ansible.builtin.template:
src: nextcloud/cloud-backup.timer.j2
dest: "/etc/systemd/system/{{ backup_name }}-backup.timer"
owner: root
group: root
mode: 0644
vars:
instance_name: "{{ backup_name }}"
- name: enable and start {{ backup_name }} backup timer
become: true
ansible.builtin.systemd:
name: "{{ backup_name }}-backup.timer"
enabled: true
state: started
daemon_reload: true

View File

@@ -75,7 +75,7 @@
- import_tasks: podman/podman-check.yml
vars:
container_name: graylog-mongo
container_image: docker.io/mongo:6
container_image: "{{ mongo_image }}"
tags: graylog
- name: create graylog-mongo container
@@ -83,7 +83,7 @@
become_user: "{{ podman_user }}"
containers.podman.podman_container:
name: graylog-mongo
image: docker.io/mongo:6
image: "{{ mongo_image }}"
state: started
restart_policy: on-failure:3
log_driver: journald
@@ -103,7 +103,7 @@
- import_tasks: podman/podman-check.yml
vars:
container_name: graylog-opensearch
container_image: docker.io/opensearchproject/opensearch:2
container_image: "{{ opensearch_image }}"
tags: graylog
- name: create graylog-opensearch container
@@ -111,7 +111,7 @@
become_user: "{{ podman_user }}"
containers.podman.podman_container:
name: graylog-opensearch
image: docker.io/opensearchproject/opensearch:2
image: "{{ opensearch_image }}"
state: started
restart_policy: on-failure:3
log_driver: journald
@@ -135,7 +135,7 @@
- import_tasks: podman/podman-check.yml
vars:
container_name: graylog
container_image: docker.io/graylog/graylog:6.0
container_image: "{{ image }}"
tags: graylog
# Graylog uses host network to reach MongoDB/OpenSearch on 127.0.0.1
@@ -145,7 +145,7 @@
become_user: "{{ podman_user }}"
containers.podman.podman_container:
name: graylog
image: docker.io/graylog/graylog:6.0
image: "{{ image }}"
state: started
restart_policy: on-failure:3
log_driver: journald

View File

@@ -0,0 +1,54 @@
---
- name: create n8n host directory volumes
become: true
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ podman_user }}"
group: "{{ podman_user }}"
mode: 0755
notify: restorecon podman
loop:
- "{{ n8n_path }}"
- name: set n8n volume ownership for node user
become: true
become_user: "{{ podman_user }}"
ansible.builtin.command:
cmd: podman unshare chown -R 1000:1000 {{ n8n_path }}
changed_when: false
- name: flush handlers
ansible.builtin.meta: flush_handlers
- import_tasks: podman/podman-check.yml
vars:
container_name: n8n
container_image: "{{ image }}"
- name: create n8n container
become: true
become_user: "{{ podman_user }}"
containers.podman.podman_container:
name: n8n
image: "{{ image }}"
image_strict: true
restart_policy: on-failure:3
log_driver: journald
network: shared
volumes:
- "{{ n8n_path }}:/home/node/.n8n"
ports:
- 5678:5678/tcp
env:
N8N_HOST: "{{ n8n_server_name }}"
N8N_PORT: "5678"
N8N_PROTOCOL: https
WEBHOOK_URL: "https://{{ n8n_server_name }}/"
N8N_SECURE_COOKIE: "true"
GENERIC_TIMEZONE: America/New_York
- name: create systemd startup job for n8n
include_tasks: podman/systemd-generate.yml
vars:
container_name: n8n

View File

@@ -83,3 +83,13 @@
include_tasks: podman/systemd-generate.yml
vars:
container_name: cloud
- include_tasks: containers/cloud-backup.yml
vars:
backup_name: cloud
data_path: "{{ cloud_path }}/data"
ssh_key_path: /etc/ssh/backup_keys/cloud
ssh_key_content: "{{ cloud_backup_ssh_key }}"
ssh_user: cloud
remote_path: /mnt/glacier/nextcloud
script_path: /usr/local/bin/cloud-backup.sh

View File

@@ -40,7 +40,13 @@
- host
env:
TZ: America/New_York
# Gemini AI for @bot ask command
# Ollama + SearXNG for FISTO AI responses
OLLAMA_HOST: "http://127.0.0.1:11434"
OLLAMA_MODEL: "dolphin-mistral"
OLLAMA_FALLBACK_MODEL: "dolphin-phi"
OLLAMA_NUM_PREDICT: "300"
SEARXNG_URL: "http://127.0.0.1:8080"
# Gemini API for @bot gemini command
GEMINI_API_KEY: "{{ gemini_api_key }}"
# Zomboid RCON configuration for Discord restart command
ZOMBOID_RCON_HOST: "127.0.0.1"
@@ -52,8 +58,9 @@
- "{{ gregtime_path }}/logs:/app/logs"
- "{{ gregtime_path }}/data:/app/data"
- "{{ zomboid_path }}/data:/zomboid-logs:ro"
- "{{ podman_volumes }}/zomboid-stats.json:/app/data/zomboid-stats.json:ro"
- name: create systemd startup job for gregtime
include_tasks: podman/systemd-generate.yml
vars:
container_name: gregtime
container_name: gregtime

View File

@@ -0,0 +1,59 @@
---
- name: create searxng host directory volumes
become: true
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ podman_subuid.stdout }}"
group: "{{ podman_user }}"
mode: 0755
notify: restorecon podman
loop:
- "{{ searxng_path }}/config"
- "{{ searxng_path }}/data"
- name: template searxng settings
become: true
ansible.builtin.template:
src: searxng/settings.yml.j2
dest: "{{ searxng_path }}/config/settings.yml"
owner: "{{ podman_subuid.stdout }}"
group: "{{ podman_user }}"
mode: 0644
- name: unshare chown the searxng volumes for internal uid 977
become: true
become_user: "{{ podman_user }}"
changed_when: false
ansible.builtin.shell: |
podman unshare chown -R 977:977 {{ searxng_path }}/config
podman unshare chown -R 977:977 {{ searxng_path }}/data
- name: flush handlers
ansible.builtin.meta: flush_handlers
- import_tasks: podman/podman-check.yml
vars:
container_name: searxng
container_image: "{{ image }}"
- name: create searxng container
become: true
become_user: "{{ podman_user }}"
containers.podman.podman_container:
name: searxng
image: "{{ image }}"
restart_policy: on-failure:3
log_driver: journald
network:
- host
env:
SEARXNG_BASE_URL: "http://127.0.0.1:8080/"
volumes:
- "{{ searxng_path }}/config:/etc/searxng"
- "{{ searxng_path }}/data:/srv/searxng/data"
- name: create systemd startup job for searxng
include_tasks: podman/systemd-generate.yml
vars:
container_name: searxng

View File

@@ -0,0 +1,38 @@
---
- name: create uptime-kuma-personal host directory volumes
become: true
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ podman_user }}"
group: "{{ podman_user }}"
mode: 0755
notify: restorecon podman
loop:
- "{{ uptime_kuma_personal_path }}/data"
- name: flush handlers
ansible.builtin.meta: flush_handlers
- import_tasks: podman/podman-check.yml
vars:
container_name: uptime-kuma-personal
container_image: "{{ image }}"
- name: create uptime-kuma-personal container
become: true
become_user: "{{ podman_user }}"
containers.podman.podman_container:
name: uptime-kuma-personal
image: "{{ image }}"
restart_policy: on-failure:3
log_driver: journald
volumes:
- "{{ uptime_kuma_personal_path }}/data:/app/data"
ports:
- "3002:3001/tcp"
- name: create systemd startup job for uptime-kuma-personal
include_tasks: podman/systemd-generate.yml
vars:
container_name: uptime-kuma-personal

View File

@@ -12,6 +12,95 @@
- "{{ zomboid_path }}/server"
- "{{ zomboid_path }}/data"
- "{{ zomboid_path }}/scripts"
- "{{ zomboid_path }}/logs"
- name: create podman bin directory
become: true
ansible.builtin.file:
path: "{{ podman_home }}/bin"
state: directory
owner: "{{ podman_user }}"
group: "{{ podman_user }}"
mode: '0755'
- name: deploy zomboid world reset script
become: true
ansible.builtin.template:
src: zomboid/world-reset.sh.j2
dest: "{{ podman_home }}/bin/zomboid-world-reset.sh"
owner: "{{ podman_user }}"
group: "{{ podman_user }}"
mode: '0755'
- name: deploy zomboid world reset path unit
become: true
ansible.builtin.template:
src: zomboid/zomboid-world-reset.path.j2
dest: "{{ podman_home }}/.config/systemd/user/zomboid-world-reset.path"
owner: "{{ podman_user }}"
group: "{{ podman_user }}"
mode: '0644'
notify: reload zomboid systemd
- name: deploy zomboid world reset service unit
become: true
ansible.builtin.template:
src: zomboid/zomboid-world-reset.service.j2
dest: "{{ podman_home }}/.config/systemd/user/zomboid-world-reset.service"
owner: "{{ podman_user }}"
group: "{{ podman_user }}"
mode: '0644'
notify: reload zomboid systemd
- name: deploy zomboid stats script
become: true
ansible.builtin.template:
src: zomboid/zomboid-stats.sh.j2
dest: "{{ podman_home }}/bin/zomboid-stats.sh"
owner: "{{ podman_user }}"
group: "{{ podman_user }}"
mode: '0755'
- name: create zomboid stats file with correct permissions
become: true
ansible.builtin.file:
path: "{{ podman_volumes }}/zomboid-stats.json"
state: touch
owner: "{{ podman_user }}"
group: "{{ podman_user }}"
mode: '0644'
modification_time: preserve
access_time: preserve
- name: deploy zomboid stats service unit
become: true
ansible.builtin.template:
src: zomboid/zomboid-stats.service.j2
dest: "{{ podman_home }}/.config/systemd/user/zomboid-stats.service"
owner: "{{ podman_user }}"
group: "{{ podman_user }}"
mode: '0644'
notify: reload zomboid systemd
- name: deploy zomboid stats timer unit
become: true
ansible.builtin.template:
src: zomboid/zomboid-stats.timer.j2
dest: "{{ podman_home }}/.config/systemd/user/zomboid-stats.timer"
owner: "{{ podman_user }}"
group: "{{ podman_user }}"
mode: '0644'
notify: reload zomboid systemd
- name: enable zomboid stats timer
become: true
become_user: "{{ podman_user }}"
ansible.builtin.systemd:
name: zomboid-stats.timer
scope: user
enabled: true
state: started
daemon_reload: true
- name: copy zomboid entrypoint script
become: true
@@ -190,9 +279,240 @@
- zomboid_ini_stat.stat.exists
tags: zomboid-conf
# World reset tasks REMOVED - too dangerous to have in automation
# To reset the world manually:
# 1. Stop the server: systemctl --user stop zomboid.service
# 2. Delete saves: rm -rf /home/podman/.local/share/volumes/zomboid/data/Saves
# 3. Delete db: rm -rf /home/podman/.local/share/volumes/zomboid/data/db
# 4. Start the server: systemctl --user start zomboid.service
# Firewall logging for player IP correlation
# Logs new UDP connections to Zomboid port for IP address tracking
- name: add firewall rule to log zomboid connections
become: true
ansible.builtin.command: >
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 0
-p udp --dport 16261 -m conntrack --ctstate NEW
-j LOG --log-prefix "ZOMBOID_CONN: " --log-level 4
register: firewall_result
changed_when: "'already' not in firewall_result.stderr"
failed_when: false
notify: restart firewalld
tags: firewall
- name: add firewall rule to log zomboid connections (runtime)
become: true
ansible.builtin.command: >
firewall-cmd --direct --add-rule ipv4 filter INPUT 0
-p udp --dport 16261 -m conntrack --ctstate NEW
-j LOG --log-prefix "ZOMBOID_CONN: " --log-level 4
changed_when: false
failed_when: false
tags: firewall
# =============================================================================
# Add logging for port 16262 (mirrors existing 16261 logging)
# =============================================================================
- name: add firewall rule to log zomboid connections on 16262
become: true
ansible.builtin.command: >
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 0
-p udp --dport 16262 -m conntrack --ctstate NEW
-j LOG --log-prefix "ZOMBOID_CONN: " --log-level 4
register: firewall_result_16262
changed_when: "'already' not in firewall_result_16262.stderr"
failed_when: false
notify: restart firewalld
tags: firewall
- name: add firewall rule to log zomboid connections on 16262 (runtime)
become: true
ansible.builtin.command: >
firewall-cmd --direct --add-rule ipv4 filter INPUT 0
-p udp --dport 16262 -m conntrack --ctstate NEW
-j LOG --log-prefix "ZOMBOID_CONN: " --log-level 4
changed_when: false
failed_when: false
tags: firewall
# =============================================================================
# Zomboid Rate Limiting and Query Flood Protection
# =============================================================================
# These rules mitigate Steam server query floods while allowing legitimate play.
# Query packets are typically 53 bytes; game traffic is larger and sustained.
#
# Rule priority: 0=logging (existing), 1=allow established, 2=rate limit queries
# Allow established/related connections without rate limiting
# This ensures active players aren't affected by query rate limits
- name: allow established zomboid connections on 16261
become: true
ansible.builtin.command: >
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 1
-p udp --dport 16261 -m conntrack --ctstate ESTABLISHED,RELATED
-j ACCEPT
register: established_result
changed_when: "'already' not in established_result.stderr"
failed_when: false
notify: restart firewalld
tags: firewall
- name: allow established zomboid connections on 16262
become: true
ansible.builtin.command: >
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 1
-p udp --dport 16262 -m conntrack --ctstate ESTABLISHED,RELATED
-j ACCEPT
register: established_result_16262
changed_when: "'already' not in established_result_16262.stderr"
failed_when: false
notify: restart firewalld
tags: firewall
# =============================================================================
# Smart Zomboid Traffic Filtering (Packet-Size Based)
# =============================================================================
# Distinguishes legitimate players from scanner bots:
# - Players send varied packet sizes (53, 37, 1472 bytes)
# - Scanners only send 53-byte query packets
#
# Rule priority:
# 0 = LOG all (existing above)
# 1 = ACCEPT established (existing above)
# 2 = Mark + ACCEPT non-query packets (verifies player)
# 3 = ACCEPT queries from verified IPs
# 4 = LOG rate-limited queries from unverified IPs
# 5 = DROP rate-limited queries from unverified IPs
# Priority 2: Mark IPs sending non-query packets as verified (1 hour TTL)
# Any packet NOT 53 bytes proves actual connection attempt
- name: mark verified players on 16261 (non-query packets)
become: true
ansible.builtin.command: >
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 2
-p udp --dport 16261 -m conntrack --ctstate NEW
-m length ! --length 53
-m recent --name zomboid_verified --set
-j ACCEPT
register: verify_result
changed_when: "'already' not in verify_result.stderr"
failed_when: false
notify: restart firewalld
tags: firewall
- name: mark verified players on 16262 (non-query packets)
become: true
ansible.builtin.command: >
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 2
-p udp --dport 16262 -m conntrack --ctstate NEW
-m length ! --length 53
-m recent --name zomboid_verified --set
-j ACCEPT
register: verify_result_16262
changed_when: "'already' not in verify_result_16262.stderr"
failed_when: false
notify: restart firewalld
tags: firewall
# Priority 3: Allow queries from verified players (within 1 hour)
- name: allow queries from verified players on 16261
become: true
ansible.builtin.command: >
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 3
-p udp --dport 16261 -m conntrack --ctstate NEW
-m length --length 53
-m recent --name zomboid_verified --rcheck --seconds 3600
-j ACCEPT
register: verified_query_result
changed_when: "'already' not in verified_query_result.stderr"
failed_when: false
notify: restart firewalld
tags: firewall
- name: allow queries from verified players on 16262
become: true
ansible.builtin.command: >
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 3
-p udp --dport 16262 -m conntrack --ctstate NEW
-m length --length 53
-m recent --name zomboid_verified --rcheck --seconds 3600
-j ACCEPT
register: verified_query_result_16262
changed_when: "'already' not in verified_query_result_16262.stderr"
failed_when: false
notify: restart firewalld
tags: firewall
# Priority 4: LOG rate-limited queries from unverified IPs
# Very aggressive: 2 burst, then 1 per hour
# Note: Uses same hashlimit name as DROP rule to share bucket
- name: log rate-limited queries from unverified IPs on 16261
become: true
ansible.builtin.command: >
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 4
-p udp --dport 16261 -m conntrack --ctstate NEW
-m length --length 53
-m hashlimit --hashlimit-above 1/hour --hashlimit-burst 2
--hashlimit-mode srcip --hashlimit-name zomboid_query_16261
--hashlimit-htable-expire 3600000
-j LOG --log-prefix "ZOMBOID_RATELIMIT: " --log-level 4
register: unverified_log_result
changed_when: "'already' not in unverified_log_result.stderr"
failed_when: false
notify: restart firewalld
tags: firewall
- name: log rate-limited queries from unverified IPs on 16262
become: true
ansible.builtin.command: >
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 4
-p udp --dport 16262 -m conntrack --ctstate NEW
-m length --length 53
-m hashlimit --hashlimit-above 1/hour --hashlimit-burst 2
--hashlimit-mode srcip --hashlimit-name zomboid_query_16262
--hashlimit-htable-expire 3600000
-j LOG --log-prefix "ZOMBOID_RATELIMIT: " --log-level 4
register: unverified_log_result_16262
changed_when: "'already' not in unverified_log_result_16262.stderr"
failed_when: false
notify: restart firewalld
tags: firewall
# Priority 5: DROP rate-limited queries from unverified IPs
# Note: Uses same hashlimit name as LOG rule to share bucket
- name: drop rate-limited queries from unverified IPs on 16261
become: true
ansible.builtin.command: >
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 5
-p udp --dport 16261 -m conntrack --ctstate NEW
-m length --length 53
-m hashlimit --hashlimit-above 1/hour --hashlimit-burst 2
--hashlimit-mode srcip --hashlimit-name zomboid_query_16261
--hashlimit-htable-expire 3600000
-j DROP
register: unverified_drop_result
changed_when: "'already' not in unverified_drop_result.stderr"
failed_when: false
notify: restart firewalld
tags: firewall
- name: drop rate-limited queries from unverified IPs on 16262
become: true
ansible.builtin.command: >
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 5
-p udp --dport 16262 -m conntrack --ctstate NEW
-m length --length 53
-m hashlimit --hashlimit-above 1/hour --hashlimit-burst 2
--hashlimit-mode srcip --hashlimit-name zomboid_query_16262
--hashlimit-htable-expire 3600000
-j DROP
register: unverified_drop_result_16262
changed_when: "'already' not in unverified_drop_result_16262.stderr"
failed_when: false
notify: restart firewalld
tags: firewall
# World reset is now triggered via Discord bot -> systemd path unit
# See zomboid-world-reset.path and zomboid-world-reset.service
- name: enable zomboid world reset path unit
become: true
become_user: "{{ podman_user }}"
ansible.builtin.systemd:
name: zomboid-world-reset.path
scope: user
enabled: true
state: started
daemon_reload: true

View File

@@ -130,3 +130,13 @@
register: trusted_domain_result
changed_when: "'System config value trusted_domains' in trusted_domain_result.stdout"
failed_when: false
- include_tasks: containers/cloud-backup.yml
vars:
backup_name: skudak-cloud
data_path: "{{ cloud_skudak_path }}/data"
ssh_key_path: /etc/ssh/backup_keys/skudak-cloud
ssh_key_content: "{{ cloud_skudak_backup_ssh_key }}"
ssh_user: skucloud
remote_path: /mnt/glacier/skudakcloud
script_path: /usr/local/bin/skudak-cloud-backup.sh

View File

@@ -31,7 +31,7 @@
- import_tasks: containers/home/hass.yml
vars:
image: ghcr.io/home-assistant/home-assistant:2025.9
image: ghcr.io/home-assistant/home-assistant:2026.1
tags: hass
- import_tasks: containers/home/partkeepr.yml
@@ -54,48 +54,65 @@
- import_tasks: containers/home/photos.yml
vars:
db_image: docker.io/tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
ml_image: ghcr.io/immich-app/immich-machine-learning:v2.4.1
ml_image: ghcr.io/immich-app/immich-machine-learning:v2.5.0
redis_image: docker.io/redis:6.2-alpine@sha256:eaba718fecd1196d88533de7ba49bf903ad33664a92debb24660a922ecd9cac8
image: ghcr.io/immich-app/immich-server:v2.4.1
image: ghcr.io/immich-app/immich-server:v2.5.0
tags: photos
- import_tasks: containers/home/cloud.yml
vars:
db_image: docker.io/library/mariadb:10.6
image: docker.io/library/nextcloud:32.0.1-apache
image: docker.io/library/nextcloud:33.0.0-apache
tags: cloud
- import_tasks: containers/skudak/cloud.yml
vars:
db_image: docker.io/library/mariadb:10.6
image: docker.io/library/nextcloud:32.0.1-apache
image: docker.io/library/nextcloud:33.0.0-apache
tags: skudak, skudak-cloud
- import_tasks: containers/debyltech/fulfillr.yml
vars:
image: "git.debyl.io/debyltech/fulfillr:20260109.0522"
image: git.debyl.io/debyltech/fulfillr:20260124.0411
tags: debyltech, fulfillr
- import_tasks: containers/debyltech/n8n.yml
vars:
image: docker.io/n8nio/n8n:2.11.3
tags: debyltech, n8n
- import_tasks: containers/debyltech/uptime-kuma.yml
vars:
image: docker.io/louislam/uptime-kuma:1
tags: debyltech, uptime-kuma
image: docker.io/louislam/uptime-kuma:2.0.2
tags: debyltech, uptime-debyltech
- import_tasks: containers/debyltech/geoip.yml
tags: debyltech, graylog, geoip
- import_tasks: containers/home/uptime-kuma.yml
vars:
image: docker.io/louislam/uptime-kuma:2.0.2
tags: home, uptime
- import_tasks: data/geoip.yml
tags: graylog, geoip
- import_tasks: containers/debyltech/graylog.yml
vars:
mongo_image: docker.io/mongo:7.0
opensearch_image: docker.io/opensearchproject/opensearch:2
image: docker.io/graylog/graylog:7.0.1
tags: debyltech, graylog
- import_tasks: containers/base/fluent-bit.yml
tags: fluent-bit, graylog
- import_tasks: containers/home/searxng.yml
vars:
image: docker.io/searxng/searxng:latest
tags: searxng
- import_tasks: containers/home/gregtime.yml
vars:
image: localhost/greg-time-bot:1.9.0
image: localhost/greg-time-bot:3.4.3
tags: gregtime
- import_tasks: containers/home/zomboid.yml
vars:
image: docker.io/cm2network/steamcmd:root
tags: zomboid

View File

@@ -112,6 +112,7 @@
- name: fetch subuid of {{ podman_user }}
become: true
changed_when: false
check_mode: false
ansible.builtin.shell: |
set -o pipefail && cat /etc/subuid | awk -F':' '/{{ podman_user }}/{ print $2 }' | head -n 1
register: podman_subuid

View File

@@ -130,11 +130,6 @@
# CI/Drone - REMOVED
# ci.bdebyl.net configuration removed - Drone CI infrastructure decommissioned
# Home server - redirect old to new
{{ home_server_name }} {
redir https://{{ home_server_name_io }}{uri} 302
}
# Home server - {{ home_server_name_io }}
{{ home_server_name_io }} {
{{ ip_restricted_site() }}
@@ -164,7 +159,7 @@
}
}
# Uptime Kuma - {{ uptime_kuma_server_name }}
# Uptime Kuma (Debyltech) - {{ uptime_kuma_server_name }}
{{ uptime_kuma_server_name }} {
{{ ip_restricted_site() }}
@@ -182,6 +177,24 @@
}
}
# Uptime Kuma (Personal) - {{ uptime_kuma_personal_server_name }}
{{ uptime_kuma_personal_server_name }} {
{{ ip_restricted_site() }}
handle @local {
import common_headers
reverse_proxy localhost:3002 {
# WebSocket support for live updates
flush_interval -1
}
}
log {
output file /var/log/caddy/uptime-kuma-personal.log
format json
}
}
# Graylog Logs - {{ logs_server_name }}
{{ logs_server_name }} {
# GELF HTTP endpoint - open for Lambda (auth via header)
@@ -319,6 +332,23 @@
}
}
# N8N Workflow Automation - {{ n8n_server_name }}
{{ n8n_server_name }} {
{{ ip_restricted_site() }}
handle @local {
import common_headers
reverse_proxy localhost:5678 {
flush_interval -1
}
}
log {
output file {{ caddy_log_path }}/n8n.log
format {{ caddy_log_format }}
}
}
# Fulfillr - {{ fulfillr_server_name }} (Static + API with IP restrictions)
{{ fulfillr_server_name }} {
{{ ip_restricted_site() }}

View File

@@ -1,77 +0,0 @@
[SERVICE]
Flush 5
Daemon Off
Log_Level info
Parsers_File parsers.conf
# =============================================================================
# INPUT: Podman container logs
# =============================================================================
# Container logs come from conmon process with CONTAINER_NAME field
[INPUT]
Name systemd
Tag podman.*
Systemd_Filter _COMM=conmon
Read_From_Tail On
Strip_Underscores On
# =============================================================================
# INPUT: SSH logs for security monitoring
# =============================================================================
[INPUT]
Name systemd
Tag ssh.*
Systemd_Filter _SYSTEMD_UNIT=sshd.service
Read_From_Tail On
Strip_Underscores On
# =============================================================================
# INPUT: Caddy access logs (JSON format)
# =============================================================================
{% for log_name in caddy_log_names %}
[INPUT]
Name tail
Tag caddy.{{ log_name }}
Path {{ caddy_log_path }}/{{ log_name }}.log
Parser caddy_json
Read_From_Head False
Refresh_Interval 5
DB /var/lib/fluent-bit/caddy_{{ log_name }}.db
{% endfor %}
# =============================================================================
# FILTERS: Add metadata for Graylog categorization
# =============================================================================
[FILTER]
Name record_modifier
Match podman.*
Record host {{ ansible_hostname }}
Record source podman
Record log_type container
[FILTER]
Name record_modifier
Match ssh.*
Record host {{ ansible_hostname }}
Record source sshd
Record log_type security
[FILTER]
Name record_modifier
Match caddy.*
Record host {{ ansible_hostname }}
Record source caddy
Record log_type access
# =============================================================================
# OUTPUT: All logs to Graylog GELF UDP
# =============================================================================
# Graylog needs a GELF UDP input configured on port 12203
[OUTPUT]
Name gelf
Match *
Host 127.0.0.1
Port 12203
Mode udp
Gelf_Short_Message_Key MESSAGE
Gelf_Host_Key host

View File

@@ -1,5 +0,0 @@
[PARSER]
Name caddy_json
Format json
Time_Key ts
Time_Format %s.%L

View File

@@ -10,7 +10,7 @@
},
"tax": {
"ein": "{{ fulfillr_tax_ein }}",
"ioss": "{{ fulfillr_tax_ioss }}"
"ioss": null
},
"sender_address": {
"city": "Newbury",
@@ -20,7 +20,18 @@
"phone": "6034160859",
"state": "NH",
"street1": "976 Route 103",
"street2": "Unit 509",
"street2": "Unit 95",
"zip": "03255"
}
},
"outreach": {
"outreach_table": "debyltech-outreach-prod",
"unsubscribe_table": "debyltech-unsubscribe-prod",
"email_log_table": "debyltech-email-log-prod",
"reviews_table": "debyltech-reviews-prod",
"hmac_secret_arn": "{{ fulfillr_hmac_arn }}",
"ses_from_email": "noreply@debyltech.com",
"ses_reply_to": "support@debyltech.com",
"ses_region": "us-east-1",
"base_url": "https://debyltech.com"
}
}

View File

@@ -0,0 +1,6 @@
[Unit]
Description=Nextcloud {{ instance_name }} backup to TrueNAS
[Service]
Type=oneshot
ExecStart={{ script_path }}

View File

@@ -0,0 +1,4 @@
#!/bin/bash
set -euo pipefail
rsync -az --exclude .ssh -e "ssh -i {{ ssh_key_path }} -o StrictHostKeyChecking=accept-new" \
{{ data_path }}/ {{ ssh_user }}@truenas.localdomain:{{ remote_path }}/

View File

@@ -0,0 +1,9 @@
[Unit]
Description=Daily Nextcloud {{ instance_name }} backup
[Timer]
OnCalendar=*-*-* 04:00:00
Persistent=true
[Install]
WantedBy=timers.target

View File

@@ -0,0 +1,35 @@
use_default_settings: true
general:
instance_name: "SearXNG"
debug: false
server:
bind_address: "127.0.0.1"
port: 8080
secret_key: "{{ searxng_secret_key }}"
limiter: false
image_proxy: false
search:
safe_search: 0
formats:
- html
- json
engines:
- name: duckduckgo
engine: duckduckgo
disabled: false
- name: google
engine: google
disabled: false
- name: wikipedia
engine: wikipedia
disabled: false
- name: bing
engine: bing
disabled: false

View File

@@ -84,6 +84,6 @@ fi
# Start server
cd "${INSTALL_DIR}"
echo "=== Starting Project Zomboid Server ==="
echo "Connect to: home.bdebyl.net:16261"
echo "Connect to: home.debyl.io:16261"
exec su -c "export LD_LIBRARY_PATH=${INSTALL_DIR}/jre64/lib:\${LD_LIBRARY_PATH} && ./start-server.sh ${SERVER_ARGS}" steam

View File

@@ -0,0 +1,57 @@
#!/bin/bash
# Zomboid World Reset Script
# Triggered by systemd path unit when discord bot requests reset
set -e
LOGFILE="{{ podman_home }}/.local/share/volumes/zomboid/logs/world-reset.log"
TRIGGER_FILE="{{ podman_home }}/.local/share/volumes/gregtime/data/zomboid-reset.trigger"
SERVER_NAME="{{ zomboid_server_names[zomboid_server_mode] }}"
SAVES_PATH="{{ podman_home }}/.local/share/volumes/zomboid/data/Saves/Multiplayer/${SERVER_NAME}"
DB_PATH="{{ podman_home }}/.local/share/volumes/zomboid/data/db/${SERVER_NAME}.db"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOGFILE"
}
# Ensure XDG_RUNTIME_DIR is set for systemctl --user
export XDG_RUNTIME_DIR="/run/user/$(id -u)"
log "World reset triggered"
# Read requester info from trigger file if available
# Note: Must use podman unshare because file is owned by container's UID (232071)
if podman unshare test -f "$TRIGGER_FILE"; then
REQUESTER=$(podman unshare cat "$TRIGGER_FILE")
log "Requested by: $REQUESTER"
podman unshare rm -f "$TRIGGER_FILE"
fi
# Stop server
log "Stopping zomboid service..."
systemctl --user stop zomboid.service || true
sleep 5
# Delete world (using podman unshare to work within user namespace)
log "Deleting world saves at: $SAVES_PATH"
if [[ -d "$SAVES_PATH" ]]; then
podman unshare rm -rf "$SAVES_PATH"
log "World saves deleted"
else
log "No world saves found at $SAVES_PATH"
fi
# Delete player database
log "Deleting player database at: $DB_PATH"
if [[ -f "$DB_PATH" ]]; then
podman unshare rm -f "$DB_PATH"
log "Player database deleted"
else
log "No database found at $DB_PATH"
fi
# Start server
log "Starting zomboid service..."
systemctl --user start zomboid.service
log "World reset complete - new world will generate on first connection"

View File

@@ -0,0 +1,6 @@
[Unit]
Description=Write Zomboid container stats to file
[Service]
Type=oneshot
ExecStart={{ podman_home }}/bin/zomboid-stats.sh

View File

@@ -0,0 +1,3 @@
#!/bin/bash
# Write zomboid container stats to file for gregtime to read
podman stats --no-stream --format json zomboid 2>/dev/null > {{ podman_volumes }}/zomboid-stats.json || true

View File

@@ -0,0 +1,9 @@
[Unit]
Description=Update Zomboid container stats every 30 seconds
[Timer]
OnBootSec=30s
OnUnitActiveSec=30s
[Install]
WantedBy=timers.target

View File

@@ -0,0 +1,9 @@
[Unit]
Description=Watch for Zomboid world reset trigger
[Path]
PathExists={{ podman_home }}/.local/share/volumes/gregtime/data/zomboid-reset.trigger
Unit=zomboid-world-reset.service
[Install]
WantedBy=default.target

View File

@@ -0,0 +1,8 @@
[Unit]
Description=Zomboid World Reset Service
[Service]
Type=oneshot
ExecStart={{ podman_home }}/bin/zomboid-world-reset.sh
StandardOutput=journal
StandardError=journal

Binary file not shown.