Compare commits

4 Commits
main ... geyser

Author SHA1 Message Date
9bd218241d removed bot tools 2026-09-12 14:33:47 -06:00
85510acd33 initial commit 1 2026-09-12 14:33:01 -06:00
09a5891364 initial opencode commit 2026-09-12 14:31:27 -06:00
a2deef0f5a create new geyser branch 2026-09-12 12:11:58 -06:00
48 changed files with 845 additions and 1605 deletions

21
LICENSE Normal file
View 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.

103
Makefile
View File

@@ -1,103 +0,0 @@
# =============================================================================
# Minecraft Podman Deployment - Makefile
# =============================================================================
# Usage:
# make install # Install dependencies
# make deploy # Deploy Minecraft server
# make check # Dry-run (check mode)
# make debug # Verbose output
# make status # Check server status
# make backup # Run manual backup
# make restore # Restore from latest backup
# make logs # View server logs
# make stop # Stop Minecraft server
# make start # Start Minecraft server
# make restart # Restart Minecraft server
# make update # Update Minecraft to latest version
# make uninstall # Remove Minecraft deployment (dangerous!)
# =============================================================================
PLAYBOOK := site.yml
LIMIT ?= external-apps
INVENTORY := inventory/hosts
.PHONY: help install deploy check debug status backup restore logs stop start restart update uninstall
help:
@echo "Minecraft Podman Deployment Makefile"
@echo ""
@echo "Usage: make [target]"
@echo ""
@echo "Targets:"
@echo " install Install Ansible collections and dependencies"
@echo " deploy Deploy Minecraft server (default: $(LIMIT))"
@echo " check Dry-run deployment (--check mode)"
@echo " debug Deploy with verbose output"
@echo " status Check server status on remote"
@echo " backup Run manual backup on remote"
@echo " restore Restore from latest backup on remote"
@echo " logs View Minecraft server logs"
@echo " stop Stop Minecraft server"
@echo " start Start Minecraft server"
@echo " restart Restart Minecraft server"
@echo " update Update Minecraft to latest version"
@echo " uninstall Remove Minecraft deployment (DANGEROUS)"
@echo ""
@echo "Override target host: make deploy LIMIT=my-server"
install:
@echo "Installing Ansible collections..."
ansible-galaxy collection install -r requirements.yml
deploy:
@echo "Deploying Minecraft to $(LIMIT)..."
ansible-playbook $(PLAYBOOK) --limit $(LIMIT)
check:
@echo "Dry-run deployment for $(LIMIT)..."
ansible-playbook $(PLAYBOOK) --limit $(LIMIT) --check
debug:
@echo "Deploying with debug output for $(LIMIT)..."
ansible-playbook $(PLAYBOOK) --limit $(LIMIT) -vvv
status:
@echo "Checking server status on $(LIMIT)..."
ansible $(LIMIT) -m shell -a 'systemctl status minecraft --no-pager'
backup:
@echo "Running backup on $(LIMIT)..."
ansible $(LIMIT) -m shell -a '/opt/minecraft/backup.sh'
restore:
@echo "Running restore on $(LIMIT)..."
ansible $(LIMIT) -m shell -a '/opt/minecraft/restore.sh'
logs:
@echo "Viewing logs for $(LIMIT)..."
ansible $(LIMIT) -m shell -a 'journalctl -u minecraft -n 100 --no-pager'
stop:
@echo "Stopping Minecraft server on $(LIMIT)..."
ansible $(LIMIT) -m systemd -a 'name=minecraft state=stopped'
start:
@echo "Starting Minecraft server on $(LIMIT)..."
ansible $(LIMIT) -m systemd -a 'name=minecraft state=started'
restart:
@echo "Restarting Minecraft server on $(LIMIT)..."
ansible $(LIMIT) -m systemd -a 'name=minecraft state=restarted'
update:
@echo "Updating Minecraft on $(LIMIT)..."
ansible-playbook $(PLAYBOOK) --limit $(LIMIT) --tags update
uninstall:
@echo "WARNING: This will remove the Minecraft deployment from $(LIMIT)"
@read -p "Are you sure? Type 'yes' to continue: " CONFIRM; \
if [ "$$CONFIRM" = "yes" ]; then \
ansible-playbook $(PLAYBOOK) --limit $(LIMIT) --tags uninstall; \
else \
echo "Aborted."; \
fi

280
README.md
View File

