move to recommended file and directory structure

This commit is contained in:
Tobias Petrich 2025-12-26 19:59:39 +01:00
parent 8af8eafaf7
commit 7c2c5e06b2
No known key found for this signature in database
GPG Key ID: 220BE847F99B1B62
25 changed files with 222 additions and 298 deletions

3
.gitignore vendored
View File

@ -22,3 +22,6 @@ bookstack-db.container
bookstack-srv.container bookstack-srv.container
sgnarva-srv.container sgnarva-srv.container
sgnarva-db.container sgnarva-db.container
ansible/inventories/production/host_vars/*/vars.yml
ansible/inventories/production/host_vars/*/vault.yml

View File

@ -1,31 +1,34 @@
# Ansible MicroOS VM setup # Ansible MicroOS VM setup
<!-- ## Description
Unfortunately, the devsec hardening role does not play well with MicroOS.
1. Install devsec hardening collection (Vaulted) Variables for accessing the host with specific credentials are stored in the host_vars,
```shell they are handled as specific for how each user accesses a specific host.
ansible-galaxy collection install devsec.hardening
(Vaulted) Variables for the services are stored in the group_vars,
they are shared between all administrators of the host.
## Requirements
Create vars and vault file for accessing the host following this structure.
Adjust the username, become method and password.
`inventories/production/host_vars/io/vars.yml`:
```yaml
---
ansible_user: tobias
ansible_become_method: sudo
``` ```
2. Create the inventory.txt file for the server
3. Run the hardening playbook. Does not run completely through because of MicroOS immutability. At some point, a PR properly supporting MicroOS could be opened to https://github.com/dev-sec/ansible-os-hardening `inventories/production/host_vars/io/vault.yml`:
```shell (create through `ansible-vault create vault.yml` with a strong password)
ansible-playbook -i inventory.txt hardening.yml ```yaml
---
ansible_become_pass: EXAMPLE
``` ```
-->
4. Run the custom_hardening playbook. This mostly sets SSH parameters to best practice values. ## Usage
```shell ```shell
ansible-playbook -i inventory.txt custom_hardening.yml ansible-playbook main.yml
```
5. Run the allow_privileged_ports_rootless playbook. This allows a rootless traefik container to use ports 80 and 443.
```shell
ansible-playbook -i inventory.txt allow_privileged_ports_rootless.yml
```
6. Run the deploy_services playbook. This creates groups and users for each service, creates a btrfs subvolume for data and copies the quadlet files to the correct location, then activates the service.
```shell
ansible-playbook -i inventory.txt deploy_services.yml
```
7. Run the deploy_traefik_config playbool. This copies the traefik configuration to the correct location.
```shell
ansible-playbook -i inventory.txt deploy_traefik_config.yml
``` ```

View File

@ -1,18 +0,0 @@
---
- name: Allow normal users to bind to port 80
hosts: all
become: yes
tasks:
- name: Set sysctl to allow normal users to bind to ports starting from 80
sysctl:
name: net.ipv4.ip_unprivileged_port_start
value: 80
state: present
reload: yes
- name: Verify the sysctl setting
command: sysctl net.ipv4.ip_unprivileged_port_start
register: sysctl_result
- debug:
msg: "net.ipv4.ip_unprivileged_port_start: {{ sysctl_result.stdout }}"

View File

@ -1,52 +0,0 @@
---
- name: Automate borg backup
hosts: all
become: yes
tasks:
- name: Check if Borg backup script exist
stat:
path: /usr/local/sbin/backup.bash
register: backup_status
- name: Check if Borg check script exists
stat:
path: /usr/local/sbin/check_backup.bash
register: check_status
- name: Synchronize Borg scripts
when: backup_status.stat.exists == False or check_status.stat.exists == False
synchronize:
src: ./borg_scripts/
dest: /usr/local/sbin/
rsync_opts:
- "--chown=root:root"
- "--chmod=0700"
- name: Create borg backup systemd service
copy:
content: |
[Unit]
Description=Borg backup
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/borg_backup.sh
User=root
Group=root
dest: /etc/systemd/system/borg_backup.service
- name: Create borg backup systemd timer
copy:
content: |
[Unit]
Description=Borg backup timer
[Timer]
OnCalendar=*-*-* 05:00:00
Persistent=true
[Install]
WantedBy=timers.target
dest: /etc/systemd/system/borg_backup.timer
- name: Start and enable borg backup timer
systemd:
name: borg_backup.timer
enabled: yes
state: started

View File

@ -1,17 +0,0 @@
---
- name: install commonly used programs
hosts: all
become: yes
tasks:
- name: install common programs with zypper and transactional-update
community.general.zypper:
pkg:
- borgbackup
- tmux
- lynis
- toolbox
state: present
register: zypper_result
- name: reboot if software needed to be installed
ansible.builtin.reboot:
when: zypper_result.changed

