initial opencode commit
This commit is contained in:
10
.omo/run-continuation/ses_f68dcefb0ffeMmH0HMoYkd9jKa.json
Normal file
10
.omo/run-continuation/ses_f68dcefb0ffeMmH0HMoYkd9jKa.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"sessionID": "ses_f68dcefb0ffeMmH0HMoYkd9jKa",
|
||||||
|
"updatedAt": "2026-09-12T20:30:25.640Z",
|
||||||
|
"sources": {
|
||||||
|
"background-task": {
|
||||||
|
"state": "idle",
|
||||||
|
"updatedAt": "2026-09-12T20:30:25.640Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 external-minecraft contributors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
146
README.md
Normal file
146
README.md
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
# Hardened Fedora Minecraft Server (Ansible)
|
||||||
|
|
||||||
|
Ansible playbook that turns a fresh **Fedora 42** server into a hardened,
|
||||||
|
containerized **Minecraft** game server — Java *and* Bedrock clients —
|
||||||
|
running with **rootless Podman** via systemd Quadlet units.
|
||||||
|
|
||||||
|
Everything is managed declaratively from `harden_fedora_minecraft.yml`:
|
||||||
|
OS hardening, firewall, SELinux, SSH access control, the Podman runtime,
|
||||||
|
and the Minecraft/Geyser containers are all deployed by one command.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 🔒 **[Hardening]** SELinux enforcing, firewalld default-DROP policy, password
|
||||||
|
policy (pwquality + aging), root account locked, automatic security updates
|
||||||
|
(`dnf-automatic`).
|
||||||
|
- 🔑 **[SSH]** Key-only authentication for a dedicated admin user, root login
|
||||||
|
disabled, `AllowUsers` restriction, and a `fail2ban` SSH jail.
|
||||||
|
- 🐳 **[Runtime]** Rootless Podman with user lingering, subuid/subgid ranges,
|
||||||
|
IPv4 forwarding, and the overlay storage driver.
|
||||||
|
- ⛏️ **[Minecraft]** Portable Java server built from
|
||||||
|
[`itzg/docker-minecraft-server`](https://github.com/itzg/docker-minecraft-server)
|
||||||
|
(PAPER by default, version/memory/difficulty configurable).
|
||||||
|
- 🪨 **[Geyser]** A second container, [`geysermc/geyser`](https://github.com/GeyserMC/geyser-docker),
|
||||||
|
in the same pod so **Bedrock** (phone/console) players can join the Java world.
|
||||||
|
- 🗄️ **[Data]** World data and backups live on the host in `/srv/minecraft`
|
||||||
|
with correct SELinux labels.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
Both game containers run in a single Podman **pod** (shared network
|
||||||
|
namespace) as the non-root user `mcadmin`:
|
||||||
|
|
||||||
|
```
|
||||||
|
Internet
|
||||||
|
│
|
||||||
|
┌───────┴────────┐
|
||||||
|
│ firewall (DROP)│ only the ports below are allowed
|
||||||
|
└───────┬────────┘
|
||||||
|
│
|
||||||
|
pod: minecraft (rootless, user mcadmin)
|
||||||
|
├── minecraft-server JVM on 0.0.0.0:25565 (Java clients)
|
||||||
|
│ └── world data → /srv/minecraft/data
|
||||||
|
└── geyser Bedrock listener :19132/udp
|
||||||
|
└── config → /srv/minecraft/geyser
|
||||||
|
└── forwards Bedrock players to 127.0.0.1:25565
|
||||||
|
```
|
||||||
|
|
||||||
|
| Port | Protocol | Purpose |
|
||||||
|
|------|----------|---------|
|
||||||
|
| `22` | TCP | SSH (configurable via `ssh_port`) |
|
||||||
|
| `25565` | TCP | Minecraft Java server |
|
||||||
|
| `25565` | UDP | Optional Java server UDP (game query) |
|
||||||
|
| `19132` | UDP | Bedrock (Geyser), when `enable_geyser: true` |
|
||||||
|
|
||||||
|
## Repository layout
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── harden_fedora_minecraft.yml # entry point: hosts: external-apps → 7 roles
|
||||||
|
├── inventory.yml # target host definition
|
||||||
|
├── ansible.cfg # inventory + role path + pipelining
|
||||||
|
├── requirements.yml # Ansible collections (ansible.posix)
|
||||||
|
├── group_vars/all.yml # shared variables
|
||||||
|
├── host_vars/external-apps.yml # per-server settings (IP, SSH key, MC tuning)
|
||||||
|
└── roles/
|
||||||
|
├── packages/ # base + container tooling, full dnf update, dnf-automatic
|
||||||
|
├── firewall/ # firewalld: DROP target, open SSH + Minecraft + Geyser
|
||||||
|
├── selinux/ # enforcing mode + container_file_t context for /srv/minecraft
|
||||||
|
├── auth/ # admin user, SSH key, sudoers, password policy, lock root
|
||||||
|
├── ssh_harden/ # sshd drop-in + fail2ban jail
|
||||||
|
├── podman/ # socket, linger, subuid/subgid, ip_forward, storage
|
||||||
|
└── minecraft/ # data dirs, Quadlet pod/container units, image pulls
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Ansible control node (this repo) with Ansible Core ≥ 2.14 and `ansible.posix`
|
||||||
|
- A Fedora 42 target server reachable over SSH with an account that has sudo
|
||||||
|
- The `ansible.user` in the inventory must be able to `become` root on the target
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Install the required collection
|
||||||
|
ansible-galaxy collection install -r requirements.yml
|
||||||
|
|
||||||
|
# 2. Configure the target server (REQUIRED before first run)
|
||||||
|
# - host_vars/external-apps.yml
|
||||||
|
# * ansible_host: real IP or DNS name
|
||||||
|
# * admin_user_ssh_key: your real SSH public key
|
||||||
|
# * game settings to taste (version, memory, whitelist…)
|
||||||
|
|
||||||
|
# 3. Run the playbook
|
||||||
|
ansible-playbook harden_fedora_minecraft.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
> ⚠️ `admin_user_ssh_key` **must** be a real key before the first run —
|
||||||
|
> the playbook disables SSH password authentication and root login once
|
||||||
|
> the `ssh_harden` role executes.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
All tunables live in `host_vars/external-apps.yml` (per server) and
|
||||||
|
`group_vars/all.yml` (cluster-wide). Highlights:
|
||||||
|
|
||||||
|
| Variable | Default | Meaning |
|
||||||
|
|----------|---------|---------|
|
||||||
|
| `admin_user` | `mcadmin` | Non-root user running the containers |
|
||||||
|
| `minecraft_version` | `LATEST` | Server version (`1.21.1`, …) |
|
||||||
|
| `minecraft_server_type` | `PAPER` | PAPER / VANILLA / FABRIC / FORGE |
|
||||||
|
| `minecraft_memory` | `2G` | JVM memory limit |
|
||||||
|
| `minecraft_whitelist` | `""` | Comma-separated allowed usernames (empty = off) |
|
||||||
|
| `enable_geyser` | `true` | Deploy the Bedrock bridge |
|
||||||
|
| `geyser_port` | `19132` | Bedrock UDP port |
|
||||||
|
| `geyser_auth_type` | `online` | `offline` only with a paired Floodgate key |
|
||||||
|
| `ssh_port` | `22` | SSH port used by firewall + sshd |
|
||||||
|
|
||||||
|
## Operations
|
||||||
|
|
||||||
|
After the playbook has run, manage the servers as `mcadmin`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo machinectl shell mcadmin@ -- systemctl --user status minecraft-server geyser
|
||||||
|
sudo machinectl shell mcadmin@ -- systemctl --user restart minecraft-server
|
||||||
|
sudo machinectl shell mcadmin@ -- podman ps # container list
|
||||||
|
sudo machinectl shell mcadmin@ -- journalctl --user-unit minecraft-server -f
|
||||||
|
```
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Ansible Documentation](https://docs.ansible.com/)
|
||||||
|
- [Fedora Project](https://fedoraproject.org/)
|
||||||
|
- [Podman](https://podman.io/)
|
||||||
|
- [Podman Quadlet (systemd.unit) man page](https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html)
|
||||||
|
- [Rootless Podman tutorial](https://github.com/containers/podman/blob/main/docs/tutorials/rootless_tutorial.md)
|
||||||
|
- [itzg/docker-minecraft-server](https://github.com/itzg/docker-minecraft-server)
|
||||||
|
- [GeyserMC — Bedrock ↔ Java bridge](https://geysermc.dev/)
|
||||||
|
- [GeyserMC/Geyser](https://github.com/GeyserMC/Geyser)
|
||||||
|
- [Geyser docker image](https://github.com/GeyserMC/geyser-docker)
|
||||||
|
- [Firewalld](https://firewalld.org/)
|
||||||
|
- [fail2ban](https://github.com/fail2ban/fail2ban)
|
||||||
|
- [SELinux quick docs (Fedora)](https://docs.fedoraproject.org/en-US/quick-docs/selinux-getting-started/)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT](./LICENSE)
|
||||||
10
ansible.cfg
Normal file
10
ansible.cfg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[defaults]
|
||||||
|
inventory = inventory.yml
|
||||||
|
roles_path = roles
|
||||||
|
retry_files_enabled = False
|
||||||
|
interpreter_python = auto_silent
|
||||||
|
stdout_callback = yaml
|
||||||
|
gathering = smart
|
||||||
|
|
||||||
|
[ssh_connection]
|
||||||
|
pipelining = True
|
||||||
31
group_vars/all.yml
Normal file
31
group_vars/all.yml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Variables shared by every host in the inventory.
|
||||||
|
# Role defaults live in roles/<role>/defaults/main.yml and are overridden here.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Administrative non-root user created by the auth role.
|
||||||
|
# This is the account the Minecraft containers run under (rootless Podman).
|
||||||
|
admin_user: mcadmin
|
||||||
|
|
||||||
|
# SSH public key installed for {{ admin_user }}. MUST be set before first run
|
||||||
|
# or you will lock yourself out once ssh_harden disables password auth.
|
||||||
|
# Put your real key in host_vars/external-apps.yml.
|
||||||
|
admin_user_ssh_key: ""
|
||||||
|
|
||||||
|
# SSH daemon port (firewall + ssh_harden will both use this).
|
||||||
|
ssh_port: 22
|
||||||
|
|
||||||
|
# Minecraft Java server port exposed to the internet.
|
||||||
|
minecraft_port: 25565
|
||||||
|
minecraft_udp: false
|
||||||
|
|
||||||
|
# Wher the Minecraft world/config data lives on the host.
|
||||||
|
minecraft_data_dir: /srv/minecraft
|
||||||
|
|
||||||
|
# Geyser - Bedrock protocol bridge. Opens the Bedrock UDP port in the
|
||||||
|
# firewall (roles/firewall) and deploys the geyser.container Quadlet unit
|
||||||
|
# (roles/minecraft) next to the Java server.
|
||||||
|
enable_geyser: true
|
||||||
|
# Bedrock UDP port Geyser publishes (host + container, must match).
|
||||||
|
geyser_port: 19132
|
||||||
12
harden_fedora_minecraft.yml
Normal file
12
harden_fedora_minecraft.yml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
- name: Harden Fedora and deploy Minecraft server
|
||||||
|
hosts: external-apps
|
||||||
|
become: true
|
||||||
|
roles:
|
||||||
|
- packages
|
||||||
|
- firewall
|
||||||
|
- selinux
|
||||||
|
- auth
|
||||||
|
- ssh_harden
|
||||||
|
- podman
|
||||||
|
- minecraft
|
||||||
32
host_vars/external-apps.yml
Normal file
32
host_vars/external-apps.yml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
# Host-specific variables for the external-apps server.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 1. Set ansible_host to the real public IP or DNS name.
|
||||||
|
# 2. Put your real SSH public key in admin_user_ssh_key.
|
||||||
|
# 3. Adjust Minecraft / Geyser settings as needed.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# The server's real address (overrides the placeholder in inventory.yml).
|
||||||
|
ansible_host: 203.0.113.10
|
||||||
|
|
||||||
|
# Your SSH public key. This is what lets you log in after the playbook
|
||||||
|
# disables password authentication. Replace the placeholder below.
|
||||||
|
admin_user_ssh_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... your-public-key-here"
|
||||||
|
|
||||||
|
# Minecraft server tuning (see roles/minecraft/defaults/main.yml for all options).
|
||||||
|
minecraft_memory: 2G # RAM allocated to the JVM
|
||||||
|
minecraft_version: LATEST # or a specific version, e.g. "1.21.1"
|
||||||
|
minecraft_server_type: PAPER # PAPER | VANILLA | FABRIC | FORGE | ...
|
||||||
|
minecraft_difficulty: normal
|
||||||
|
minecraft_max_players: "20"
|
||||||
|
minecraft_view_distance: "10"
|
||||||
|
minecraft_motd: "Hardened Fedora 42 Minecraft Server"
|
||||||
|
|
||||||
|
# Comma-separated list of Minecraft usernames allowed to join.
|
||||||
|
# Empty string means the whitelist is disabled.
|
||||||
|
minecraft_whitelist: ""
|
||||||
|
|
||||||
|
# Geyser - Bedrock protocol bridge (bedrock clients join the Java world).
|
||||||
|
# enable_geyser: true # set to false to skip the Geyser container
|
||||||
|
# geyser_port: 19132 # Bedrock UDP port (firewall + container)
|
||||||
|
# geyser_auth_type: online # online | offline (offline requires Floodgate)
|
||||||
12
inventory.yml
Normal file
12
inventory.yml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
all:
|
||||||
|
children:
|
||||||
|
minecraft_servers:
|
||||||
|
hosts:
|
||||||
|
external-apps:
|
||||||
|
# Replace with the server's real IP address or DNS name.
|
||||||
|
ansible_host: 203.0.113.10
|
||||||
|
# Initial connection user (must have sudo on first run).
|
||||||
|
# After the playbook creates the admin user, switch to that user.
|
||||||
|
ansible_user: root
|
||||||
|
ansible_become: true
|
||||||
3
requirements.yml
Normal file
3
requirements.yml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
collections:
|
||||||
|
- ansible.posix
|
||||||
14
roles/auth/defaults/main.yml
Normal file
14
roles/auth/defaults/main.yml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
admin_user: mcadmin
|
||||||
|
admin_user_ssh_key: ""
|
||||||
|
admin_user_password_hash: "" # empty / unset keeps the account password-locked
|
||||||
|
|
||||||
|
auth_minlen: 14
|
||||||
|
auth_dcredit: -1
|
||||||
|
auth_ucredit: -1
|
||||||
|
auth_lcredit: -1
|
||||||
|
auth_ocredit: -1
|
||||||
|
auth_password_max_days: 90
|
||||||
|
auth_password_min_days: 7
|
||||||
|
auth_password_warn_age: 7
|
||||||
|
auth_lock_root_password: true
|
||||||
70
roles/auth/tasks/main.yml
Normal file
70
roles/auth/tasks/main.yml
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
---
|
||||||
|
# Create the administrative user, install its SSH key, enforce password
|
||||||
|
# policy, and lock the root account.
|
||||||
|
|
||||||
|
- name: Create the administrative user
|
||||||
|
ansible.builtin.user:
|
||||||
|
name: "{{ admin_user }}"
|
||||||
|
groups: wheel
|
||||||
|
append: true
|
||||||
|
create_home: true
|
||||||
|
home: "/home/{{ admin_user }}"
|
||||||
|
shell: /bin/bash
|
||||||
|
comment: "Minecraft server administrator"
|
||||||
|
password: "{{ admin_user_password_hash | default('!') }}"
|
||||||
|
password_expire_max: "{{ auth_password_max_days }}"
|
||||||
|
password_expire_min: "{{ auth_password_min_days }}"
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Install the admin user's SSH public key
|
||||||
|
ansible.builtin.authorized_key:
|
||||||
|
user: "{{ admin_user }}"
|
||||||
|
state: present
|
||||||
|
key: "{{ admin_user_ssh_key }}"
|
||||||
|
when: admin_user_ssh_key | length > 0
|
||||||
|
|
||||||
|
- name: Allow the admin user passwordless sudo
|
||||||
|
ansible.builtin.copy:
|
||||||
|
content: "{{ admin_user }} ALL=(ALL) NOPASSWD: ALL\n"
|
||||||
|
dest: "/etc/sudoers.d/{{ admin_user }}"
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0440"
|
||||||
|
validate: /usr/sbin/visudo -cf %s
|
||||||
|
|
||||||
|
- name: Enforce password complexity settings
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
path: /etc/security/pwquality.conf
|
||||||
|
regexp: '^#?\s*{{ item.key }}\s*='
|
||||||
|
line: "{{ item.key }} = {{ item.value }}"
|
||||||
|
loop:
|
||||||
|
- { key: minlen, value: "{{ auth_minlen }}" }
|
||||||
|
- { key: dcredit, value: "{{ auth_dcredit }}" }
|
||||||
|
- { key: ucredit, value: "{{ auth_ucredit }}" }
|
||||||
|
- { key: lcredit, value: "{{ auth_lcredit }}" }
|
||||||
|
- { key: ocredit, value: "{{ auth_ocredit }}" }
|
||||||
|
|
||||||
|
- name: Enforce global password aging defaults
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
path: /etc/login.defs
|
||||||
|
regexp: '^PASS_MAX_DAYS\s+'
|
||||||
|
line: "PASS_MAX_DAYS {{ auth_password_max_days }}"
|
||||||
|
|
||||||
|
- name: Enforce minimum days between password changes
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
path: /etc/login.defs
|
||||||
|
regexp: '^PASS_MIN_DAYS\s+'
|
||||||
|
line: "PASS_MIN_DAYS {{ auth_password_min_days }}"
|
||||||
|
|
||||||
|
- name: Enforce password change warning
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
path: /etc/login.defs
|
||||||
|
regexp: '^PASS_WARN_AGE\s+'
|
||||||
|
line: "PASS_WARN_AGE {{ auth_password_warn_age }}"
|
||||||
|
|
||||||
|
- name: Lock the root account's password
|
||||||
|
ansible.builtin.user:
|
||||||
|
name: root
|
||||||
|
password: '!'
|
||||||
|
state: present
|
||||||
|
when: auth_lock_root_password | bool
|
||||||
6
roles/firewall/defaults/main.yml
Normal file
6
roles/firewall/defaults/main.yml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
firewall_zone: public
|
||||||
|
firewall_zone_target: DROP
|
||||||
|
firewall_allow_ping: true
|
||||||
|
enable_geyser: true
|
||||||
|
geyser_port: 19132
|
||||||
68
roles/firewall/tasks/main.yml
Normal file
68
roles/firewall/tasks/main.yml
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
---
|
||||||
|
# Firewall policy: default DROP, only SSH and the Minecraft port are exposed.
|
||||||
|
|
||||||
|
- name: Ensure firewalld is running and enabled
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: firewalld
|
||||||
|
enabled: true
|
||||||
|
state: started
|
||||||
|
|
||||||
|
- name: Set the default zone target to DROP
|
||||||
|
ansible.posix.firewalld:
|
||||||
|
zone: "{{ firewall_zone }}"
|
||||||
|
target: "{{ firewall_zone_target }}"
|
||||||
|
permanent: true
|
||||||
|
immediate: true
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Allow SSH service
|
||||||
|
ansible.posix.firewalld:
|
||||||
|
zone: "{{ firewall_zone }}"
|
||||||
|
service: ssh
|
||||||
|
permanent: true
|
||||||
|
immediate: true
|
||||||
|
state: enabled
|
||||||
|
|
||||||
|
- name: Allow custom SSH port
|
||||||
|
ansible.posix.firewalld:
|
||||||
|
zone: "{{ firewall_zone }}"
|
||||||
|
port: "{{ ssh_port }}/tcp"
|
||||||
|
permanent: true
|
||||||
|
immediate: true
|
||||||
|
state: enabled
|
||||||
|
when: ssh_port != 22
|
||||||
|
|
||||||
|
- name: Allow Minecraft server port (TCP)
|
||||||
|
ansible.posix.firewalld:
|
||||||
|
zone: "{{ firewall_zone }}"
|
||||||
|
port: "{{ minecraft_port }}/tcp"
|
||||||
|
permanent: true
|
||||||
|
immediate: true
|
||||||
|
state: enabled
|
||||||
|
|
||||||
|
- name: Allow Minecraft server port (UDP)
|
||||||
|
ansible.posix.firewalld:
|
||||||
|
zone: "{{ firewall_zone }}"
|
||||||
|
port: "{{ minecraft_port }}/udp"
|
||||||
|
permanent: true
|
||||||
|
immediate: true
|
||||||
|
state: enabled
|
||||||
|
when: minecraft_udp | bool
|
||||||
|
|
||||||
|
- name: Allow Geyser Bedrock port (UDP)
|
||||||
|
ansible.posix.firewalld:
|
||||||
|
zone: "{{ firewall_zone }}"
|
||||||
|
port: "{{ geyser_port }}/udp"
|
||||||
|
permanent: true
|
||||||
|
immediate: true
|
||||||
|
state: enabled
|
||||||
|
when: enable_geyser | bool
|
||||||
|
|
||||||
|
- name: Allow ICMP echo (ping) for basic connectivity checks
|
||||||
|
ansible.posix.firewalld:
|
||||||
|
zone: "{{ firewall_zone }}"
|
||||||
|
icmp_block_inversion: true
|
||||||
|
permanent: true
|
||||||
|
immediate: true
|
||||||
|
state: present
|
||||||
|
when: firewall_allow_ping | bool
|
||||||
31
roles/minecraft/defaults/main.yml
Normal file
31
roles/minecraft/defaults/main.yml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
minecraft_data_dir: /srv/minecraft
|
||||||
|
minecraft_image: docker.io/itzg/minecraft-server:latest
|
||||||
|
minecraft_server_type: PAPER
|
||||||
|
minecraft_version: LATEST
|
||||||
|
minecraft_memory: 2G
|
||||||
|
minecraft_difficulty: normal
|
||||||
|
minecraft_motd: "Hardened Fedora 42 Minecraft Server"
|
||||||
|
minecraft_max_players: "20"
|
||||||
|
minecraft_view_distance: "10"
|
||||||
|
minecraft_online_mode: "TRUE"
|
||||||
|
minecraft_whitelist: ""
|
||||||
|
# Port the Java server listens on INSIDE the container (host port
|
||||||
|
# publish is controlled by minecraft_port / minecraft_udp).
|
||||||
|
minecraft_container_port: 25565
|
||||||
|
minecraft_prepull: true
|
||||||
|
|
||||||
|
# Geyser - Bedrock protocol bridge (geysermc/geyser). Runs in the same pod
|
||||||
|
# as the Java server and publishes the Bedrock UDP port so Bedrock clients
|
||||||
|
# can join the Java world.
|
||||||
|
enable_geyser: true
|
||||||
|
geyser_image: docker.io/geysermc/geyser:latest
|
||||||
|
# Bedrock UDP port published on the host AND listened on by Geyser inside
|
||||||
|
# the container (pod-level publish, must match the firewall rule).
|
||||||
|
geyser_port: 19132
|
||||||
|
# Inside the pod all containers share localhost, so Geyser talks to the Java
|
||||||
|
# server over 127.0.0.1:{{ minecraft_container_port }}.
|
||||||
|
geyser_remote_address: 127.0.0.1
|
||||||
|
# online matches the Java server's ONLINE_MODE. Set to "offline" only if you
|
||||||
|
# also run Floodgate and pair the servers with a shared key.
|
||||||
|
geyser_auth_type: online
|
||||||
101
roles/minecraft/tasks/main.yml
Normal file
101
roles/minecraft/tasks/main.yml
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
---
|
||||||
|
# Minecraft server deployment: a Quadlet pod running the Java server
|
||||||
|
# (itzg/minecraft-server) and, when enabled, the Geyser bridge container
|
||||||
|
# (geysermc/geyser) so Bedrock clients can join. Everything runs rootless
|
||||||
|
# under the admin user. Containers share the pod's network namespace, so
|
||||||
|
# Geyser reaches the Java server at 127.0.0.1.
|
||||||
|
|
||||||
|
- name: Build the list of data directories
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
minecraft_data_dirs: >-
|
||||||
|
{{ [minecraft_data_dir, minecraft_data_dir + '/data', minecraft_data_dir + '/backups']
|
||||||
|
+ ([minecraft_data_dir + '/geyser'] if enable_geyser | bool else []) }}
|
||||||
|
|
||||||
|
- name: Create the Minecraft data directory tree
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ item }}"
|
||||||
|
state: directory
|
||||||
|
owner: "{{ admin_user }}"
|
||||||
|
group: "{{ admin_user }}"
|
||||||
|
mode: "0750"
|
||||||
|
loop: "{{ minecraft_data_dirs }}"
|
||||||
|
|
||||||
|
- name: Restore SELinux context on the data directory
|
||||||
|
ansible.builtin.command: restorecon -Rv {{ minecraft_data_dir }}
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Create the Quadlet systemd unit directory
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "/home/{{ admin_user }}/.config/containers/systemd"
|
||||||
|
state: directory
|
||||||
|
owner: "{{ admin_user }}"
|
||||||
|
group: "{{ admin_user }}"
|
||||||
|
mode: "0700"
|
||||||
|
|
||||||
|
- name: Deploy the Minecraft pod unit
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: minecraft.pod.j2
|
||||||
|
dest: "/home/{{ admin_user }}/.config/containers/systemd/minecraft.pod"
|
||||||
|
owner: "{{ admin_user }}"
|
||||||
|
group: "{{ admin_user }}"
|
||||||
|
mode: "0644"
|
||||||
|
|
||||||
|
- name: Deploy the Minecraft Java server unit
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: minecraft-server.container.j2
|
||||||
|
dest: "/home/{{ admin_user }}/.config/containers/systemd/minecraft-server.container"
|
||||||
|
owner: "{{ admin_user }}"
|
||||||
|
group: "{{ admin_user }}"
|
||||||
|
mode: "0644"
|
||||||
|
|
||||||
|
- name: Deploy the Geyser config file
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: geyser-config.yml.j2
|
||||||
|
dest: "{{ minecraft_data_dir }}/geyser/config.yml"
|
||||||
|
owner: "{{ admin_user }}"
|
||||||
|
group: "{{ admin_user }}"
|
||||||
|
mode: "0640"
|
||||||
|
when: enable_geyser | bool
|
||||||
|
|
||||||
|
- name: Deploy the Geyser bridge unit
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: geyser.container.j2
|
||||||
|
dest: "/home/{{ admin_user }}/.config/containers/systemd/geyser.container"
|
||||||
|
owner: "{{ admin_user }}"
|
||||||
|
group: "{{ admin_user }}"
|
||||||
|
mode: "0644"
|
||||||
|
when: enable_geyser | bool
|
||||||
|
|
||||||
|
- name: Build the list of images to pull
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
minecraft_images: "{{ [minecraft_image] + ([geyser_image] if enable_geyser | bool else []) }}"
|
||||||
|
when: minecraft_prepull | bool
|
||||||
|
|
||||||
|
- name: Check if container images are already present
|
||||||
|
ansible.builtin.command: machinectl shell {{ admin_user }}@ -- podman image exists {{ item }}
|
||||||
|
loop: "{{ minecraft_images }}"
|
||||||
|
register: images_exist
|
||||||
|
changed_when: false
|
||||||
|
failed_when: false
|
||||||
|
when: minecraft_prepull | bool
|
||||||
|
|
||||||
|
- name: Pull missing container images
|
||||||
|
ansible.builtin.command: machinectl shell {{ admin_user }}@ -- podman pull {{ item.item }}
|
||||||
|
loop: "{{ images_exist.results }}"
|
||||||
|
when:
|
||||||
|
- minecraft_prepull | bool
|
||||||
|
- item.rc != 0
|
||||||
|
|
||||||
|
- name: Build the list of user services to enable
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
minecraft_units: "{{ ['minecraft-pod.service', 'minecraft-server.service'] + (['geyser.service'] if enable_geyser | bool else []) }}"
|
||||||
|
|
||||||
|
- name: Reload the user systemd daemon
|
||||||
|
ansible.builtin.command: machinectl shell {{ admin_user }}@ -- systemctl --user daemon-reload
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Enable and start the Minecraft services
|
||||||
|
ansible.builtin.command: machinectl shell {{ admin_user }}@ -- systemctl --user enable --now {{ item }}
|
||||||
|
loop: "{{ minecraft_units }}"
|
||||||
|
register: mc_services
|
||||||
|
changed_when: false
|
||||||
17
roles/minecraft/templates/geyser-config.yml.j2
Normal file
17
roles/minecraft/templates/geyser-config.yml.j2
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Managed by Ansible - minecraft role (Geyser bridge).
|
||||||
|
# Minimal config: Geyser merges these keys over its built-in defaults.
|
||||||
|
# The Java server shares the pod's network namespace, so it is reachable
|
||||||
|
# at 127.0.0.1:{{ minecraft_container_port }}.
|
||||||
|
|
||||||
|
bedrock:
|
||||||
|
address: 0.0.0.0
|
||||||
|
port: {{ geyser_port }}
|
||||||
|
clone-remote-port: false
|
||||||
|
enable-proxy-protocol: false
|
||||||
|
|
||||||
|
remote:
|
||||||
|
address: {{ geyser_remote_address }}
|
||||||
|
port: {{ minecraft_container_port }}
|
||||||
|
auth-type: {{ geyser_auth_type }}
|
||||||
|
allow-passthrough-motd: true
|
||||||
|
allow-passthrough-player-counts: true
|
||||||
18
roles/minecraft/templates/geyser.container.j2
Normal file
18
roles/minecraft/templates/geyser.container.j2
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Geyser Bedrock bridge container ({{ geyser_image }})
|
||||||
|
Wants=network-online.target
|
||||||
|
After=network-online.target
|
||||||
|
|
||||||
|
[Container]
|
||||||
|
Label=app=geyser
|
||||||
|
ContainerName=geyser
|
||||||
|
Pod=minecraft.pod
|
||||||
|
Image={{ geyser_image }}
|
||||||
|
Volume={{ minecraft_data_dir }}/geyser:/geyser:Z
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
33
roles/minecraft/templates/minecraft-server.container.j2
Normal file
33
roles/minecraft/templates/minecraft-server.container.j2
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Minecraft Java server container ({{ minecraft_image }})
|
||||||
|
Wants=network-online.target
|
||||||
|
After=network-online.target
|
||||||
|
|
||||||
|
[Container]
|
||||||
|
Label=app=minecraft-server
|
||||||
|
ContainerName=minecraft-server
|
||||||
|
Pod=minecraft.pod
|
||||||
|
Image={{ minecraft_image }}
|
||||||
|
Environment=EULA=TRUE
|
||||||
|
Environment=TYPE={{ minecraft_server_type }}
|
||||||
|
Environment=VERSION={{ minecraft_version }}
|
||||||
|
Environment=MEMORY={{ minecraft_memory }}
|
||||||
|
Environment=DIFFICULTY={{ minecraft_difficulty }}
|
||||||
|
Environment=MOTD={{ minecraft_motd }}
|
||||||
|
Environment=MAX_PLAYERS={{ minecraft_max_players }}
|
||||||
|
Environment=VIEW_DISTANCE={{ minecraft_view_distance }}
|
||||||
|
Environment=ONLINE_MODE={{ minecraft_online_mode }}
|
||||||
|
Environment=OVERRIDE_SERVER_PROPERTIES=TRUE
|
||||||
|
{% if minecraft_whitelist | length > 0 %}
|
||||||
|
Environment=ENABLE_WHITELIST=TRUE
|
||||||
|
Environment=WHITELIST={{ minecraft_whitelist }}
|
||||||
|
{% endif %}
|
||||||
|
Volume={{ minecraft_data_dir }}/data:/data:Z
|
||||||
|
Volume={{ minecraft_data_dir }}/backups:/backups:Z
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
14
roles/minecraft/templates/minecraft.pod.j2
Normal file
14
roles/minecraft/templates/minecraft.pod.j2
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Minecraft pod (Java server + Geyser bridge)
|
||||||
|
|
||||||
|
[Pod]
|
||||||
|
PublishPort={{ minecraft_port }}:25565/tcp
|
||||||
|
{% if minecraft_udp | bool %}
|
||||||
|
PublishPort={{ minecraft_port }}:25565/udp
|
||||||
|
{% endif %}
|
||||||
|
{% if enable_geyser | bool %}
|
||||||
|
PublishPort={{ geyser_port }}:{{ geyser_port }}/udp
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
30
roles/packages/defaults/main.yml
Normal file
30
roles/packages/defaults/main.yml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
base_packages:
|
||||||
|
- wget
|
||||||
|
- curl
|
||||||
|
- tar
|
||||||
|
- unzip
|
||||||
|
- git
|
||||||
|
- vim-enhanced
|
||||||
|
- htop
|
||||||
|
- tmux
|
||||||
|
- firewalld
|
||||||
|
- fail2ban
|
||||||
|
- dnf-automatic
|
||||||
|
- policycoreutils-python-utils
|
||||||
|
- python3-pip
|
||||||
|
|
||||||
|
podman_packages:
|
||||||
|
- container-tools
|
||||||
|
- podman
|
||||||
|
- podman-docker
|
||||||
|
- podman-compose
|
||||||
|
- fuse-overlayfs
|
||||||
|
- slirp4netns
|
||||||
|
- netavark
|
||||||
|
|
||||||
|
qemu_packages:
|
||||||
|
- qemu-kvm-core
|
||||||
|
|
||||||
|
install_qemu: false
|
||||||
|
enable_dnf_automatic: true
|
||||||
45
roles/packages/tasks/main.yml
Normal file
45
roles/packages/tasks/main.yml
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
# Install base system packages, Podman tooling, and keep everything patched.
|
||||||
|
|
||||||
|
- name: Install common system packages
|
||||||
|
ansible.builtin.dnf:
|
||||||
|
name: "{{ base_packages }}"
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Install Podman container tooling
|
||||||
|
ansible.builtin.dnf:
|
||||||
|
name: "{{ podman_packages }}"
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Install QEMU packages (optional - required for building images that need emulation)
|
||||||
|
ansible.builtin.dnf:
|
||||||
|
name: "{{ qemu_packages }}"
|
||||||
|
state: present
|
||||||
|
when: install_qemu | bool
|
||||||
|
|
||||||
|
- name: Apply all available package updates
|
||||||
|
ansible.builtin.dnf:
|
||||||
|
name: "*"
|
||||||
|
state: latest
|
||||||
|
update_only: true
|
||||||
|
|
||||||
|
- name: Enable automatic security updates
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: dnf-automatic.timer
|
||||||
|
enabled: true
|
||||||
|
state: started
|
||||||
|
when: enable_dnf_automatic | bool
|
||||||
|
|
||||||
|
- name: Configure dnf-automatic to apply updates
|
||||||
|
ansible.builtin.ini_file:
|
||||||
|
path: /etc/dnf/automatic.conf
|
||||||
|
section: commands
|
||||||
|
option: apply_updates
|
||||||
|
value: "yes"
|
||||||
|
when: enable_dnf_automatic | bool
|
||||||
|
|
||||||
|
- name: Install container SELinux policy support
|
||||||
|
ansible.builtin.dnf:
|
||||||
|
name: container-selinux
|
||||||
|
state: present
|
||||||
|
when: ansible_selinux is defined and ansible_selinux.status == "enabled"
|
||||||
8
roles/podman/defaults/main.yml
Normal file
8
roles/podman/defaults/main.yml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
enable_podman_socket: true
|
||||||
|
podman_enable_linger: true
|
||||||
|
podman_manage_subuid: true
|
||||||
|
podman_subuid_start: 200000
|
||||||
|
podman_manage_subgid: true
|
||||||
|
podman_subgid_start: 200000
|
||||||
|
podman_storage_driver: overlay
|
||||||
62
roles/podman/tasks/main.yml
Normal file
62
roles/podman/tasks/main.yml
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
---
|
||||||
|
# Podman runtime preparation: rootless container support for the admin user.
|
||||||
|
|
||||||
|
- name: Enable Podman socket activation
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: podman.socket
|
||||||
|
enabled: true
|
||||||
|
state: started
|
||||||
|
when: enable_podman_socket | bool
|
||||||
|
|
||||||
|
- name: Enable user lingering for rootless containers
|
||||||
|
ansible.builtin.command: loginctl enable-linger {{ admin_user }}
|
||||||
|
changed_when: false
|
||||||
|
when: podman_enable_linger | bool
|
||||||
|
|
||||||
|
- name: Ensure subuid range for the admin user
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
path: /etc/subuid
|
||||||
|
regexp: '^{{ admin_user }}:'
|
||||||
|
line: "{{ admin_user }}:{{ podman_subuid_start }}:65536"
|
||||||
|
when: podman_manage_subuid | bool
|
||||||
|
|
||||||
|
- name: Ensure subgid range for the admin user
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
path: /etc/subgid
|
||||||
|
regexp: '^{{ admin_user }}:'
|
||||||
|
line: "{{ admin_user }}:{{ podman_subgid_start }}:65536"
|
||||||
|
when: podman_manage_subgid | bool
|
||||||
|
|
||||||
|
- name: Persist kernel modules for container networking
|
||||||
|
ansible.builtin.copy:
|
||||||
|
dest: /etc/modules-load.d/containers.conf
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0644"
|
||||||
|
content: "overlay\nbr_netfilter\n"
|
||||||
|
|
||||||
|
- name: Load overlay kernel module
|
||||||
|
ansible.builtin.command: modprobe overlay
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Load br_netfilter kernel module
|
||||||
|
ansible.builtin.command: modprobe br_netfilter
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Enable IPv4 forwarding persistently
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
path: /etc/sysctl.d/99-podman.conf
|
||||||
|
create: true
|
||||||
|
regexp: '^net\.ipv4\.ip_forward'
|
||||||
|
line: "net.ipv4.ip_forward = 1"
|
||||||
|
mode: "0644"
|
||||||
|
|
||||||
|
- name: Apply IPv4 forwarding
|
||||||
|
ansible.builtin.command: sysctl -w net.ipv4.ip_forward=1
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Configure Podman storage driver
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
path: /etc/containers/storage.conf
|
||||||
|
regexp: '^driver\s*='
|
||||||
|
line: 'driver = "{{ podman_storage_driver }}"'
|
||||||
2
roles/selinux/defaults/main.yml
Normal file
2
roles/selinux/defaults/main.yml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
---
|
||||||
|
selinux_set_minecraft_fcontext: true
|
||||||
22
roles/selinux/tasks/main.yml
Normal file
22
roles/selinux/tasks/main.yml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
# SELinux hardening: enforcing mode plus correct labels for container data.
|
||||||
|
|
||||||
|
- name: Enforce SELinux (targeted policy)
|
||||||
|
ansible.builtin.selinux:
|
||||||
|
policy: targeted
|
||||||
|
state: enforcing
|
||||||
|
|
||||||
|
- name: Ensure semanage is available
|
||||||
|
ansible.builtin.dnf:
|
||||||
|
name: policycoreutils-python-utils
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Add SELinux file context for the Minecraft data directory
|
||||||
|
ansible.builtin.command:
|
||||||
|
cmd: semanage fcontext -a -t container_file_t "{{ minecraft_data_dir }}(/.*)?"
|
||||||
|
register: selinux_fcontext
|
||||||
|
changed_when: selinux_fcontext.rc == 0
|
||||||
|
failed_when:
|
||||||
|
- selinux_fcontext.rc != 0
|
||||||
|
- "'already defined' not in (selinux_fcontext.stdout + selinux_fcontext.stderr)"
|
||||||
|
when: selinux_set_minecraft_fcontext | bool
|
||||||
7
roles/ssh_harden/defaults/main.yml
Normal file
7
roles/ssh_harden/defaults/main.yml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
ssh_port: 22
|
||||||
|
ssh_restrict_users: true
|
||||||
|
ssh_client_alive_interval: 300
|
||||||
|
ssh_client_alive_count_max: 2
|
||||||
|
ssh_max_auth_tries: 3
|
||||||
|
enable_fail2ban: true
|
||||||
10
roles/ssh_harden/handlers/main.yml
Normal file
10
roles/ssh_harden/handlers/main.yml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
- name: restart sshd
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: sshd
|
||||||
|
state: restarted
|
||||||
|
|
||||||
|
- name: restart fail2ban
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: fail2ban
|
||||||
|
state: restarted
|
||||||
34
roles/ssh_harden/tasks/main.yml
Normal file
34
roles/ssh_harden/tasks/main.yml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
# SSH hardening: key-only auth for the admin user via a drop-in config,
|
||||||
|
# plus fail2ban to rate-limit brute force attempts.
|
||||||
|
|
||||||
|
- name: Deploy SSH server hardening drop-in
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: sshd-hardening.conf.j2
|
||||||
|
dest: /etc/ssh/sshd_config.d/99-ansible-hardening.conf
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0644"
|
||||||
|
notify: restart sshd
|
||||||
|
|
||||||
|
- name: Validate sshd configuration before restart
|
||||||
|
ansible.builtin.command: /usr/sbin/sshd -t
|
||||||
|
register: sshd_test
|
||||||
|
changed_when: false
|
||||||
|
failed_when: sshd_test.rc != 0
|
||||||
|
|
||||||
|
- name: Deploy fail2ban SSH jail
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: fail2ban-local.j2
|
||||||
|
dest: /etc/fail2ban/jail.local
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0644"
|
||||||
|
notify: restart fail2ban
|
||||||
|
|
||||||
|
- name: Ensure fail2ban is running and enabled
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: fail2ban
|
||||||
|
enabled: true
|
||||||
|
state: started
|
||||||
|
when: enable_fail2ban | bool
|
||||||
11
roles/ssh_harden/templates/fail2ban-local.j2
Normal file
11
roles/ssh_harden/templates/fail2ban-local.j2
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Managed by Ansible - ssh_harden role.
|
||||||
|
|
||||||
|
[DEFAULT]
|
||||||
|
bantime = 1h
|
||||||
|
findtime = 10m
|
||||||
|
maxretry = 5
|
||||||
|
ignoreip = 127.0.0.1/8 ::1
|
||||||
|
|
||||||
|
[sshd]
|
||||||
|
enabled = true
|
||||||
|
port = {{ ssh_port }}
|
||||||
18
roles/ssh_harden/templates/sshd-hardening.conf.j2
Normal file
18
roles/ssh_harden/templates/sshd-hardening.conf.j2
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Managed by Ansible - ssh_harden role.
|
||||||
|
# Sorted last (99-) so this wins over other drop-ins and the main config.
|
||||||
|
|
||||||
|
PermitRootLogin no
|
||||||
|
PasswordAuthentication no
|
||||||
|
KbdInteractiveAuthentication no
|
||||||
|
PubkeyAuthentication yes
|
||||||
|
PermitEmptyPasswords no
|
||||||
|
UseDNS no
|
||||||
|
X11Forwarding no
|
||||||
|
PrintMotd no
|
||||||
|
MaxAuthTries {{ ssh_max_auth_tries }}
|
||||||
|
MaxSessions 4
|
||||||
|
LoginGraceTime 30
|
||||||
|
ClientAliveInterval {{ ssh_client_alive_interval }}
|
||||||
|
ClientAliveCountMax {{ ssh_client_alive_count_max }}
|
||||||
|
{% if ssh_port != 22 %}Port {{ ssh_port }}{% endif %}
|
||||||
|
{% if ssh_restrict_users %}AllowUsers {{ admin_user }}{% endif %}
|
||||||
Reference in New Issue
Block a user