initial opencode commit
This commit is contained in:
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