View File

@ -1,67 +0,0 @@
---
- name: Apply SSH best practices configuration
hosts: all
become: yes
tasks:
- name: Ensure the sshd_config.d directory exists
file:
path: /etc/ssh/sshd_config.d
state: directory
owner: root
group: root
mode: '0755'
- name: Create a configuration file to apply SSH best practices
copy:
content: |
# Disable password authentication
PasswordAuthentication no
# Disable challenge-response authentication
ChallengeResponseAuthentication no
# Allow root login
PermitRootLogin yes
# Disable empty passwords
PermitEmptyPasswords no
# Disable X11 forwarding
X11Forwarding no
# Use only protocol 2
Protocol 2
# Log more verbosely
LogLevel VERBOSE
# Keep-alive packets to ensure connection stability
TCPKeepAlive yes
ClientAliveInterval 60
ClientAliveCountMax 10
dest: /etc/ssh/sshd_config.d/best_practices.conf
owner: root
group: root
mode: '0644'
- name: Restart SSH service to apply changes
service:
name: sshd
state: restarted
- name: Verify SSH configuration settings
shell: "sshd -T"
register: ssh_config_result
- name: Check specific SSH settings
debug:
msg: "{{ ssh_config_result.stdout_lines | select('search', 'passwordauthentication no') | list }}\n
{{ ssh_config_result.stdout_lines | select('search', 'challengeresponseauthentication no') | list }}\n
{{ ssh_config_result.stdout_lines | select('search', 'permitrootlogin yes') | list }}\n
{{ ssh_config_result.stdout_lines | select('search', 'permitemptypasswords no') | list }}\n
{{ ssh_config_result.stdout_lines | select('search', 'x11forwarding no') | list }}\n
{{ ssh_config_result.stdout_lines | select('search', 'protocol 2') | list }}\n
{{ ssh_config_result.stdout_lines | select('search', 'loglevel verbose') | list }}\n
{{ ssh_config_result.stdout_lines | select('search', 'clientaliveinterval 60') | list }}\n
{{ ssh_config_result.stdout_lines | select('search', 'clientalivecountmax 3') | list }}\n
{{ ssh_config_result.stdout_lines | select('search', 'tcpkeepalive yes') | list }}"

View File

@ -1,11 +0,0 @@
---
- name: Copy Traefik configuration files to the server
hosts: all
become: yes
tasks:
- name: Synchronize Traefik configuration files
synchronize:
src: ./traefik_config/
dest: /var/vol/traefik/
rsync_opts:
- "--chown=traefik:traefik"

View File

@ -1,89 +0,0 @@
- name: Apply DevSec hardening
hosts: all
become: yes
vars:
sysctl_overwrite:
# Enable IPv4 traffic forwarding. Needed for containers.
net.ipv4.ip_forward: 1
os_security_users_allow:
- "change_user" # Ensure this user is allowed to avoid modifying /bin/su (does not work on read-only filesystems)
os_family: "Suse"
os_release: "Tumbleweed" # Treat MicroOS as Tumbleweed
os_version: "{{ ansible_distribution_version }}"
os_vars:
packages:
- sudo
- openssh
ignore_fs_types:
- squashfs
- iso9660
- vfat
auth_pam:
- common-password
- common-auth
- common-account
- common-session
pam_passwords:
- password requisite pam_pwquality.so retry=3
- password required pam_unix.so use_authtok remember=5 sha512 shadow
securetty: [console, tty1, tty2, tty3, tty4, tty5, tty6]
sshd:
package: openssh
service: sshd
config: /etc/ssh/sshd_config
kernel_modules_disabled:
- cramfs
- freevxfs
- jffs2
- hfs
- hfsplus
- squashfs
- udf
- vfat
auditd_package: audit # This is the correct package name for auditd in openSUSE
os_env_umask: "027" # Setting a default umask value
os_auth_uid_min: "1000" # Setting the minimum user ID for non-system users
os_auth_uid_max: "60000" # Setting the maximum user ID for non-system users
os_auth_gid_min: 1000
os_auth_gid_max: 60000
os_auth_sys_uid_min: "100" # Setting the minimum user ID for system users
os_auth_sys_uid_max: "499" # Setting the maximum user ID for system users
os_auth_sys_gid_min: 100
os_auth_sys_gid_max: 499
os_auth_sub_uid_min: 100000
os_auth_sub_uid_max: 600100000
os_auth_sub_uid_count: 65536
os_auth_sub_gid_min: 100000
os_auth_sub_gid_max: 600100000
os_auth_sub_gid_count: 65536
os_shadow_perms:
owner: root
group: shadow
mode: "0640"
os_passwd_perms:
owner: root
group: root
mode: "0644"
hidepid_option: "2" # allowed values: 0, 1, 2
os_mnt_boot_group: 'root'
os_mnt_boot_owner: 'root'
os_mnt_dev_group: 'root'
os_mnt_dev_owner: 'root'
os_mnt_dev_shm_group: 'root'
os_mnt_dev_shm_owner: 'root'
os_mnt_home_group: 'root'
os_mnt_home_owner: 'root'
os_mnt_run_group: 'root'
os_mnt_run_owner: 'root'
os_mnt_tmp_group: 'root'
os_mnt_tmp_owner: 'root'
os_mnt_var_group: 'root'
os_mnt_var_owner: 'root'
os_mnt_var_log_group: 'root'
os_mnt_var_log_owner: 'root'
os_mnt_var_log_audit_group: 'root'
os_mnt_var_log_audit_owner: 'root'
os_mnt_var_tmp_group: 'root'
os_mnt_var_tmp_owner: 'root'
roles:
- devsec.hardening.os_hardening