@@ -1,190 +1,146 @@
# Minecraft Podman Deployment (Ansible) git pu# Hardened Fedora Minecraft Server (Ansible)
Automated deployment of a Minecraft server running in a **Podman** container on a remote **Fedora** server using **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 ## Architecture
``` Both game containers run in a single Podman **pod** (shared network
┌─────────────────────────────────────────────────┐ namespace) as the non-root user `mcadmin`:
│ Ansible Control Node │
│ (your machine) │
│ ansible.cfg + site.yml + roles/ │
└──────────────────────┬──────────────────────────┘
│ SSH
┌──────────────────────▼──────────────────────────┐
│ Fedora Server │
│ "external-apps" │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ Podman Container │ │
│ │ itzg/minecraft-server:latest (Paper) │ │
│ │ ┌────────────────────────────────────┐ │ │
│ │ │ Minecraft Server (Java) │ │ │
│ │ └────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────┘ │
│ │
│ systemd: minecraft.service │
│ systemd: minecraft-backup.timer │
│ data: /opt/minecraft/data │
│ backups: /opt/minecraft/backups │
└─────────────────────────────────────────────────┘
```
## Directory Structure
``` ```
external-minecraft/ Internet
├── ansible.cfg # Ansible configuration │
├── site.yml # Main deployment playbook ┌───────┴────────┐
├── uninstall.yml # Cleanup playbook │ firewall (DROP)│ only the ports below are allowed
├── requirements.yml # Galaxy collections └───────┬────────┘
├── Makefile # Convenient wrapper │
├── inventory/ pod: minecraft (rootless, user mcadmin)
│ └── hosts # Server inventory ├── minecraft-server JVM on 0.0.0.0:25565 (Java clients)
├── group_vars/ │ └── world data → /srv/minecraft/data
│ └── all.yml # All configuration variables └── geyser Bedrock listener :19132/udp
├── host_vars/ # (optional) Per-host overrides └── 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/ └── roles/
└── minecraft-podman/ ├── packages/ # base + container tooling, full dnf update, dnf-automatic
├── tasks/main.yml # All deployment tasks ├── firewall/ # firewalld: DROP target, open SSH + Minecraft + Geyser
├── handlers/main.yml # Service handlers ├── selinux/ # enforcing mode + container_file_t context for /srv/minecraft
├── templates/ # Jinja2 templates ├── auth/ # admin user, SSH key, sudoers, password policy, lock root
│ ├── minecraft.service.j2 ├── ssh_harden/ # sshd drop-in + fail2ban jail
│ ├── minecraft.env.j2 ├── podman/ # socket, linger, subuid/subgid, ip_forward, storage
│ ├── server.properties.j2 └── minecraft/ # data dirs, Quadlet pod/container units, image pulls
│ ├── Containerfile.j2
│ ├── backup.sh.j2
│ ├── restore.sh.j2
│ ├── health-check.sh.j2
│ └── jvm.opts.j2
└── defaults/main.yml # Role defaults
``` ```
## Prerequisites ## Prerequisites
### On the control node (your machine) - 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 ```bash
# Fedora/RHEL/CentOS # 1. Install the required collection
sudo dnf install python3-pip
pip3 install --user ansible
# or via pipx
pipx install ansible
# Debian/Ubuntu
sudo apt install ansible
```
### Install Ansible collections
```bash
make install
# or
ansible-galaxy collection install -r requirements.yml 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 ## Configuration
Edit `group_vars/all.yml` to configure your server: All tunables live in `host_vars/external-apps.yml` (per server) and
`group_vars/all.yml` (cluster-wide). Highlights:
| Variable | Description | Default | | Variable | Default | Meaning |
|----------|-------------|---------| |----------|---------|---------|
| `minecraft.server_name` | Service/container name | `minecraft` | | `admin_user` | `mcadmin` | Non-root user running the containers |
| `minecraft.version` | Server version | `latest` | | `minecraft_version` | `LATEST` | Server version (`1.21.1`, …) |
| `minecraft.server_type` | Vanilla / Paper / etc. | `paper` | | `minecraft_server_type` | `PAPER` | PAPER / VANILLA / FABRIC / FORGE |
| `minecraft.java_memory` | Java heap size | `4G` | | `minecraft_memory` | `2G` | JVM memory limit |
| `minecraft.max_players` | Max players | `20` | | `minecraft_whitelist` | `""` | Comma-separated allowed usernames (empty = off) |
| `minecraft.port` | Minecraft port | `25565` | | `enable_geyser` | `true` | Deploy the Bedrock bridge |
| `minecraft.rcon_password` | RCON password | `CHANGE_ME_NOW` | | `geyser_port` | `19132` | Bedrock UDP port |
| `minecraft.motd` | Server message of day | Welcome message | | `geyser_auth_type` | `online` | `offline` only with a paired Floodgate key |
| `paths.base_dir` | Install directory | `/opt/minecraft` | | `ssh_port` | `22` | SSH port used by firewall + sshd |
| `backup.enabled` | Enable automatic backups | `true` |
**IMPORTANT**: Change `minecraft.rcon_password` before deploying. ## Operations
## Deployment After the playbook has run, manage the servers as `mcadmin`:
```bash ```bash
# Basic deployment sudo machinectl shell mcadmin@ -- systemctl --user status minecraft-server geyser
make deploy sudo machinectl shell mcadmin@ -- systemctl --user restart minecraft-server
sudo machinectl shell mcadmin@ -- podman ps # container list
# With explicit host sudo machinectl shell mcadmin@ -- journalctl --user-unit minecraft-server -f
make deploy LIMIT=external-apps
# Or directly
ansible-playbook site.yml --limit external-apps
# Dry-run
make check
# Debug mode
make debug
``` ```
## Management ## References
```bash - [Ansible Documentation](https://docs.ansible.com/)
make status # Check server status - [Fedora Project](https://fedoraproject.org/)
make logs # View recent logs - [Podman](https://podman.io/)
make restart # Restart the server - [Podman Quadlet (systemd.unit) man page](https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html)
make stop # Stop the server - [Rootless Podman tutorial](https://github.com/containers/podman/blob/main/docs/tutorials/rootless_tutorial.md)
make start # Start the server - [itzg/docker-minecraft-server](https://github.com/itzg/docker-minecraft-server)
make backup # Run backup immediately - [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/)
## Connecting to the Server ## License
In Minecraft, use: [MIT](./LICENSE)
```
<server-ip>:<port>
```
For the default setup: `external-apps:25565`
## Backups
Backups run automatically every 6 hours (configurable in `group_vars/all.yml` under `backup.schedule`).
- **Location**: `/opt/minecraft/backups/`
- **Format**: `minecraft_YYYYMMDD_HHMMSS.tar.gz`
- **Retention**: 10 backups (configurable)
Restore with:
```bash
make restore
```
## Uninstall
```bash
make uninstall # Interactive confirmation
```
Or manually:
```bash
ansible-playbook uninstall.yml --limit external-apps
```
## Advanced Configuration
### Custom Server Type
Set `minecraft.server_type` to:
- `vanilla` - Official Mojang server
- `paper` - High-performance fork (recommended)
- `fabric` - Modding platform
- `forge` - Classic modding
- `purpur` - Paper fork with more features
### Custom Container Image
To use your own `Containerfile` instead of pulling a public image:
1. Set `minecraft.use_custom_container: true` in `group_vars`
2. The role will build and deploy a custom image
### Extras
- **RCON**: Enabled by default on port 25575. Allows remote console admin.
- **Whitelist**: Set `minecraft.whitelist_enabled: true` to require whitelist
- **Command Blocks**: Set `minecraft.enable_command_block: true` to enable

View File

@@ -1,18 +1,10 @@
[defaults] [defaults]
inventory = inventory/hosts inventory = inventory.yml
roles_path = roles roles_path = roles
remote_user = boxswapper
host_key_checking = False
retry_files_enabled = False retry_files_enabled = False
interpreter_python = auto_silent
stdout_callback = yaml stdout_callback = yaml
timeout = 30 gathering = smart
[privilege_escalation]
become = True
become_method = sudo
become_user = root
become_ask_pass = False
[ssh_connection] [ssh_connection]
pipelining = True pipelining = True
ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o ServerAliveInterval=30

View File

@@ -1,162 +1,31 @@
--- ---
# ============================================================================= # ---------------------------------------------------------------------------
# Minecraft Podman Deployment - Global Variables # Variables shared by every host in the inventory.
# ============================================================================= # Role defaults live in roles/<role>/defaults/main.yml and are overridden here.
# All variables defined here can be overridden in host_vars/<hostname>.yml # ---------------------------------------------------------------------------
# or passed at runtime with -e "variable=value"
# ----------------------------------------------------------------------------- # Administrative non-root user created by the auth role.
# Server Configuration # This is the account the Minecraft containers run under (rootless Podman).
# ----------------------------------------------------------------------------- admin_user: mcadmin
minecraft:
# Server name - used for systemd service naming and container name
server_name: "minecraft"
# Minecraft version (latest, 1.21.4, 1.20.6, etc.) # SSH public key installed for {{ admin_user }}. MUST be set before first run
version: "latest" # 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: ""
# Server type: "vanilla", "paper", "fabric", "forge", "purpur" # SSH daemon port (firewall + ssh_harden will both use this).
server_type: "paper" ssh_port: 22
# Memory allocation for Java (e.g., "4G", "8G", "2G") # Minecraft Java server port exposed to the internet.
java_memory: "4G" minecraft_port: 25565
minecraft_udp: false
# Java JVM arguments (additional flags beyond memory) # Wher the Minecraft world/config data lives on the host.
java_args: "-XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:MaxGCPauseMillis=200" minecraft_data_dir: /srv/minecraft
# Maximum players allowed # Geyser - Bedrock protocol bridge. Opens the Bedrock UDP port in the
max_players: 20 # firewall (roles/firewall) and deploys the geyser.container Quadlet unit
# (roles/minecraft) next to the Java server.
# Server port (default Minecraft port: 25565) enable_geyser: true
port: 25565 # Bedrock UDP port Geyser publishes (host + container, must match).
geyser_port: 19132
# RCON port (for remote console access)
rcon_port: 25575
# Enable RCON (useful for management)
rcon_enabled: true
# RCON password (CHANGE THIS!)
rcon_password: "CHANGE_ME_NOW"
# Server MOTD (Message of the Day)
motd: "Welcome to the Minecraft Server!"
# Game mode: survival, creative, adventure, spectator
game_mode: "survival"
# Difficulty: peaceful, easy, normal, hard
difficulty: "normal"
# Enable whitelist
whitelist_enabled: false
# Allow command blocks
enable_command_block: false
# View distance (chunks, max 32)
view_distance: 10
# Simulation distance (chunks)
simulation_distance: 6
# Online mode (true = requires Mojang account verification)
online_mode: true
# Max world size (blocks, default 29999984)
max_world_size: 29999984
# Spawn protection (blocks around spawn that can't be modified)
spawn_protection: 16
# -----------------------------------------------------------------------------
# Podman Configuration
# -----------------------------------------------------------------------------
podman:
# Container image (itzg/minecraft-server is highly configurable)
image: "itzg/minecraft-server"
# Image tag (usually the server type)
image_tag: "latest"
# Container restart policy: always, unless-stopped, on-failure, no
restart_policy: "unless-stopped"
# Container memory limit (leave empty for unlimited)
memory_limit: ""
# CPU shares (relative weight)
cpu_shares: 1024
# Container log driver
log_driver: "journald"
# Container log max size
log_max_size: "50m"
# Container log max file count
log_max_file: "3"
# Extra volumes to mount (format: "host_path:container_path:options")
extra_volumes: []
# Container labels
labels:
managed-by: "ansible"
app: "minecraft"
# Network mode (host, bridge, or slirp4netns)
network_mode: "host"
# -----------------------------------------------------------------------------
# Path Configuration
# -----------------------------------------------------------------------------
paths:
# Base installation directory
base_dir: "/opt/minecraft"
# Data directory (where world, configs, etc. are stored)
data_dir: "/opt/minecraft/data"
# Backup directory
backup_dir: "/opt/minecraft/backups"
# Systemd service directory
systemd_dir: "/etc/systemd/system"
# -----------------------------------------------------------------------------
# Backup Configuration
# -----------------------------------------------------------------------------
backup:
# Enable automatic backups
enabled: true
# Backup schedule (cron format)
schedule: "0 */6 * * *" # Every 6 hours
# Number of backups to retain
retention_count: 10
# Compress backups with gzip
compress: true
# -----------------------------------------------------------------------------
# Firewall Configuration
# -----------------------------------------------------------------------------
firewall:
# Open Minecraft ports in firewalld
enabled: true
# Additional ports to open (for mods, voice chat, etc.)
additional_ports:
- "19132/udp" # Bedrock (if needed)
# -----------------------------------------------------------------------------
# Monitoring (Optional)
# -----------------------------------------------------------------------------
monitoring:
# Enable basic health checks
enabled: false
# Health check interval in seconds
interval: 300

View 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

View 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
View 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

View File

@@ -1,9 +0,0 @@
# Ansible Inventory - External Minecraft Server
# Managed by Sisyphus for Minecraft Podman Deployment
[minecraft]
external-apps ansible_host=external-apps ansible_user=boxswapper
[minecraft:vars]
ansible_python_interpreter=/usr/bin/python3
ansible_ssh_common_args='-o StrictHostKeyChecking=no'

View File

@@ -1,9 +1,3 @@
--- ---
# Ansible Galaxy collections required by this deployment
# Install with: ansible-galaxy collection install -r requirements.yml
collections: collections:
- name: ansible.posix - ansible.posix
version: ">=1.5.0"
- name: containers.podman
version: ">=1.10.0"

View 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
View 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

View File

@@ -0,0 +1,6 @@
---
firewall_zone: public
firewall_zone_target: DROP
firewall_allow_ping: true
enable_geyser: true
geyser_port: 19132

View 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

View File

@@ -1,19 +0,0 @@
---
# =============================================================================
# Minecraft Podman Deployment - Role Defaults
# =============================================================================
# These are the default values for this role. They can be overridden by
# group_vars/all.yml, host_vars/<host>.yml, or -e at runtime.
# =============================================================================
# User to run the Minecraft container as
minecraft_user: "minecraft"
# Podman container name
podman_container_name: "minecraft"
# Whether to use a custom Containerfile (true) or pull pre-built image (false)
minecraft_use_custom_container: "{{ minecraft.use_custom_container | default(false) }}"
# Custom image name (only used if use_custom_container is true)
minecraft_custom_image_name: "minecraft-custom"

View File

@@ -1,25 +0,0 @@
---
# =============================================================================
# Minecraft Podman Deployment - Handlers
# =============================================================================
- name: restart minecraft
ansible.builtin.systemd:
name: "{{ minecraft.server_name }}.service"
state: restarted
daemon_reload: true
- name: reload systemd
ansible.builtin.systemd:
daemon_reload: true
- name: restart firewalld
ansible.builtin.service:
name: firewalld
state: restarted
- name: reload backup timer
ansible.builtin.systemd:
name: "{{ minecraft.server_name }}-backup.timer"
state: restarted
daemon_reload: true

View File

@@ -1,423 +0,0 @@
---
# =============================================================================
# Minecraft Podman Deployment - Main Tasks
# =============================================================================
# This role deploys a Minecraft server using Podman on Fedora
# with systemd integration for service management
# -----------------------------------------------------------------------------
# System Preparation
# -----------------------------------------------------------------------------
- name: System Preparation
block:
- name: Update DNF cache
ansible.builtin.dnf:
update_cache: true
state: present
tags: [install, packages]
- name: Install required packages
ansible.builtin.dnf:
name:
- podman
- podman-plugins # Podman plugins (Netavark, etc.)
- fuse-overlayfs # Required for rootless storage
- slirp4netns # Required for rootless networking
- systemd-container # Systemd integration
- firewalld
- python3-firewall # For ansible firewalld module
- python3-podman # For ansible podman modules
- tar # For backups
- gzip # For compressed backups
- curl # For health checks
state: present
tags: [install, packages]
- name: Ensure Podman is installed
ansible.builtin.command: podman --version
register: podman_version
changed_when: false
tags: [install, verify]
- name: Display Podman version
ansible.builtin.debug:
msg: "Podman {{ podman_version.stdout }} installed"
tags: [install, verify]
tags: [install, packages]
# -----------------------------------------------------------------------------
# User and Permissions
# -----------------------------------------------------------------------------
- name: User Configuration
block:
- name: Create minecraft system user
ansible.builtin.user:
name: "{{ minecraft_user }}"
system: true
shell: /bin/bash
home: "{{ paths.base_dir }}"
create_home: false
comment: "Minecraft Server User"
when: minecraft_user != "root"
tags: [install, user]
- name: Configure subuid for rootless podman
ansible.builtin.lineinfile:
path: /etc/subuid
line: "{{ minecraft_user }}:100000:65536"
regexp: "^{{ minecraft_user }}:"
state: present
create: true
mode: "0644"
when: minecraft_user != "root"
tags: [install, user]
- name: Configure subgid for rootless podman
ansible.builtin.lineinfile:
path: /etc/subgid
line: "{{ minecraft_user }}:100000:65536"
regexp: "^{{ minecraft_user }}:"
state: present
create: true
mode: "0644"
when: minecraft_user != "root"
tags: [install, user]
- name: Create user config directory for podman
ansible.builtin.file:
path: "{{ paths.base_dir }}/.config/containers"
state: directory
owner: "{{ minecraft_user }}"
group: "{{ minecraft_user }}"
mode: "0755"
when: minecraft_user != "root"
tags: [install, user]
- name: Create user local share for podman storage
ansible.builtin.file:
path: "{{ paths.base_dir }}/.local/share/containers"
state: directory
owner: "{{ minecraft_user }}"
group: "{{ minecraft_user }}"
mode: "0755"
when: minecraft_user != "root"
tags: [install, user]
- name: Ensure systemd lingering is enabled for user
ansible.builtin.command: "loginctl enable-linger {{ minecraft_user }}"
changed_when: false
when: minecraft_user != "root"
tags: [install, user]
tags: [install]
# -----------------------------------------------------------------------------
# Directory Structure
# -----------------------------------------------------------------------------
- name: Directory Setup
block:
- name: Create base directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ minecraft_user }}"
group: "{{ minecraft_user }}"
mode: "0755"
loop:
- "{{ paths.base_dir }}"
- "{{ paths.data_dir }}"
- "{{ paths.backup_dir }}"
- "{{ paths.data_dir }}/logs"
- "{{ paths.data_dir }}/world"
tags: [install, directories]
- name: Create Containerfile directory
ansible.builtin.file:
path: "{{ paths.base_dir }}/container"
state: directory
owner: "{{ minecraft_user }}"
group: "{{ minecraft_user }}"
mode: "0755"
tags: [install, container]
tags: [install]
# -----------------------------------------------------------------------------
# Container Build (if using custom Containerfile)
# -----------------------------------------------------------------------------
- name: Container Build
block:
- name: Copy Containerfile
ansible.builtin.template:
src: Containerfile.j2
dest: "{{ paths.base_dir }}/container/Containerfile"
owner: "{{ minecraft_user }}"
group: "{{ minecraft_user }}"
mode: "0644"
when: minecraft.use_custom_container | default(false)
tags: [install, container]
- name: Build custom Minecraft container image
containers.podman.podman_image:
name: "{{ minecraft.custom_image_name | default('minecraft-custom') }}"
build:
path: "{{ paths.base_dir }}/container/"
file: Containerfile
tag: "{{ minecraft.version }}"
state: present
when: minecraft.use_custom_container | default(false)
tags: [install, container]
tags: [install]
# -----------------------------------------------------------------------------
# Server Configuration
# -----------------------------------------------------------------------------
- name: Server Configuration
block:
- name: Deploy server.properties
ansible.builtin.template:
src: server.properties.j2
dest: "{{ paths.data_dir }}/server.properties"
owner: "{{ minecraft_user }}"
group: "{{ minecraft_user }}"
mode: "0644"
notify: restart minecraft
tags: [config]
- name: Deploy eula.txt
ansible.builtin.copy:
content: "eula=true\n"
dest: "{{ paths.data_dir }}/eula.txt"
owner: "{{ minecraft_user }}"
group: "{{ minecraft_user }}"
mode: "0644"
tags: [config]
- name: Deploy environment variables
ansible.builtin.template:
src: minecraft.env.j2
dest: "{{ paths.base_dir }}/minecraft.env"
owner: "{{ minecraft_user }}"
group: "{{ minecraft_user }}"
mode: "0600"
notify: restart minecraft
tags: [config]
- name: Deploy JVM options file
ansible.builtin.template:
src: jvm.opts.j2
dest: "{{ paths.data_dir }}/jvm.opts"
owner: "{{ minecraft_user }}"
group: "{{ minecraft_user }}"
mode: "0644"
when: minecraft.jvm_opts_file | default(false)
notify: restart minecraft
tags: [config]
tags: [config]
# -----------------------------------------------------------------------------
# Firewall Configuration
# -----------------------------------------------------------------------------
- name: Firewall Configuration
block:
- name: Ensure firewalld is running
ansible.builtin.systemd:
name: firewalld
state: started
enabled: true
tags: [firewall]
- name: Open Minecraft port
ansible.posix.firewalld:
port: "{{ minecraft.port }}/tcp"
permanent: true
immediate: true
state: enabled
tags: [firewall]
- name: Open RCON port
ansible.posix.firewalld:
port: "{{ minecraft.rcon_port }}/tcp"
permanent: true
immediate: true
state: enabled
when: minecraft.rcon_enabled
tags: [firewall]
- name: Open additional firewall ports
ansible.posix.firewalld:
port: "{{ item }}"
permanent: true
immediate: true
state: enabled
loop: "{{ firewall.additional_ports | default([]) }}"
tags: [firewall]
tags: [firewall]
# -----------------------------------------------------------------------------
# Systemd Service
# -----------------------------------------------------------------------------
- name: Systemd Service Configuration
block:
- name: Deploy systemd service unit
ansible.builtin.template:
src: minecraft.service.j2
dest: "{{ paths.systemd_dir }}/{{ minecraft.server_name }}.service"
owner: root
group: root
mode: "0644"
notify:
- reload systemd
- restart minecraft
tags: [service]
- name: Reload systemd daemon
ansible.builtin.systemd:
daemon_reload: true
tags: [service]
- name: Enable and start Minecraft service
ansible.builtin.systemd:
name: "{{ minecraft.server_name }}.service"
state: started
enabled: true
tags: [service]
tags: [service]
# -----------------------------------------------------------------------------
# Backup Configuration
# -----------------------------------------------------------------------------
- name: Backup Configuration
block:
- name: Deploy backup script
ansible.builtin.template:
src: backup.sh.j2
dest: "{{ paths.base_dir }}/backup.sh"
owner: "{{ minecraft_user }}"
group: "{{ minecraft_user }}"
mode: "0755"
when: backup.enabled
tags: [backup]
- name: Deploy restore script
ansible.builtin.template:
src: restore.sh.j2
dest: "{{ paths.base_dir }}/restore.sh"
owner: "{{ minecraft_user }}"
group: "{{ minecraft_user }}"
mode: "0755"
when: backup.enabled
tags: [backup]
- name: Create systemd timer for backups
ansible.builtin.template:
src: minecraft-backup.timer.j2
dest: "{{ paths.systemd_dir }}/{{ minecraft.server_name }}-backup.timer"
owner: root
group: root
mode: "0644"
when: backup.enabled
tags: [backup]
- name: Create systemd service for backups
ansible.builtin.template:
src: minecraft-backup.service.j2
dest: "{{ paths.systemd_dir }}/{{ minecraft.server_name }}-backup.service"
owner: root
group: root
mode: "0644"
when: backup.enabled
tags: [backup]
- name: Enable backup timer
ansible.builtin.systemd:
name: "{{ minecraft.server_name }}-backup.timer"
state: started
enabled: true
when: backup.enabled
tags: [backup]
tags: [backup]
# -----------------------------------------------------------------------------
# Health Check
# -----------------------------------------------------------------------------
- name: Health Check Configuration
block:
- name: Deploy health check script
ansible.builtin.template:
src: health-check.sh.j2
dest: "{{ paths.base_dir }}/health-check.sh"
owner: "{{ minecraft_user }}"
group: "{{ minecraft_user }}"
mode: "0755"
when: monitoring.enabled
tags: [monitoring]
- name: Create systemd timer for health checks
ansible.builtin.template:
src: minecraft-health.timer.j2
dest: "{{ paths.systemd_dir }}/{{ minecraft.server_name }}-health.timer"
owner: root
group: root
mode: "0644"
when: monitoring.enabled
tags: [monitoring]
- name: Create systemd service for health checks
ansible.builtin.template:
src: minecraft-health.service.j2
dest: "{{ paths.systemd_dir }}/{{ minecraft.server_name }}-health.service"
owner: root
group: root
mode: "0644"
when: monitoring.enabled
tags: [monitoring]
- name: Enable health check timer
ansible.builtin.systemd:
name: "{{ minecraft.server_name }}-health.timer"
state: started
enabled: true
when: monitoring.enabled
tags: [monitoring]
tags: [monitoring]
# -----------------------------------------------------------------------------
# Final Verification
# -----------------------------------------------------------------------------
- name: Final Verification
block:
- name: Wait for Minecraft server to start
ansible.builtin.uri:
url: "http://localhost:{{ minecraft.port }}"
return_content: false
status_code: [401, 200] # Minecraft responds to HTTP with 401
register: server_check
retries: 30
delay: 5
until: server_check.status in [401, 200]
ignore_errors: true
tags: [verify]
- name: Check service status
ansible.builtin.systemd:
name: "{{ minecraft.server_name }}.service"
register: service_status
tags: [verify]
- name: Display verification results
ansible.builtin.debug:
msg: |
Server Status: {{ 'Running' if service_status.status.ActiveState == 'active' else 'Stopped' }}
Service: {{ service_status.status.SubState }}
Port {{ minecraft.port }}: {{ 'Open' if server_check is succeeded else 'Closed' }}
tags: [verify]
tags: [verify]

View File

@@ -1,28 +0,0 @@
# Custom Minecraft Server Containerfile
# Deployed by Ansible - only used if minecraft.use_custom_container is true
# Base image
FROM itzg/minecraft-server:java{{ minecraft.java_version | default('21') }}
# Custom server type
ENV TYPE={{ minecraft.server_type | upper }}
ENV VERSION={{ minecraft.version }}
# Custom plugins/mods directory (uncomment if using mods)
# VOLUME /data/mods
# VOLUME /data/plugins
# VOLUME /data/config
# Custom entrypoint overrides
COPY server.properties /tmp/server.properties
RUN mkdir -p /data && cp /tmp/server.properties /data/server.properties
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
CMD curl -f http://localhost:{{ minecraft.port }} || exit 1
# Expose ports
EXPOSE {{ minecraft.port }}/tcp
{% if minecraft.rcon_enabled %}
EXPOSE {{ minecraft.rcon_port }}/tcp
{% endif %}

View File

@@ -1,96 +0,0 @@
#!/bin/bash
#
# Minecraft Server Backup Script
# Deployed by Ansible - {{ ansible_date_time.iso8601 if ansible_date_time is defined else 'managed' }}
#
# Creates rolling backups of the Minecraft world and data directories.
# Prunes old backups based on retention count.
#
set -euo pipefail
# Configuration
DATA_DIR="{{ paths.data_dir }}"
BACKUP_DIR="{{ paths.backup_dir }}"
RETENTION_COUNT="{{ backup.retention_count | default(10) }}"
COMPRESS="{{ backup.compress | default('true') }}"
SERVER_NAME="{{ minecraft.server_name }}"
# Timestamp for this backup
TIMESTAMP="$(date +%Y%m%d_%H%M%S)"
# Log function
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
# Safety checks
if [ ! -d "$DATA_DIR" ]; then
log "ERROR: Data directory $DATA_DIR does not exist"
exit 1
fi
if [ ! -d "$BACKUP_DIR" ]; then
log "Creating backup directory: $BACKUP_DIR"
mkdir -p "$BACKUP_DIR"
fi
# Create backup using tar
BACKUP_FILE="${BACKUP_DIR}/${SERVER_NAME}_${TIMESTAMP}"
log "Starting backup of $DATA_DIR..."
# Use atomic save if RCON is enabled
USE_RCON="{{ minecraft.rcon_enabled | default('false') }}"
if [ "$USE_RCON" = "true" ]; then
log "Using RCON for world save..."
# Send save-all command via rcon-cli or telnet
# This is a best-effort operation - fall back to tar if it fails
if command -v rcon-cli >/dev/null 2>&1; then
rcon-cli -a localhost -p {{ minecraft.rcon_port }} -P {{ minecraft.rcon_password }} save-all || true
sleep 5
elif command -v mcrcon >/dev/null 2>&1; then
mcrcon -H localhost -P {{ minecraft.rcon_port }} -p {{ minecraft.rcon_password }} "save-all" || true
sleep 5
else
log "RCON client not found, skipping save-all command (backup may be inconsistent)"
fi
fi
# Deterministic backup using tar
if [ "$COMPRESS" = "true" ]; then
BACKUP_FILE="${BACKUP_FILE}.tar.gz"
log "Creating compressed backup: $BACKUP_FILE"
tar --warning=no-file-changed -czf "$BACKUP_FILE" \
--exclude="$DATA_DIR/logs/*.gz" \
-C "$(dirname $DATA_DIR)" "$(basename $DATA_DIR)"
else
BACKUP_FILE="${BACKUP_FILE}.tar"
log "Creating uncompressed backup: $BACKUP_FILE"
tar --warning=no-file-changed -cf "$BACKUP_FILE" \
--exclude="$DATA_DIR/logs/*.gz" \
-C "$(dirname $DATA_DIR)" "$(basename $DATA_DIR)"
fi
log "Backup created successfully: $BACKUP_FILE"
# Prune old backups, retaining the most recent N
log "Pruning old backups (keeping $RETENTION_COUNT)..."
# Count backups (whether .tar or .tar.gz)
BACKUP_COUNT=$(ls "${BACKUP_DIR}/"${SERVER_NAME}_*.tar* 2>/dev/null | wc -l)
OVERFLOW=$((BACKUP_COUNT - RETENTION_COUNT))
if [ "$OVERFLOW" -gt 0 ]; then
# Remove oldest backups
ls -t "${BACKUP_DIR}/"${SERVER_NAME}_*.tar* 2>/dev/null | tail -n "$OVERFLOW" | while read -r old_backup; do
log "Removing old backup: $old_backup"
rm -f "$old_backup"
done
else
log "Backup count ($BACKUP_COUNT) is within retention limit ($RETENTION_COUNT)"
fi
log "Backup completed at $(date '+%Y-%m-%d %H:%M:%S')"
log "Current backups:"
ls -lh "${BACKUP_DIR}/" 2>/dev/null || true

View File

@@ -1,66 +0,0 @@
#!/bin/bash
#
# Minecraft Server Health Check Script
# Deployed by Ansible - managed file
#
# Checks that the Minecraft server is running and responding.
#
set -euo pipefail
SERVER_NAME="{{ minecraft.server_name }}"
PORT="{{ minecraft.port }}"
CONTAINER_NAME="{{ podman_container_name }}"
# Colors
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m'
log() {
echo -e "$(date '+%Y-%m-%d %H:%M:%S') $*"
}
# Check 1: Is the systemd service active?
SERVICE_STATUS=$(systemctl is-active "${SERVER_NAME}" 2>/dev/null || echo "inactive")
if [ "$SERVICE_STATUS" != "active" ]; then
log "${RED}[FAIL]${NC} Systemd service ${SERVER_NAME} is not active (status: ${SERVICE_STATUS})"
exit 1
fi
log "${GREEN}[OK]${NC} Systemd service ${SERVER_NAME} is active"
# Check 2: Is the container running?
if ! podman ps --format '{% raw %}{{.Names}} {{.Status}}{% endraw %}' | grep -q "${CONTAINER_NAME}"; then
log "${RED}[FAIL]${NC} Container ${CONTAINER_NAME} is not running"
exit 1
fi
log "${GREEN}[OK]${NC} Container ${CONTAINER_NAME} is running"
# Check 3: Is the Minecraft port responding?
# Minecraft responds to HTTP with 401 "It's a teapot" or similar
if curl -s -o /dev/null -m 5 http://localhost:${PORT} 2>/dev/null; then
log "${GREEN}[OK]${NC} Port ${PORT} is responding"
elif curl -s -o /dev/null -m 5 -w "%{http_code}" http://localhost:${PORT} 2>/dev/null | grep -q "401"; then
log "${GREEN}[OK]${NC} Port ${PORT} is responding (401 is expected for Minecraft)"
else
# Check if port is at least listening
if ss -tln | grep -q ":${PORT}"; then
log "${YELLOW}[WARN]${NC} Port ${PORT} is listening but not responding to HTTP (may be normal)"
else
log "${RED}[FAIL]${NC} Port ${PORT} is not listening"
exit 1
fi
fi
# Check 4: Check recent logs for errors
LOG_TAIL=$(journalctl -u "${SERVER_NAME}" --no-pager -n 50 2>/dev/null | grep -i "error\|fatal\|crash" | tail -n 5)
if [ -n "$LOG_TAIL" ]; then
log "${YELLOW}[WARN]${NC} Recent errors found in logs:"
echo "$LOG_TAIL"
else
log "${GREEN}[OK]${NC} No recent errors in logs"
fi
log "${GREEN}Health check completed successfully${NC}"
exit 0

View File

@@ -1,4 +0,0 @@
# JVM Options file for Minecraft server
# Deployed by Ansible - manage via java_args in group_vars/all.yml
{{ minecraft.java_args | default('-XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:MaxGCPauseMillis=200') }}

View File

@@ -1,16 +0,0 @@
# Systemd service for Minecraft backup
# Deployed by Ansible
[Unit]
Description=Minecraft Server Backup
Requires={{ minecraft.server_name }}.service
After={{ minecraft.server_name }}.service
[Service]
Type=oneshot
User={{ minecraft_user }}
Group={{ minecraft_user }}
ExecStart={{ paths.base_dir }}/backup.sh
# Timeout for large worlds
TimeoutStartSec=3600

View File

@@ -1,15 +0,0 @@
# Systemd timer for Minecraft backup
# Deployed by Ansible
[Unit]
Description=Minecraft Server Backup Timer
[Timer]
# Backup schedule (default: every 6 hours)
OnCalendar={{ backup.schedule | default('*-*-* */6:00:00') }}
# Run the backup immediately if the machine was powered off during the scheduled time
Persistent=true
[Install]
WantedBy=timers.target

View File

@@ -1,11 +0,0 @@
# Systemd service for Minecraft health checks
# Deployed by Ansible
[Unit]
Description=Minecraft Server Health Check
[Service]
Type=oneshot
User={{ minecraft_user }}
Group={{ minecraft_user }}
ExecStart={{ paths.base_dir }}/health-check.sh

View File

@@ -1,12 +0,0 @@
# Systemd timer for Minecraft health checks
# Deployed by Ansible
[Unit]
Description=Minecraft Server Health Check Timer
[Timer]
OnBootSec=2min
OnUnitActiveSec={{ monitoring.interval | default(300) }}s
[Install]
WantedBy=timers.target

View File

@@ -1,61 +0,0 @@
# Minecraft Server Environment Variables
# Deployed by Ansible - Managed file, do not edit manually
# Version and Server Type
TYPE={{ minecraft.server_type | upper }}
VERSION={{ minecraft.version }}
# Memory
MEMORY={{ minecraft.java_memory }}
{% if minecraft.java_args is defined %}
JVM_OPTS="{{ minecraft.java_args }}"
{% endif %}
# Network
PORT={{ minecraft.port }}
{% if minecraft.rcon_enabled %}
ENABLE_RCON=true
RCON_PORT={{ minecraft.rcon_port }}
RCON_PASSWORD={{ minecraft.rcon_password }}
{% else %}
ENABLE_RCON=false
{% endif %}
# Server Settings
MOTD={{ minecraft.motd }}
MAX_PLAYERS={{ minecraft.max_players }}
GAMEMODE={{ minecraft.game_mode }}
DIFFICULTY={{ minecraft.difficulty }}
{% if minecraft.online_mode %}
ONLINE_MODE=true
{% else %}
ONLINE_MODE=false
{% endif %}
VIEW_DISTANCE={{ minecraft.view_distance | default(10) }}
SIMULATION_DISTANCE={{ minecraft.simulation_distance | default(6) }}
{% if minecraft.whitelist_enabled %}
ENABLE_WHITELIST=true
{% else %}
ENABLE_WHITELIST=false
{% endif %}
{% if minecraft.enable_command_block %}
ENABLE_COMMAND_BLOCK=true
{% else %}
ENABLE_COMMAND_BLOCK=false
{% endif %}
{% if minecraft.max_world_size %}
MAX_WORLD_SIZE={{ minecraft.max_world_size }}
{% endif %}
{% if minecraft.spawn_protection %}
SPAWN_PROTECTION={{ minecraft.spawn_protection }}
{% endif %}
# Server name broadcast
SERVER_NAME={{ minecraft.server_name }}
# Make the container non-interactive
EULA=TRUE

View File

@@ -1,42 +0,0 @@
# Systemd unit for {{ minecraft.server_name }} Minecraft server running on Podman
# Deployed by Ansible - managed file
[Unit]
Description=Minecraft Server ({{ minecraft.server_name }}) - Podman Container
Documentation=https://hub.docker.com/r/itzg/minecraft-server
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User={{ minecraft_user }}
Group={{ minecraft_user }}
WorkingDirectory={{ paths.data_dir }}
# Pull the image if not present (idempotent)
ExecStartPre=/usr/bin/podman pull {{ podman.image }}:{{ podman.image_tag }}
# Stop existing container before starting
ExecStartPre=-/usr/bin/podman rm -f {{ podman_container_name }}
# Run the Minecraft server in a Podman container
ExecStart=/usr/bin/podman run --name {{ podman_container_name }} --replace --env-file {{ paths.base_dir }}/minecraft.env --volume {{ paths.data_dir }}:/data:Z {% if podman.extra_volumes is defined and podman.extra_volumes %}{% for vol in podman.extra_volumes %}--volume {{ vol }} {% endfor %}{% endif %}{% if podman.memory_limit is defined and podman.memory_limit %}--memory {{ podman.memory_limit }} {% endif %}{% if podman.cpu_shares is defined and podman.cpu_shares %}--cpu-shares {{ podman.cpu_shares }} {% endif %}{% if podman.network_mode == 'host' %}--network host {% else %}--publish {{ minecraft.port }}:{{ minecraft.port }}/tcp {% if minecraft.rcon_enabled %}--publish {{ minecraft.rcon_port }}:{{ minecraft.rcon_port }}/tcp {% endif %}{% endif %}{% for key, value in podman.labels.items() %}--label "{{ key }}={{ value }}" {% endfor %}--log-driver {{ podman.log_driver | default('journald') }} {{ podman.image }}:{{ podman.image_tag }}
# Restart if it crashes (systemd handles restart policy)
Restart=on-failure
RestartSec=15
# Basic process limits
LimitNOFILE=65536
LimitNPROC=4096
# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier={{ minecraft.server_name }}
# Allow container to manage its own namespaces/limits
NoNewPrivileges=false
ProtectSystem=false
[Install]
WantedBy=multi-user.target

View File

@@ -1,106 +0,0 @@
#!/bin/bash
#
# Minecraft Server Restore Script
# Deployed by Ansible - managed file
#
# Usage: ./restore.sh [path_to_backup_file]
# If no argument given, restores the most recent backup.
#
# WARNING: This will OVERWRITE the current world data!
#
set -euo pipefail
# Configuration
DATA_DIR="{{ paths.data_dir }}"
BACKUP_DIR="{{ paths.backup_dir }}"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
log() {
echo -e "$(date '+%Y-%m-%d %H:%M:%S') $*"
}
error() {
log "${RED}[ERROR]${NC} $*"
exit 1
}
warn() {
log "${YELLOW}[WARN]${NC} $*"
}
ok() {
log "${GREEN}[OK]${NC} $*"
}
# Determine which backup to restore
if [ $# -eq 1 ]; then
# Explicit file provided
RESTORE_FILE="$1"
if [ ! -f "$RESTORE_FILE" ]; then
# Try to find it in the backup dir
RESTORE_FILE="${BACKUP_DIR}/$1"
if [ ! -f "$RESTORE_FILE" ]; then
error "Backup file not found: $1 (looked in $BACKUP_DIR)"
fi
fi
else
# Find most recent backup
RESTORE_FILE=$(ls -t "${BACKUP_DIR}/"{{ minecraft.server_name }}_*.tar* 2>/dev/null | head -n1)
if [ -z "$RESTORE_FILE" ]; then
error "No backups found in $BACKUP_DIR"
fi
fi
log "Selected backup: $RESTORE_FILE"
# Double-check safety
warn "This will overwrite the current Minecraft world data in $DATA_DIR"
read -p "Are you sure you want to continue? (yes/no): " CONFIRM
if [ "$CONFIRM" != "yes" ]; then
log "Aborting restore."
exit 1
fi
# Stop the Minecraft server if running
if podman ps --format '{% raw %}{{.Names}}{% endraw %}' | grep -q '{{ podman_container_name }}'; then
warn "Stopping Minecraft server container..."
systemctl stop {{ minecraft.server_name }}
sleep 3
warn "Server stopped."
fi
# Create a temporary backup of current state (safety net)
TIMESTAMP="$(date +%Y%m%d_%H%M%S)"
SAFETY_BACKUP="${BACKUP_DIR}/pre_restore_${TIMESTAMP}.tar.gz"
log "Creating safety backup of current state: $SAFETY_BACKUP"
tar -czf "$SAFETY_BACKUP" -C "$(dirname $DATA_DIR)" "$(basename $DATA_DIR)"
# Extract the restore file
log "Restoring from: $RESTORE_FILE"
case "$RESTORE_FILE" in
*.tar.gz|*.tgz)
tar -xzf "$RESTORE_FILE" -C "$(dirname $DATA_DIR)"
;;
*.tar)
tar -xf "$RESTORE_FILE" -C "$(dirname $DATA_DIR)"
;;
*)
error "Unsupported archive format: $RESTORE_FILE"
;;
esac
ok "Restore completed successfully!"
# Restart the server
log "Restarting Minecraft server..."
systemctl start {{ minecraft.server_name }}
sleep 5
ok "Server restarted. Verify it's running: systemctl status {{ minecraft.server_name }}"
log "A safety backup of the pre-restore state was saved to: $SAFETY_BACKUP"

View File

@@ -1,59 +0,0 @@
#Minecraft server properties
#Deployed by Ansible - Manage via group_vars/all.yml
#Minecraft properties
enable-jmx-monitoring=false
rcon.port={{ minecraft.rcon_port | default(25575) }}
level-seed=
gamemode={{ minecraft.game_mode }}
enable-command-block={{ minecraft.enable_command_block | default('false') }}
enable-query=false
generator-settings={}
enforce-secure-profile={{ minecraft.enforce_secure_profile | default('true') }}
level-name=world
motd={{ minecraft.motd | default('A Minecraft Server') }}
query.port={{ minecraft.port | default(25565) }}
pvp=true
generate-structures=true
max-chained-neighbor-updates=1000000
difficulty={{ minecraft.difficulty }}
network-compression-threshold=256
max-tick-time=60000
require-resource-pack=false
use-native-transport=true
max-players={{ minecraft.max_players | default(20) }}
online-mode={{ minecraft.online_mode | default('true') }}
enable-status=true
allow-flight=false
initial-disabled-packs=
broadcast-rcon-to-ops=true
view-distance={{ minecraft.view_distance | default(10) }}
server-ip=
resource-pack-prompt=
allow-nether=true
server-port={{ minecraft.port | default(25565) }}
enable-rcon={{ minecraft.rcon_enabled | default('false') }}
sync-chunk-writes=true
op-permission-level=4
prevent-proxy-connections=false
hide-online-players=false
resource-pack=
entity-broadcast-range-percentage=100
simulation-distance={{ minecraft.simulation_distance | default(6) }}
rcon.password={{ minecraft.rcon_password | default('') }}
player-idle-timeout=0
force-gamemode=false
rate-limit=0
hardcore=false
white-list={{ minecraft.whitelist_enabled | default('false') }}
broadcast-console-to-ops=true
spawn-npcs=true
spawn-animals=true
log-ips=true
function-permission-level=2
level-type=minecraft\:normal
text-filtering-config=
spawn-monsters=true
enforce-whitelist=false
spawn-protection={{ minecraft.spawn_protection | default(16) }}
max-world-size={{ minecraft.max_world_size | default(29999984) }}

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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"

View 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

View 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 }}"'

View File

@@ -0,0 +1,2 @@
---
selinux_set_minecraft_fcontext: true

View 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

View 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

View 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

View 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

View 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 }}

View 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 %}

View File

@@ -1,62 +0,0 @@
---
# =============================================================================
# Minecraft Podman Deployment - Main Playbook
# =============================================================================
# Usage:
# ansible-playbook site.yml # Deploy to all minecraft servers
# ansible-playbook site.yml --limit external-apps # Deploy to specific host
# ansible-playbook site.yml --tags install # Only run install tasks
# ansible-playbook site.yml --tags config # Only run config tasks
# ansible-playbook site.yml --tags backup # Only run backup tasks
# ansible-playbook site.yml --tags update # Only run update tasks
- name: Deploy Minecraft Server on Podman
hosts: minecraft
become: true
gather_facts: true
pre_tasks:
- name: Display deployment info
ansible.builtin.debug:
msg: |
========================================
Minecraft Podman Deployment
========================================
Target: {{ inventory_hostname }}
Server: {{ minecraft.server_name }}
Type: {{ minecraft.server_type }}
Image: {{ podman.image }}
Port: {{ minecraft.port }}
Memory: {{ minecraft.java_memory }}
========================================
- name: Verify target is Fedora
ansible.builtin.assert:
that:
- ansible_distribution == "Fedora"
fail_msg: "This playbook is designed for Fedora. Detected: {{ ansible_distribution }}"
success_msg: "Fedora {{ ansible_distribution_version }} confirmed"
roles:
- role: minecraft-podman
tags:
- install
- config
- backup
- update
- always
post_tasks:
- name: Display completion message
ansible.builtin.debug:
msg: |
========================================
Deployment Complete!
========================================
Server IP: {{ ansible_default_ipv4.address | default(ansible_host) }}
Port: {{ minecraft.port }}
RCON: {{ 'Enabled' if minecraft.rcon_enabled else 'Disabled' }}
Status: {{ minecraft.server_name }}.service
========================================
Connect: {{ ansible_default_ipv4.address | default(ansible_host) }}:{{ minecraft.port }}
========================================

View File

@@ -1,112 +0,0 @@
---
# =============================================================================
# Minecraft Podman Deployment - Uninstall Playbook
# =============================================================================
# Usage:
# ansible-playbook uninstall.yml --limit external-apps
#
# WARNING: This will remove ALL Minecraft data, containers, and configuration
# from the target server. Run only if you're sure you want to clean up.
# =============================================================================
- name: Uninstall Minecraft Server from Podman
hosts: minecraft
become: true
gather_facts: true
tasks:
- name: Confirm uninstall
ansible.builtin.pause:
prompt: "WARNING: This will remove all Minecraft data from {{ inventory_hostname }}. Type 'yes' to continue"
register: confirm
when: not ansible_check_mode
- name: Abort if not confirmed
ansible.builtin.fail:
msg: "Uninstall aborted"
when:
- not ansible_check_mode
- confirm.user_input | lower != "yes"
- name: Stop Minecraft service
ansible.builtin.systemd:
name: "minecraft.service"
state: stopped
enabled: false
ignore_errors: true
- name: Stop backup timer
ansible.builtin.systemd:
name: "minecraft-backup.timer"
state: stopped
enabled: false
ignore_errors: true
- name: Stop health timer
ansible.builtin.systemd:
name: "minecraft-health.timer"
state: stopped
enabled: false
ignore_errors: true
- name: Remove systemd unit files
ansible.builtin.file:
path: "{{ item }}"
state: absent
loop:
- "/etc/systemd/system/minecraft.service"
- "/etc/systemd/system/minecraft-backup.timer"
- "/etc/systemd/system/minecraft-backup.service"
- "/etc/systemd/system/minecraft-health.timer"
- "/etc/systemd/system/minecraft-health.service"
ignore_errors: true
- name: Reload systemd daemon
ansible.builtin.systemd:
daemon_reload: true
- name: Stop and remove container
containers.podman.podman_container:
name: "minecraft"
state: absent
ignore_errors: true
register: container_remove
when: not ansible_check_mode
- name: Remove container (fallback with command)
ansible.builtin.command: podman rm -f minecraft
ignore_errors: true
when:
- not ansible_check_mode
- container_remove is failed
- name: Remove base directory
ansible.builtin.file:
path: "/opt/minecraft"
state: absent
ignore_errors: true
when: not ansible_check_mode
- name: Remove Minecraft system user
ansible.builtin.user:
name: "minecraft"
state: absent
remove: true
ignore_errors: true
when: not ansible_check_mode
- name: Close firewall ports
ansible.posix.firewalld:
port: "{{ item }}"
permanent: true
immediate: true
state: disabled
loop:
- "25565/tcp"
- "25575/tcp"
ignore_errors: true
tags: [firewall]
- name: Uninstall complete message
ansible.builtin.debug:
msg: "Minecraft server removed from {{ inventory_hostname }}"