From 38561cb968bcc6bfb9ece11091033feeb00f3fea Mon Sep 17 00:00:00 2001 From: Bastian de Byl Date: Fri, 19 Dec 2025 10:39:56 -0500 Subject: [PATCH] gitea, zomboid updates, ssh key fixes --- ansible/roles/common/tasks/security.yml | 9 ++ .../common/templates/sshd-pq-kex.conf.j2 | 9 ++ ansible/roles/git/defaults/main.yml | 5 + ansible/roles/git/files/gitea-ssh-podman.te | 13 ++ ansible/roles/git/handlers/main.yml | 7 + ansible/roles/git/tasks/gitea-shell.yml | 28 ++++ ansible/roles/git/tasks/gitea.yml | 90 ++++++++++++ ansible/roles/git/tasks/main.yml | 8 +- ansible/roles/git/tasks/podman.yml | 80 +++++++++++ ansible/roles/git/tasks/selinux-podman.yml | 21 +++ ansible/roles/git/tasks/sshd.yml | 19 +++ .../git/templates/gitea-authorized-keys.j2 | 12 ++ ansible/roles/git/templates/gitea-shell.j2 | 27 ++++ ansible/roles/git/templates/gitea-shim.j2 | 15 ++ .../roles/git/templates/sshd-gitea.conf.j2 | 7 + ansible/roles/podman/defaults/main.yml | 23 ++++ .../podman/tasks/containers/base/awsddns.yml | 4 +- .../containers/home/.zomboid.yml.kate-swp | 0 .../podman/tasks/containers/home/zomboid.yml | 88 +++++++++++- ansible/roles/podman/tasks/main.yml | 4 +- .../roles/podman/templates/caddy/Caddyfile.j2 | 14 ++ .../podman/templates/zomboid/entrypoint.sh.j2 | 130 ++++++++---------- .../podman/templates/zomboid/install.scmd.j2 | 18 +++ ansible/vars/vault.yml | Bin 13337 -> 14697 bytes 24 files changed, 551 insertions(+), 80 deletions(-) create mode 100644 ansible/roles/common/templates/sshd-pq-kex.conf.j2 create mode 100644 ansible/roles/git/files/gitea-ssh-podman.te create mode 100644 ansible/roles/git/tasks/gitea-shell.yml create mode 100644 ansible/roles/git/tasks/gitea.yml create mode 100644 ansible/roles/git/tasks/podman.yml create mode 100644 ansible/roles/git/tasks/selinux-podman.yml create mode 100644 ansible/roles/git/tasks/sshd.yml create mode 100644 ansible/roles/git/templates/gitea-authorized-keys.j2 create mode 100644 ansible/roles/git/templates/gitea-shell.j2 create mode 100644 ansible/roles/git/templates/gitea-shim.j2 create mode 100644 ansible/roles/git/templates/sshd-gitea.conf.j2 create mode 100644 ansible/roles/podman/tasks/containers/home/.zomboid.yml.kate-swp create mode 100644 ansible/roles/podman/templates/zomboid/install.scmd.j2 diff --git a/ansible/roles/common/tasks/security.yml b/ansible/roles/common/tasks/security.yml index ef8b46f..baa7ed0 100644 --- a/ansible/roles/common/tasks/security.yml +++ b/ansible/roles/common/tasks/security.yml @@ -1,4 +1,13 @@ --- +- name: enable post-quantum key exchange for sshd + become: true + ansible.builtin.template: + src: sshd-pq-kex.conf.j2 + dest: /etc/ssh/sshd_config.d/30-pq-kex.conf + mode: 0600 + notify: restart_sshd + tags: security, sshd + - name: ensure sshd disallows passwords become: true ansible.builtin.lineinfile: diff --git a/ansible/roles/common/templates/sshd-pq-kex.conf.j2 b/ansible/roles/common/templates/sshd-pq-kex.conf.j2 new file mode 100644 index 0000000..eb7b432 --- /dev/null +++ b/ansible/roles/common/templates/sshd-pq-kex.conf.j2 @@ -0,0 +1,9 @@ +# Post-Quantum Key Exchange Algorithm +# Managed by Ansible - do not edit directly +# +# Enables sntrup761x25519-sha512 (hybrid post-quantum + classical) +# to protect against "store now, decrypt later" attacks +# +# This must be included BEFORE crypto-policies (40-redhat-crypto-policies.conf) + +KexAlgorithms sntrup761x25519-sha512@openssh.com,curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group14-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512 diff --git a/ansible/roles/git/defaults/main.yml b/ansible/roles/git/defaults/main.yml index 810aea9..5bebc4c 100644 --- a/ansible/roles/git/defaults/main.yml +++ b/ansible/roles/git/defaults/main.yml @@ -1,3 +1,8 @@ --- git_user: git git_home: "/srv/{{ git_user }}" + +# Gitea configuration +gitea_debyl_server_name: git.debyl.io +gitea_image: docker.gitea.com/gitea:1.25.2 +gitea_db_image: docker.io/library/postgres:14-alpine diff --git a/ansible/roles/git/files/gitea-ssh-podman.te b/ansible/roles/git/files/gitea-ssh-podman.te new file mode 100644 index 0000000..96f971a --- /dev/null +++ b/ansible/roles/git/files/gitea-ssh-podman.te @@ -0,0 +1,13 @@ +module gitea-ssh-podman 1.0; + +require { + type sshd_t; + type container_runtime_exec_t; + type user_home_t; + class file { execute execute_no_trans open read }; + class dir { search }; +} + +# Allow sshd to execute podman for AuthorizedKeysCommand +allow sshd_t container_runtime_exec_t:file { execute execute_no_trans open read }; +allow sshd_t user_home_t:dir search; diff --git a/ansible/roles/git/handlers/main.yml b/ansible/roles/git/handlers/main.yml index 755ef76..a64b0ff 100644 --- a/ansible/roles/git/handlers/main.yml +++ b/ansible/roles/git/handlers/main.yml @@ -15,3 +15,10 @@ tags: - git - selinux + +- name: restart sshd + become: true + ansible.builtin.systemd: + name: sshd.service + state: restarted + tags: git diff --git a/ansible/roles/git/tasks/gitea-shell.yml b/ansible/roles/git/tasks/gitea-shell.yml new file mode 100644 index 0000000..5f33c0b --- /dev/null +++ b/ansible/roles/git/tasks/gitea-shell.yml @@ -0,0 +1,28 @@ +--- +# Deploy gitea shim and shell for SSH passthrough + +# The shim is called by SSH when authorized_keys command runs +# It forwards gitea commands to the container +- name: create gitea shim script + become: true + ansible.builtin.template: + src: gitea-shim.j2 + dest: /usr/local/bin/gitea + mode: 0755 + tags: git, gitea + +# The shell is used if someone tries to SSH interactively +- name: create gitea-shell script + become: true + ansible.builtin.template: + src: gitea-shell.j2 + dest: /usr/local/bin/gitea-shell + mode: 0755 + tags: git, gitea + +- name: update git user shell to gitea-shell + become: true + ansible.builtin.user: + name: "{{ git_user }}" + shell: /usr/local/bin/gitea-shell + tags: git, gitea diff --git a/ansible/roles/git/tasks/gitea.yml b/ansible/roles/git/tasks/gitea.yml new file mode 100644 index 0000000..601b297 --- /dev/null +++ b/ansible/roles/git/tasks/gitea.yml @@ -0,0 +1,90 @@ +--- +# Deploy Gitea containers using Podman pod + +# Create pod for Gitea services +- name: create gitea-debyl pod + become: true + become_user: "{{ git_user }}" + containers.podman.podman_pod: + name: gitea-debyl-pod + state: started + ports: + - "3100:3000" + tags: gitea + +# PostgreSQL container in pod +- name: create gitea-debyl-postgres container + become: true + become_user: "{{ git_user }}" + containers.podman.podman_container: + name: gitea-debyl-postgres + image: "{{ gitea_db_image }}" + pod: gitea-debyl-pod + restart_policy: on-failure:3 + log_driver: journald + env: + POSTGRES_DB: gitea + POSTGRES_USER: gitea + POSTGRES_PASSWORD: "{{ gitea_debyl_db_pass }}" + volumes: + - "{{ git_home }}/volumes/gitea/psql:/var/lib/postgresql/data" + tags: gitea + +# Gitea container in pod +- name: create gitea-debyl container + become: true + become_user: "{{ git_user }}" + containers.podman.podman_container: + name: gitea-debyl + image: "{{ gitea_image }}" + pod: gitea-debyl-pod + restart_policy: on-failure:3 + log_driver: journald + env: + USER_UID: "1000" + USER_GID: "1000" + GITEA__database__DB_TYPE: postgres + GITEA__database__HOST: "127.0.0.1:5432" + GITEA__database__NAME: gitea + GITEA__database__USER: gitea + GITEA__database__PASSWD: "{{ gitea_debyl_db_pass }}" + GITEA__server__DOMAIN: "{{ gitea_debyl_server_name }}" + GITEA__server__ROOT_URL: "https://{{ gitea_debyl_server_name }}/" + GITEA__server__SSH_DOMAIN: "{{ gitea_debyl_server_name }}" + GITEA__server__START_SSH_SERVER: "false" + GITEA__server__DISABLE_SSH: "false" + GITEA__server__SSH_PORT: "22" + GITEA__security__SECRET_KEY: "{{ gitea_debyl_secret_key }}" + GITEA__security__INTERNAL_TOKEN: "{{ gitea_debyl_internal_token }}" + GITEA__security__INSTALL_LOCK: "true" + GITEA__service__DISABLE_REGISTRATION: "true" + GITEA__service__REQUIRE_SIGNIN_VIEW: "false" + volumes: + - "{{ git_home }}/volumes/gitea/data:/data" + - /etc/localtime:/etc/localtime:ro + tags: gitea + +# Generate systemd service for the pod +- name: create systemd job for gitea-debyl-pod + become: true + become_user: "{{ git_user }}" + ansible.builtin.shell: | + podman generate systemd --name gitea-debyl-pod --files --new + mv pod-gitea-debyl-pod.service {{ git_home }}/.config/systemd/user/ + mv container-gitea-debyl-postgres.service {{ git_home }}/.config/systemd/user/ + mv container-gitea-debyl.service {{ git_home }}/.config/systemd/user/ + args: + chdir: "{{ git_home }}" + changed_when: false + tags: gitea + +- name: enable gitea-debyl-pod service + become: true + become_user: "{{ git_user }}" + ansible.builtin.systemd: + name: pod-gitea-debyl-pod.service + daemon_reload: true + enabled: true + state: started + scope: user + tags: gitea diff --git a/ansible/roles/git/tasks/main.yml b/ansible/roles/git/tasks/main.yml index bbcd0c2..3f016bd 100644 --- a/ansible/roles/git/tasks/main.yml +++ b/ansible/roles/git/tasks/main.yml @@ -1,4 +1,10 @@ --- - import_tasks: user.yml -- import_tasks: systemd.yml +- import_tasks: podman.yml +- import_tasks: gitea-shell.yml +- import_tasks: sshd.yml - import_tasks: selinux.yml +- import_tasks: selinux-podman.yml +- import_tasks: gitea.yml +# git-daemon no longer needed - commented out +# - import_tasks: systemd.yml diff --git a/ansible/roles/git/tasks/podman.yml b/ansible/roles/git/tasks/podman.yml new file mode 100644 index 0000000..598e0ba --- /dev/null +++ b/ansible/roles/git/tasks/podman.yml @@ -0,0 +1,80 @@ +--- +# Rootless Podman setup for git user +# Enables running Gitea containers under the git user + +# Enable lingering for systemd user services +- name: check if git user lingering enabled + become: true + ansible.builtin.stat: + path: "/var/lib/systemd/linger/{{ git_user }}" + register: git_user_lingering + tags: git, gitea + +- name: enable git user lingering + become: true + ansible.builtin.command: | + loginctl enable-linger {{ git_user }} + when: not git_user_lingering.stat.exists + tags: git, gitea + +# Set ulimits for container operations +- name: set ulimits for git user + become: true + community.general.pam_limits: + domain: "{{ git_user }}" + limit_type: "{{ item.type }}" + limit_item: "{{ item.name }}" + value: "{{ item.value }}" + loop: + - { name: memlock, type: soft, value: "unlimited" } + - { name: memlock, type: hard, value: "unlimited" } + - { name: nofile, type: soft, value: 39693561 } + - { name: nofile, type: hard, value: 39693561 } + tags: git, gitea + +# Create container directories +- name: create git podman directories + become: true + become_user: "{{ git_user }}" + ansible.builtin.file: + path: "{{ item }}" + state: directory + mode: 0755 + loop: + - "{{ git_home }}/.config/systemd/user" + - "{{ git_home }}/volumes" + - "{{ git_home }}/volumes/gitea" + - "{{ git_home }}/volumes/gitea/data" + # NOTE: psql directory is created by PostgreSQL container with container user ownership + notify: restorecon git + tags: git, gitea + +# SELinux context for container volumes +- name: selinux context for git container volumes + become: true + community.general.sefcontext: + target: "{{ git_home }}/volumes(/.*)?" + setype: container_file_t + state: present + notify: restorecon git + tags: git, gitea, selinux + +# Enable podman socket for SSH key lookup via AuthorizedKeysCommand +- name: enable podman socket for git user + become: true + become_user: "{{ git_user }}" + ansible.builtin.systemd: + name: podman.socket + enabled: true + state: started + scope: user + tags: git, gitea + +# Fetch subuid for volume permissions +- name: fetch subuid of {{ git_user }} + become: true + changed_when: false + ansible.builtin.shell: | + set -o pipefail && cat /etc/subuid | awk -F':' '/{{ git_user }}/{ print $2 }' | head -n 1 + register: git_subuid + tags: always diff --git a/ansible/roles/git/tasks/selinux-podman.yml b/ansible/roles/git/tasks/selinux-podman.yml new file mode 100644 index 0000000..0d9b377 --- /dev/null +++ b/ansible/roles/git/tasks/selinux-podman.yml @@ -0,0 +1,21 @@ +--- +# SELinux policy for SSH + Podman integration + +- name: copy gitea SELinux policy module + become: true + ansible.builtin.copy: + src: gitea-ssh-podman.te + dest: /tmp/gitea-ssh-podman.te + mode: 0644 + register: selinux_policy + tags: git, gitea, selinux + +- name: compile and install gitea SELinux policy + become: true + ansible.builtin.shell: | + cd /tmp + checkmodule -M -m -o gitea-ssh-podman.mod gitea-ssh-podman.te + semodule_package -o gitea-ssh-podman.pp -m gitea-ssh-podman.mod + semodule -i gitea-ssh-podman.pp + when: selinux_policy.changed + tags: git, gitea, selinux diff --git a/ansible/roles/git/tasks/sshd.yml b/ansible/roles/git/tasks/sshd.yml new file mode 100644 index 0000000..1bb7de6 --- /dev/null +++ b/ansible/roles/git/tasks/sshd.yml @@ -0,0 +1,19 @@ +--- +# Configure SSH AuthorizedKeysCommand for Gitea + +- name: create gitea-authorized-keys script + become: true + ansible.builtin.template: + src: gitea-authorized-keys.j2 + dest: /usr/local/bin/gitea-authorized-keys + mode: 0755 + tags: git, gitea + +- name: deploy sshd gitea configuration + become: true + ansible.builtin.template: + src: sshd-gitea.conf.j2 + dest: /etc/ssh/sshd_config.d/50-gitea.conf + mode: 0644 + notify: restart sshd + tags: git, gitea diff --git a/ansible/roles/git/templates/gitea-authorized-keys.j2 b/ansible/roles/git/templates/gitea-authorized-keys.j2 new file mode 100644 index 0000000..275a213 --- /dev/null +++ b/ansible/roles/git/templates/gitea-authorized-keys.j2 @@ -0,0 +1,12 @@ +#!/bin/sh +# Query Gitea for SSH authorized keys +# Managed by Ansible - do not edit directly +# Arguments: %u (username) %t (key type) %k (key blob) + +# Use podman remote to connect via socket (avoids rootless pause process issues) +export CONTAINER_HOST=unix:///run/user/1001/podman/podman.sock + +/usr/bin/podman --remote exec -i --user 1000 gitea-debyl \ + /usr/local/bin/gitea keys \ + -c /data/gitea/conf/app.ini \ + -e git -u "$1" -t "$2" -k "$3" 2>/dev/null diff --git a/ansible/roles/git/templates/gitea-shell.j2 b/ansible/roles/git/templates/gitea-shell.j2 new file mode 100644 index 0000000..8a21758 --- /dev/null +++ b/ansible/roles/git/templates/gitea-shell.j2 @@ -0,0 +1,27 @@ +#!/bin/sh +# Gitea SSH shell - forwards commands to Gitea container +# Managed by Ansible - do not edit directly +# +# When sshd runs a forced command from authorized_keys, it invokes: +# -c "" +# The forced command is: /usr/local/bin/gitea --config=... serv key- +# SSH_ORIGINAL_COMMAND contains the client's requested command (e.g., git-upload-pack) + +# Use podman remote to connect via socket (avoids rootless pause process issues) +export CONTAINER_HOST=unix:///run/user/1001/podman/podman.sock + +if [ "$1" = "-c" ] && [ -n "$2" ]; then + # sshd invoked us with -c "command" - execute the command + # The command is: /usr/local/bin/gitea --config=... serv key- + exec $2 +elif [ -n "$SSH_ORIGINAL_COMMAND" ]; then + # Direct invocation with SSH_ORIGINAL_COMMAND (shouldn't happen normally) + echo "Interactive shell is disabled." + echo "Use: git clone git@{{ gitea_debyl_server_name }}:/.git" + exit 1 +else + # Interactive login attempt + echo "Interactive shell is disabled." + echo "Use: git clone git@{{ gitea_debyl_server_name }}:/.git" + exit 1 +fi diff --git a/ansible/roles/git/templates/gitea-shim.j2 b/ansible/roles/git/templates/gitea-shim.j2 new file mode 100644 index 0000000..1db4713 --- /dev/null +++ b/ansible/roles/git/templates/gitea-shim.j2 @@ -0,0 +1,15 @@ +#!/bin/sh +# Gitea shim - forwards gitea commands to the container +# Managed by Ansible - do not edit directly +# +# This script is called when sshd executes the forced command from authorized_keys: +# /usr/local/bin/gitea --config=/data/gitea/conf/app.ini serv key- +# +# SSH_ORIGINAL_COMMAND contains the client's git command (e.g., git-upload-pack ) + +# Use podman remote to connect via socket (avoids rootless pause process issues) +export CONTAINER_HOST=unix:///run/user/1001/podman/podman.sock + +exec /usr/bin/podman --remote exec -i --user 1000 \ + --env SSH_ORIGINAL_COMMAND="$SSH_ORIGINAL_COMMAND" \ + gitea-debyl /usr/local/bin/gitea "$@" diff --git a/ansible/roles/git/templates/sshd-gitea.conf.j2 b/ansible/roles/git/templates/sshd-gitea.conf.j2 new file mode 100644 index 0000000..a414c79 --- /dev/null +++ b/ansible/roles/git/templates/sshd-gitea.conf.j2 @@ -0,0 +1,7 @@ +# Gitea SSH Key Authentication +# Managed by Ansible - do not edit directly + +Match User {{ git_user }} + AuthorizedKeysFile none + AuthorizedKeysCommandUser {{ git_user }} + AuthorizedKeysCommand /usr/local/bin/gitea-authorized-keys %u %t %k diff --git a/ansible/roles/podman/defaults/main.yml b/ansible/roles/podman/defaults/main.yml index 5ecd67e..9677555 100644 --- a/ansible/roles/podman/defaults/main.yml +++ b/ansible/roles/podman/defaults/main.yml @@ -16,6 +16,28 @@ partsy_path: "{{ podman_volumes }}/partsy" photos_path: "{{ podman_volumes }}/photos" uptime_kuma_path: "{{ podman_volumes }}/uptime-kuma" zomboid_path: "{{ podman_volumes }}/zomboid" + +# Zomboid server mode: 'vanilla' or 'modded' +zomboid_server_mode: modded + +# Zomboid Discord integration (channel for /all chat relay) +zomboid_discord_channel_id: "1306000323642654751" + +# Zomboid RCON port for remote administration +zomboid_rcon_port: "27015" + +# Server names for each mode +zomboid_server_names: + vanilla: zomboid + modded: moddedjoboid + +# Mod configuration for modded server (deduplicated) +zomboid_mods: + workshop_items: >- + 3403870858;3171167894;3330403100;2409333430;3073430075;3379334330;3110913021;3366300557;3034636011;3409287192;3005903549;3161951724;3413704851;3413706334;3287727378;3226885926;2625625421;3418252689;3418253716;3152529790;2478247379;2942793445;2991201484;2913633066;2873290424;3428008364;3253385114;2846036306;2642541073;3435796523;3008795514;3447272250;3026723485;2900580391;2937786633;2870394916;3292659291;2969343830;2566953935;2962175696;3196180339;3258343790;3346905070;3320947974;3478633453;2952802178;3001592312;3052360250;3490370700;2932547723;2805630347;3504401781;2772575623;3110911330;3088951320;3213391371;2932549988;3041122351;2971246021;3539691958;3315443103;2886832257;2886832936;2886833398;2811383142;2799152995;3248388837;3566868353;3570973322;2897390033;3592777775;3596903773;3601417745;3614034284;3577903007;3480990544;3602388131;2463499011;3407042038;3405178154;3402493701;3402812859;3402491515;3430172149;3543229299;3616536783;3431734923;3429790870;2850935956;3307376332;3397182976;3432928943;3610005735;3540297822;3422418897;3426448380;3579640010;3389448389;3393821407;3044705007;2866258937;2544353492;3490188370;3508537032;3451167732;3461263912;2903771337 + # Build 42 requires backslash prefix for each mod ID + mod_ids: >- + \LifestyleHobbies;\damnlib;\KI5trailers;\91range;\93fordF350;\82porsche911;\90bmwE30;\91fordLTD;\89dodgeCaravan;\84jeepXJ;\63beetle;\76chevyKseries;\85chevyCaprice;\85pontiacParisienne;\92jeepYJ;\92jeepYJJP18;\87buickRegal;\isoContainers;\85buickLeSabre;\85oldsmobileDelta88;\93chevySuburban;\93chevySuburbanExpanded;\67commando;\90pierceArrow;\69camaro;\70barracuda;\70dodge;\86chevyCUCV;\81deloreanDMC12;\81deloreanDMC12BTTF;\92nissanGTR;\92amgeneralM998;\88toyotaHilux;\91geoMetro;\66pontiacLeMans;\67gt500;\49powerWagon;\69mini;\69mini_ItalianJob;\69mini_MrBean;\69mini_PitbullSpecial;\86fordE150;\86fordE150dnd;\86fordE150mm;\86fordE150pd;\86fordE150expanded;\89volvo200;\93fordElgin;\86oshkoshP19A;\92fordCVPI;\87chevySuburban;\68firebird;\77firebird;\82firebird;\82firebirdKITT;\04vwTouran;\90fordF350ambulance;\93mustangSSP;\87toyotaMR2;\73fordFalcon;\73fordFalconPS;\93townCar;\84merc;\91nissan240sx;\59meteor;\ECTO1;\87fordB700;\93fordTaurus;\75grandPrix;\89trooper;\63Type2Van;\99fordCVPI;\91fordRanger;\98stagea;\82jeepJ10;\82jeepJ10t;\88chevyS10;\89fordBronco;\83amgeneralM923;\78amgeneralM35A2;\78amgeneralM35A2extra;\78amgeneralM49A2C;\78amgeneralM50A3;\78amgeneralM62;\80manKat1;\65banshee;\89defender;\97bushmaster;\84cadillacDeVille;\84buickElectra;\84oldsmobile98;\85chevyStepVan;\85chevyStepVanexpanded;\VanillaFoodsExpanded;\Constown42;\Greenleaf B42 version;\42Grapeseed;\ATA_Jeep;\ATA_Jeep_x10;\ATA_Jeep_x2;\ATA_Jeep_x4;\ATA_Mustang;\ATA_Mustang_x2;\ATA_Mustang_x4;\autotsartrailers;\ATA_Bus;\tsarslib;\flipvehicleplustrailer;\PROJECTRVInterior42;\TombWardrobeALT;\TombWardrobeALTVanilla;\TombBody;\TombBodyCustom;\TombBodyTex;\TombBodyTexDOLL;\TombBodyTexNUDE;\SM4BootsExpandedB42;\SM4BootsExpandedFlatshoes;\GanydeBielovzki's Frockin Splendor!;\RandomClothing;\EFTBP;\AliceGear;\TableSaw;\Ahu'sToolWeapon42.13;\stanks_suicide;\STA_PryOpen;\AutoReload;\DBFaster50;\DBFaster60;\DBFaster70;\DBFaster80;\FixBlowTorchPropaneTank;\MiniHealthPanel;\P4HasBeenRead;\Project_Cook;\NeatUI_Framework;\ModernStatus;\CleanHotBar;\REORDER_THE_HOTBAR pihole_path: "{{ podman_volumes }}/pihole" sshpass_cron_path: "{{ podman_volumes }}/sshpass_cron" caddy_path: "{{ podman_volumes }}/caddy" @@ -43,6 +65,7 @@ cloud_server_name_io: cloud.debyl.io home_server_name_io: home.debyl.io parts_server_name_io: parts.debyl.io photos_server_name_io: photos.debyl.io +gitea_debyl_server_name: git.debyl.io # Legacy nginx/ModSecurity configuration removed - Caddy provides built-in security diff --git a/ansible/roles/podman/tasks/containers/base/awsddns.yml b/ansible/roles/podman/tasks/containers/base/awsddns.yml index 74dc68a..ebf4307 100644 --- a/ansible/roles/podman/tasks/containers/base/awsddns.yml +++ b/ansible/roles/podman/tasks/containers/base/awsddns.yml @@ -105,4 +105,6 @@ - name: create systemd startup job for awsddns-debyl include_tasks: podman/systemd-generate.yml vars: - container_name: awsddns-debyl \ No newline at end of file + container_name: awsddns-debyl + +# NOTE: git.debyl.io is an ALIAS record to home.debyl.io - no DDNS needed \ No newline at end of file diff --git a/ansible/roles/podman/tasks/containers/home/.zomboid.yml.kate-swp b/ansible/roles/podman/tasks/containers/home/.zomboid.yml.kate-swp new file mode 100644 index 0000000..e69de29 diff --git a/ansible/roles/podman/tasks/containers/home/zomboid.yml b/ansible/roles/podman/tasks/containers/home/zomboid.yml index e7a1b46..35c8f85 100644 --- a/ansible/roles/podman/tasks/containers/home/zomboid.yml +++ b/ansible/roles/podman/tasks/containers/home/zomboid.yml @@ -23,6 +23,26 @@ mode: 0755 notify: restorecon podman +- name: copy zomboid steamcmd install script + become: true + ansible.builtin.template: + src: zomboid/install.scmd.j2 + dest: "{{ zomboid_path }}/scripts/install.scmd" + owner: "{{ podman_subuid.stdout }}" + group: "{{ podman_user }}" + mode: 0644 + notify: restorecon podman + +# Set volume permissions for steam user (UID 1000) inside container +# This uses podman unshare to set ownership correctly for rootless podman +- name: set zomboid volume permissions for steam user + become: true + become_user: "{{ podman_user }}" + ansible.builtin.shell: | + podman unshare chown -R 1000:1000 {{ zomboid_path }}/server + podman unshare chown -R 1000:1000 {{ zomboid_path }}/data + changed_when: false + - name: flush handlers ansible.builtin.meta: flush_handlers @@ -40,19 +60,23 @@ restart_policy: on-failure:3 log_driver: journald env: - SERVER_NAME: zomboid + SERVER_NAME: "{{ zomboid_server_names[zomboid_server_mode] }}" MIN_RAM: 8g MAX_RAM: 24g AUTO_UPDATE: "true" ADMIN_PASSWORD: "{{ zomboid_admin_password }}" SERVER_PASSWORD: "{{ zomboid_password }}" + PUID: "1000" + PGID: "1000" volumes: - - "{{ zomboid_path }}/server:/home/steam/pzserver" - - "{{ zomboid_path }}/data:/home/steam/Zomboid" + - "{{ zomboid_path }}/server:/project-zomboid" + - "{{ zomboid_path }}/data:/project-zomboid-config" - "{{ zomboid_path }}/scripts/entrypoint.sh:/entrypoint.sh:ro" + - "{{ zomboid_path }}/scripts/install.scmd:/home/steam/install.scmd:ro" ports: - "16261:16261/udp" - "16262:16262/udp" + - "{{ zomboid_rcon_port }}:{{ zomboid_rcon_port }}/tcp" command: /bin/bash /entrypoint.sh - name: create systemd startup job for zomboid @@ -70,17 +94,73 @@ line: "Restart=always" notify: reload zomboid systemd +# Check if server INI exists (generated on first server run) +- name: check if zomboid server ini exists + become: true + ansible.builtin.stat: + path: "{{ zomboid_path }}/data/Server/{{ zomboid_server_names[zomboid_server_mode] }}.ini" + register: zomboid_ini_stat + tags: zomboid-conf + # Backup settings (requires server to have run once to generate ini) - name: configure zomboid backup settings become: true ansible.builtin.lineinfile: - path: "{{ zomboid_path }}/data/Server/zomboid.ini" + path: "{{ zomboid_path }}/data/Server/{{ zomboid_server_names[zomboid_server_mode] }}.ini" regexp: "^{{ item.key }}=" line: "{{ item.key }}={{ item.value }}" loop: - { key: "SaveWorldEveryMinutes", value: "10" } - { key: "BackupsPeriod", value: "30" } - { key: "BackupsCount", value: "10" } + # B42 Linux server fix: disable Lua checksum to allow mods to load + - { key: "DoLuaChecksum", value: "false" } + # Server password + - { key: "Password", value: "{{ zomboid_password }}" } + when: zomboid_ini_stat.stat.exists + tags: zomboid-conf + +# Discord integration (uses Gregbot token, posts /all chat to Discord) +- name: configure zomboid discord integration + become: true + ansible.builtin.lineinfile: + path: "{{ zomboid_path }}/data/Server/{{ zomboid_server_names[zomboid_server_mode] }}.ini" + regexp: "^{{ item.key }}=" + line: "{{ item.key }}={{ item.value }}" + loop: + - { key: "DiscordEnable", value: "true" } + - { key: "DiscordToken", value: "{{ zomboid_discord_token }}" } + - { key: "DiscordChannel", value: "zomboid" } + - { key: "DiscordChannelID", value: "{{ zomboid_discord_channel_id }}" } + when: zomboid_ini_stat.stat.exists + tags: zomboid-conf + +# RCON configuration for remote administration +- name: configure zomboid rcon + become: true + ansible.builtin.lineinfile: + path: "{{ zomboid_path }}/data/Server/{{ zomboid_server_names[zomboid_server_mode] }}.ini" + regexp: "^{{ item.key }}=" + line: "{{ item.key }}={{ item.value }}" + loop: + - { key: "RCONPort", value: "{{ zomboid_rcon_port }}" } + - { key: "RCONPassword", value: "{{ zomboid_admin_password }}" } + when: zomboid_ini_stat.stat.exists + tags: zomboid-conf + +# Mod configuration (only for modded server profile) +- name: configure zomboid mods for modded server + become: true + ansible.builtin.lineinfile: + path: "{{ zomboid_path }}/data/Server/{{ zomboid_server_names[zomboid_server_mode] }}.ini" + regexp: "^{{ item.key }}=" + line: "{{ item.key }}={{ item.value }}" + loop: + - { key: "Mods", value: "{{ zomboid_mods.mod_ids }}" } + - { key: "WorkshopItems", value: "{{ zomboid_mods.workshop_items }}" } + when: + - zomboid_server_mode == 'modded' + - zomboid_ini_stat.stat.exists tags: zomboid-conf # World reset tasks REMOVED - too dangerous to have in automation diff --git a/ansible/roles/podman/tasks/main.yml b/ansible/roles/podman/tasks/main.yml index ce7b45b..10391d7 100644 --- a/ansible/roles/podman/tasks/main.yml +++ b/ansible/roles/podman/tasks/main.yml @@ -54,9 +54,9 @@ - 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.3.1 + ml_image: ghcr.io/immich-app/immich-machine-learning:v2.4.0 redis_image: docker.io/redis:6.2-alpine@sha256:eaba718fecd1196d88533de7ba49bf903ad33664a92debb24660a922ecd9cac8 - image: ghcr.io/immich-app/immich-server:v2.3.1 + image: ghcr.io/immich-app/immich-server:v2.4.0 tags: photos - import_tasks: containers/home/cloud.yml diff --git a/ansible/roles/podman/templates/caddy/Caddyfile.j2 b/ansible/roles/podman/templates/caddy/Caddyfile.j2 index b216960..2040757 100644 --- a/ansible/roles/podman/templates/caddy/Caddyfile.j2 +++ b/ansible/roles/podman/templates/caddy/Caddyfile.j2 @@ -243,6 +243,20 @@ } } +# Gitea - {{ gitea_debyl_server_name }} +{{ gitea_debyl_server_name }} { + import common_headers + + reverse_proxy localhost:3100 { + flush_interval -1 + } + + log { + output file /var/log/caddy/gitea-debyl.log + format json + } +} + # Fulfillr - {{ fulfillr_server_name }} (Static + API with IP restrictions) {{ fulfillr_server_name }} { {{ ip_restricted_site() }} diff --git a/ansible/roles/podman/templates/zomboid/entrypoint.sh.j2 b/ansible/roles/podman/templates/zomboid/entrypoint.sh.j2 index 6330790..be78b62 100644 --- a/ansible/roles/podman/templates/zomboid/entrypoint.sh.j2 +++ b/ansible/roles/podman/templates/zomboid/entrypoint.sh.j2 @@ -1,103 +1,89 @@ #!/bin/bash +# Project Zomboid Build 42 Server Entrypoint +# Based on IndifferentBroccoli/projectzomboid-server-docker set -e +# Configuration +INSTALL_DIR="/project-zomboid" +CONFIG_DIR="/project-zomboid-config" STEAMCMD="/home/steam/steamcmd/steamcmd.sh" -INSTALL_DIR="/home/steam/pzserver" -DATA_DIR="/home/steam/Zomboid" SERVER_NAME="${SERVER_NAME:-zomboid}" -MIN_RAM="${MIN_RAM:-8g}" -MAX_RAM="${MAX_RAM:-24g}" +PUID="${PUID:-1000}" +PGID="${PGID:-1000}" +MIN_RAM="${MIN_RAM:-4g}" +MAX_RAM="${MAX_RAM:-8g}" echo "=== Project Zomboid Build 42 Server ===" echo "Server Name: ${SERVER_NAME}" echo "RAM: ${MIN_RAM} - ${MAX_RAM}" -# Fix ownership of mounted volumes (container runs as steam user, UID 1000) -echo "=== Fixing volume permissions ===" -chown -R steam:steam "${INSTALL_DIR}" || true -chown -R steam:steam "${DATA_DIR}" || true -chmod -R 755 "${INSTALL_DIR}" || true -chmod -R 755 "${DATA_DIR}" || true +# Set user permissions (IndifferentBroccoli approach) +echo "=== Setting file permissions ===" +usermod -o -u "${PUID}" steam +groupmod -o -g "${PGID}" steam +chown -R steam:steam "${INSTALL_DIR}" "${CONFIG_DIR}" +# Only chown writable parts of /home/steam (not read-only mounts) +chown steam:steam /home/steam +chown -R steam:steam /home/steam/steamcmd 2>/dev/null || true +chown -R steam:steam /home/steam/Steam 2>/dev/null || true -# Create required subdirectories with correct ownership -mkdir -p "${DATA_DIR}/Server" -mkdir -p "${DATA_DIR}/Saves/Multiplayer" -mkdir -p "${DATA_DIR}/db" -chown -R steam:steam "${DATA_DIR}" - -# Ensure steam user has proper home directory setup -export HOME=/home/steam - -# Initialize SteamCMD if needed (creates config directories) -if [ ! -d "/home/steam/Steam" ]; then - echo "=== Initializing SteamCMD ===" - su -c "${STEAMCMD} +quit" steam || true -fi +# Create required directories +mkdir -p "${CONFIG_DIR}/Server" +mkdir -p "${CONFIG_DIR}/Saves" +mkdir -p "${CONFIG_DIR}/db" # Update/Install PZ dedicated server with Build 42 unstable branch if [ "${AUTO_UPDATE:-true}" = "true" ]; then echo "=== Updating Project Zomboid Server (Build 42 unstable) ===" - # Run steamcmd as steam user with proper quoting for beta flag - su -c "${STEAMCMD} +force_install_dir ${INSTALL_DIR} +login anonymous +app_update 380870 -beta unstable validate +quit" steam + su -c "${STEAMCMD} +runscript /home/steam/install.scmd" steam echo "=== Update complete ===" fi -# Ensure data directories exist (created earlier with correct permissions) +# Configure JVM memory settings in ProjectZomboid64.json (Build 42 uses JSON config) +configure_memory() { + local json_file="${INSTALL_DIR}/ProjectZomboid64.json" -# Configure server settings on first run -SERVER_INI="${DATA_DIR}/Server/${SERVER_NAME}.ini" -if [ ! -f "${SERVER_INI}" ]; then - echo "=== First run detected, server will generate default config ===" -fi + if [ ! -f "$json_file" ]; then + echo "=== ProjectZomboid64.json not found, skipping memory config ===" + return 0 + fi -# Handle admin password for first run -# PZ requires interactive password input on first run, so we create a db file -ADMIN_DB="${DATA_DIR}/db/${SERVER_NAME}.db" -if [ ! -f "${ADMIN_DB}" ] && [ -n "${ADMIN_PASSWORD}" ]; then - echo "=== Setting up admin account ===" - mkdir -p "${DATA_DIR}/db" - # The server will prompt for password on first run - # We'll use expect-like behavior or let it use defaults -fi + echo "=== Configuring JVM memory: Xms=${MIN_RAM}, Xmx=${MAX_RAM} ===" -# Modify memory settings in ProjectZomboid64.json (Build 42 uses JSON config) -PZ_JSON="${INSTALL_DIR}/ProjectZomboid64.json" -if [ -f "${PZ_JSON}" ]; then - echo "=== Setting JVM memory: Xms=${MIN_RAM}, Xmx=${MAX_RAM} ===" - # Add -Xms if not present, otherwise update it - if grep -q "\-Xms" "${PZ_JSON}"; then - sed -i "s/-Xms[0-9]*[gGmM]*/-Xms${MIN_RAM}/g" "${PZ_JSON}" + # Update Xmx + sed -i "s/-Xmx[0-9]*[gGmM]*/-Xmx${MAX_RAM}/g" "$json_file" + + # Update or add Xms + if grep -q "\-Xms" "$json_file"; then + sed -i "s/-Xms[0-9]*[gGmM]*/-Xms${MIN_RAM}/g" "$json_file" else # Insert -Xms before -Xmx - sed -i "s/\"-Xmx/\"-Xms${MIN_RAM}\",\n\t\t\"-Xmx/g" "${PZ_JSON}" + sed -i "s/\"-Xmx/\"-Xms${MIN_RAM}\",\n\t\t\"-Xmx/g" "$json_file" fi - sed -i "s/-Xmx[0-9]*[gGmM]*/-Xmx${MAX_RAM}/g" "${PZ_JSON}" + + echo "=== Memory configuration complete ===" +} +configure_memory + +# Check if first run (no admin DB) +ADMIN_DB="${CONFIG_DIR}/db/${SERVER_NAME}.db" + +# Build server arguments +# Note: -modfolders is NOT used - mods are configured via INI only +# Reference: IndifferentBroccoli/projectzomboid-server-docker +SERVER_ARGS="-cachedir=${CONFIG_DIR} -servername ${SERVER_NAME}" + +# Add admin password for first run +if [ ! -f "${ADMIN_DB}" ] && [ -n "${ADMIN_PASSWORD}" ]; then + echo "=== First run: setting admin password ===" + SERVER_ARGS="${SERVER_ARGS} -adminpassword ${ADMIN_PASSWORD}" fi -# If server password is set, we'll need to configure it in the ini after first run -# For now, store it for later configuration -if [ -n "${SERVER_PASSWORD}" ]; then - echo "${SERVER_PASSWORD}" > "${DATA_DIR}/.server_password" -fi +# Note: Server password is set via INI file, not command line args -if [ -n "${ADMIN_PASSWORD}" ]; then - echo "${ADMIN_PASSWORD}" > "${DATA_DIR}/.admin_password" -fi - -# Change to install directory and start server +# Start server cd "${INSTALL_DIR}" - echo "=== Starting Project Zomboid Server ===" echo "Connect to: home.bdebyl.net:16261" -# Start server - on first run this will prompt for admin password -# We handle this by providing input via stdin if password file exists -if [ -f "${DATA_DIR}/.admin_password" ] && [ ! -f "${ADMIN_DB}" ]; then - # First run with admin password - ADMIN_PASS=$(cat "${DATA_DIR}/.admin_password") - echo "=== First run: setting admin password ===" - printf "%s\n%s\n" "${ADMIN_PASS}" "${ADMIN_PASS}" | su -c "bash start-server.sh -servername ${SERVER_NAME}" steam -else - # Normal run - exec su -c "bash start-server.sh -servername ${SERVER_NAME}" steam -fi +exec su -c "export LD_LIBRARY_PATH=${INSTALL_DIR}/jre64/lib:\${LD_LIBRARY_PATH} && ./start-server.sh ${SERVER_ARGS}" steam diff --git a/ansible/roles/podman/templates/zomboid/install.scmd.j2 b/ansible/roles/podman/templates/zomboid/install.scmd.j2 new file mode 100644 index 0000000..3855ebd --- /dev/null +++ b/ansible/roles/podman/templates/zomboid/install.scmd.j2 @@ -0,0 +1,18 @@ +// SteamCMD script for Project Zomboid Server installation +// Based on IndifferentBroccoli/projectzomboid-server-docker + +// Do not shutdown on a failed command +@ShutdownOnFailedCommand 0 + +// No password prompt as this is unattended +@NoPromptForPassword 1 + +// Set the game installation directory +force_install_dir /project-zomboid + +login anonymous + +// Install/Update the Project Zomboid Dedicated Server - Unstable Branch (Build 42) +app_update 380870 -beta unstable validate + +quit diff --git a/ansible/vars/vault.yml b/ansible/vars/vault.yml index be73d20341cc566e4e0c928cab5ebb1f2b4c33bb..e3a714c62a2017500f07b74abce683c5f98a8281 100644 GIT binary patch literal 14697 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