View File

@ -0,0 +1,13 @@
$ANSIBLE_VAULT;1.1;AES256;podman_hosts
34396465333337346339653661356338373861326337663939616531623866336233653963353739
6264653732373635336563333732303735653532393865350a313135343361633034623239643339
31326238316333326166366638623963653635623533623833333862646462333363353066663434
6536343138643462320a393638643763633433363861326139326536663439633566386664393964
37366533323633376436396431383231346438643136326138643565343239393734643662386232
31373630376164623663333361323531626165646236393732353031313636623434323931633434
33343834356237376264383064393135663435323134373561306166306561356431656434633834
66653039633833643930303331323236633532343731613137383835643338373364633834653264
34643861366665336638656261373531343233353735353435643431303835376635356331373437
38306537643730646131666665396466366161303165303539663438666461623335366532376666
34356161383031353939343531333062623064353865636437633436613334663866346163316664
32383134333464663133

View File

@ -0,0 +1,4 @@
podman_hosts:
hosts:
io:
ansible_host: io.rohrschacht.de

View File

@ -1,13 +1,12 @@
--- ---
- name: apply custom hardening for ssh - name: podman host setup
import_playbook: custom_hardening.yml hosts: podman_hosts
- name: install commonly used programs roles:
import_playbook: common_programs.yml - common
- name: allow privileged ports for rootless containers - hardening
import_playbook: allow_privileged_ports_rootless.yml - rootless_host
- name: deploy services - traefik
import_playbook: deploy_services.yml - backup
- name: deploy traefik configuration
import_playbook: deploy_traefik_config.yml #- name: deploy services
- name: automate backup # import_playbook: deploy_services.yml
import_playbook: automate_backup.yml

View File

@ -0,0 +1,48 @@
---
- name: Deploy backup.bash script
ansible.builtin.template:
src: backup.bash.j2
dest: /usr/local/sbin/backup.bash
owner: root
group: root
mode: '0700'
- name: Deploy check_backup.bash script
ansible.builtin.template:
src: check_backup.bash.j2
dest: /usr/local/sbin/check_backup.bash
owner: root
group: root
mode: '0700'
- name: Create borg backup systemd service
copy:
content: |
[Unit]
Description=Borg backup
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/borg_backup.sh
User=root
Group=root
dest: /etc/systemd/system/borg_backup.service
- name: Create borg backup systemd timer
copy:
content: |
[Unit]
Description=Borg backup timer
[Timer]
OnCalendar=*-*-* 05:00:00
Persistent=true
[Install]
WantedBy=timers.target
dest: /etc/systemd/system/borg_backup.timer
- name: Start and enable borg backup timer
systemd:
name: borg_backup.timer
enabled: yes
state: started

View File

@ -1,10 +1,10 @@
#!/bin/bash #!/bin/bash
# Configuration # Configuration
BACKUP_USER="your_remote_user" # Remote SSH username BACKUP_USER="{{ backup.user }}" # Remote SSH username
BACKUP_HOST="your_remote_host" # Remote SSH server BACKUP_HOST="{{ backup.host }}" # Remote SSH server
BACKUP_PATH="/path/to/remote/backup/folder" # Remote backup folder BACKUP_PATH="{{ backup.path }}" # Remote backup folder
BORG_PASSPHRASE="your_encryption_password" # Encryption password (in plain text) BORG_PASSPHRASE="{{ backup.passphrase }}" # Encryption password (in plain text)
BACKUP_NAME="backup-$(date +'%Y-%m-%d')" # Name of the backup archive BACKUP_NAME="backup-$(date +'%Y-%m-%d')" # Name of the backup archive
BACKUP_REPO="ssh://$BACKUP_USER@$BACKUP_HOST/$BACKUP_PATH" # Borg repository location BACKUP_REPO="ssh://$BACKUP_USER@$BACKUP_HOST/$BACKUP_PATH" # Borg repository location

View File

@ -1,10 +1,10 @@
#!/bin/bash #!/bin/bash
# Configuration # Configuration
BACKUP_USER="your_remote_user" # Remote SSH username BACKUP_USER="{{ backup.user }}" # Remote SSH username
BACKUP_HOST="your_remote_host" # Remote SSH server BACKUP_HOST="{{ backup.host }}" # Remote SSH server
BACKUP_PATH="/path/ro/remote/backup/folder" # Remote backup folder BACKUP_PATH="{{ backup.path }}" # Remote backup folder
BORG_PASSPHRASE="your_encryption_password" # Encryption password (in plain text) BORG_PASSPHRASE="{{ backup.passphrase }}" # Encryption password (in plain text)
BACKUP_NAME="backup-$(date +'%Y-%m-%d')" # Name of the backup archive BACKUP_NAME="backup-$(date +'%Y-%m-%d')" # Name of the backup archive
BACKUP_REPO="ssh://$BACKUP_USER@$BACKUP_HOST/$BACKUP_PATH" # Borg repository location BACKUP_REPO="ssh://$BACKUP_USER@$BACKUP_HOST/$BACKUP_PATH" # Borg repository location

View File

@ -0,0 +1,3 @@
---
- name: Install common programs
include_tasks: programs.yml

View File

@ -0,0 +1,13 @@
---
- name: install common programs with zypper and transactional-update
community.general.zypper:
pkg:
- borgbackup
- tmux
- lynis
- toolbox
state: present
register: zypper_result
- name: reboot if software needed to be installed
ansible.builtin.reboot:
when: zypper_result.changed

View File

@ -0,0 +1,6 @@
---
- name: Restart sshd
ansible.builtin.service:
name: sshd
state: restarted

View File

@ -0,0 +1,3 @@
---
- name: SSH hardening
include_tasks: ssh.yml

View File

@ -0,0 +1,59 @@
---
- name: Ensure the sshd_config.d directory exists
file:
path: /etc/ssh/sshd_config.d
state: directory
owner: root
group: root
mode: '0755'
- name: Create a configuration file to apply SSH best practices
copy:
content: |
# Disable password authentication
PasswordAuthentication no
# Disable challenge-response authentication
ChallengeResponseAuthentication no
# Allow root login
PermitRootLogin yes
# Disable empty passwords
PermitEmptyPasswords no
# Disable X11 forwarding
X11Forwarding no
# Use only protocol 2
Protocol 2
# Log more verbosely
LogLevel VERBOSE
# Keep-alive packets to ensure connection stability
TCPKeepAlive yes
ClientAliveInterval 60
ClientAliveCountMax 10
dest: /etc/ssh/sshd_config.d/best_practices.conf
owner: root
group: root
mode: '0644'
notify: Restart sshd
- name: Verify SSH configuration settings
shell: "sshd -T"
register: ssh_config_result
- name: Check specific SSH settings
debug:
msg: "{{ ssh_config_result.stdout_lines | select('search', 'passwordauthentication no') | list }}\n
{{ ssh_config_result.stdout_lines | select('search', 'challengeresponseauthentication no') | list }}\n
{{ ssh_config_result.stdout_lines | select('search', 'permitrootlogin yes') | list }}\n
{{ ssh_config_result.stdout_lines | select('search', 'permitemptypasswords no') | list }}\n
{{ ssh_config_result.stdout_lines | select('search', 'x11forwarding no') | list }}\n
{{ ssh_config_result.stdout_lines | select('search', 'protocol 2') | list }}\n
{{ ssh_config_result.stdout_lines | select('search', 'loglevel verbose') | list }}\n
{{ ssh_config_result.stdout_lines | select('search', 'clientaliveinterval 60') | list }}\n
{{ ssh_config_result.stdout_lines | select('search', 'clientalivecountmax 3') | list }}\n
{{ ssh_config_result.stdout_lines | select('search', 'tcpkeepalive yes') | list }}"

View File

@ -0,0 +1,3 @@
---
- name: Allow rootless ports for podman
include_tasks: rootless_ports.yml

View File

@ -0,0 +1,14 @@
---
- name: Set sysctl to allow normal users to bind to ports starting from 80
sysctl:
name: net.ipv4.ip_unprivileged_port_start
value: 80
state: present
reload: yes
- name: Verify the sysctl setting
command: sysctl net.ipv4.ip_unprivileged_port_start
register: sysctl_result
- debug:
msg: "net.ipv4.ip_unprivileged_port_start: {{ sysctl_result.stdout }}"

View File

@ -0,0 +1,7 @@
---
- name: Synchronize Traefik configuration files
synchronize:
src: ./
dest: /var/vol/traefik/
rsync_opts:
- "--chown=traefik:traefik"