--- - name: Initial Server Setup - Debian 13 (Trixie) hosts: production become: yes gather_facts: yes vars: # User configuration deploy_user: "{{ ansible_user | default('deploy') }}" deploy_user_groups: ['sudo'] # docker group added after Docker installation # SSH configuration ssh_key_only_auth: false # Set to true AFTER SSH keys are properly configured ssh_disable_root_login: false # Set to true after deploy user is configured # Firewall configuration firewall_enable: false # Set to true after initial setup is complete firewall_ports: - { port: 22, proto: 'tcp', comment: 'SSH' } - { port: 80, proto: 'tcp', comment: 'HTTP' } - { port: 443, proto: 'tcp', comment: 'HTTPS' } - { port: 51820, proto: 'udp', comment: 'WireGuard' } # System packages system_base_packages: - curl - wget - git - vim - sudo - ufw - fail2ban - rsync tasks: - name: Display system information ansible.builtin.debug: msg: - "Distribution: {{ ansible_distribution }} {{ ansible_distribution_version }}" - "Hostname: {{ ansible_hostname }}" - "Deploy User: {{ deploy_user }}" # ======================================== # 1. System Updates # ======================================== - name: Check and wait for apt locks to be released ansible.builtin.shell: cmd: | for lock in /var/lib/dpkg/lock /var/lib/apt/lists/lock /var/cache/apt/archives/lock; do if [ -f "$lock" ]; then echo "Waiting for lock: $lock" count=0 while [ -f "$lock" ] && [ $count -lt 60 ]; do sleep 1 count=$((count + 1)) done if [ -f "$lock" ]; then echo "Warning: Lock still exists after 60s: $lock" else echo "Lock released: $lock" fi fi done changed_when: false failed_when: false timeout: 70 - name: Update apt cache ansible.builtin.shell: cmd: timeout 300 apt-get update -qq environment: DEBIAN_FRONTEND: noninteractive APT_LISTCHANGES_FRONTEND: none register: apt_update_result changed_when: apt_update_result.rc == 0 failed_when: apt_update_result.rc != 0 timeout: 300 - name: Display apt update result ansible.builtin.debug: msg: "apt update completed successfully" when: apt_update_result.rc == 0 - name: Show packages to be upgraded ansible.builtin.command: cmd: apt list --upgradable 2>/dev/null | tail -n +2 | wc -l register: packages_to_upgrade changed_when: false failed_when: false - name: Display upgrade information ansible.builtin.debug: msg: "Packages to upgrade: {{ packages_to_upgrade.stdout | default('0') | trim }}" - name: Upgrade system packages ansible.builtin.shell: cmd: timeout 600 apt-get upgrade -y -qq && apt-get autoremove -y -qq environment: DEBIAN_FRONTEND: noninteractive APT_LISTCHANGES_FRONTEND: none register: apt_upgrade_result changed_when: apt_upgrade_result.rc == 0 failed_when: apt_upgrade_result.rc != 0 timeout: 600 - name: Display apt upgrade result ansible.builtin.debug: msg: "apt upgrade completed: {{ 'Packages upgraded' if apt_upgrade_result.rc == 0 else 'Failed' }}" when: apt_upgrade_result.rc is defined # ======================================== # 2. Install Base Packages # ======================================== - name: Install base packages ansible.builtin.shell: cmd: timeout 300 apt-get install -y -qq {{ system_base_packages | join(' ') }} environment: DEBIAN_FRONTEND: noninteractive APT_LISTCHANGES_FRONTEND: none register: apt_install_result changed_when: apt_install_result.rc == 0 failed_when: apt_install_result.rc != 0 timeout: 300 - name: Display apt install result ansible.builtin.debug: msg: "apt install completed: {{ 'Packages installed/updated' if apt_install_result.rc == 0 else 'Failed' }}" when: apt_install_result.rc is defined # ======================================== # 3. Create Deploy User # ======================================== - name: Check if deploy user exists ansible.builtin.shell: cmd: timeout 5 getent passwd {{ deploy_user }} >/dev/null 2>&1 && echo "exists" || echo "not_found" register: deploy_user_check changed_when: false failed_when: false timeout: 10 - name: Create deploy user ansible.builtin.user: name: "{{ deploy_user }}" groups: "{{ deploy_user_groups }}" append: yes shell: /bin/bash create_home: yes when: - "'not_found' in deploy_user_check.stdout" - deploy_user != 'root' - name: Ensure deploy user has sudo access ansible.builtin.lineinfile: path: /etc/sudoers.d/deploy line: "{{ deploy_user }} ALL=(ALL) NOPASSWD: ALL" create: yes validate: 'visudo -cf %s' mode: '0440' when: deploy_user != 'root' # ======================================== # 4. SSH Configuration # ======================================== - name: Get deploy user home directory ansible.builtin.getent: database: passwd key: "{{ deploy_user }}" register: deploy_user_info when: deploy_user != 'root' ignore_errors: yes - name: Set deploy user home directory (root) ansible.builtin.set_fact: deploy_user_home: "/root" when: deploy_user == 'root' - name: Set deploy user home directory (from getent) ansible.builtin.set_fact: deploy_user_home: "{{ deploy_user_info.ansible_facts.getent_passwd[deploy_user][4] }}" when: - deploy_user != 'root' - deploy_user_info.ansible_facts.getent_passwd[deploy_user] is defined - name: Set deploy user home directory (fallback) ansible.builtin.set_fact: deploy_user_home: "/home/{{ deploy_user }}" when: deploy_user_home is not defined - name: Ensure .ssh directory exists ansible.builtin.file: path: "{{ deploy_user_home }}/.ssh" state: directory owner: "{{ deploy_user }}" group: "{{ deploy_user }}" mode: '0700' - name: Add SSH public key from control node ansible.builtin.authorized_key: user: "{{ deploy_user }}" state: present key: "{{ lookup('file', ansible_ssh_private_key_file | default('~/.ssh/production') + '.pub') }}" when: ansible_ssh_private_key_file is defined - name: Verify SSH key is configured before disabling password auth ansible.builtin.stat: path: "{{ deploy_user_home }}/.ssh/authorized_keys" register: ssh_key_file when: ssh_key_only_auth | bool - name: Configure SSH key-only authentication ansible.builtin.lineinfile: path: /etc/ssh/sshd_config regexp: "{{ item.regexp }}" line: "{{ item.line }}" backup: yes loop: - { regexp: '^#?PasswordAuthentication', line: 'PasswordAuthentication no' } - { regexp: '^#?PubkeyAuthentication', line: 'PubkeyAuthentication yes' } - { regexp: '^#?AuthorizedKeysFile', line: 'AuthorizedKeysFile .ssh/authorized_keys' } when: - ssh_key_only_auth | bool - ssh_key_file.stat.exists | default(false) notify: restart sshd - name: Disable root login (optional) ansible.builtin.lineinfile: path: /etc/ssh/sshd_config regexp: '^#?PermitRootLogin' line: 'PermitRootLogin no' backup: yes when: ssh_disable_root_login | bool notify: restart sshd # ======================================== # 5. Firewall Configuration # ======================================== # WICHTIG: Firewall wird erst am Ende konfiguriert, um SSH-Verbindung nicht zu unterbrechen - name: Check current UFW status ansible.builtin.command: cmd: ufw status | head -1 register: ufw_current_status changed_when: false failed_when: false when: firewall_enable | bool - name: Display current firewall status ansible.builtin.debug: msg: "Current firewall status: {{ ufw_current_status.stdout | default('Unknown') }}" when: firewall_enable | bool and ufw_current_status is defined - name: Ensure SSH port is allowed before configuring firewall ansible.builtin.command: cmd: ufw allow 22/tcp comment 'SSH - Allow before enabling firewall' when: - firewall_enable | bool - "'inactive' in (ufw_current_status.stdout | default(''))" ignore_errors: yes - name: Reset UFW to defaults (only if inactive) ansible.builtin.command: cmd: ufw --force reset when: - firewall_enable | bool - "'inactive' in (ufw_current_status.stdout | default(''))" changed_when: false - name: Set UFW default policies ansible.builtin.command: cmd: "ufw default {{ item.policy }} {{ item.direction }}" loop: - { policy: 'deny', direction: 'incoming' } - { policy: 'allow', direction: 'outgoing' } when: - firewall_enable | bool - "'inactive' in (ufw_current_status.stdout | default(''))" - name: Allow firewall ports (ensure SSH is first) ansible.builtin.command: cmd: "ufw allow {{ item.port }}/{{ item.proto }} comment '{{ item.comment }}'" loop: "{{ firewall_ports }}" when: - firewall_enable | bool - "'inactive' in (ufw_current_status.stdout | default(''))" register: ufw_rules changed_when: ufw_rules.rc == 0 - name: Enable UFW (only if inactive) ansible.builtin.command: cmd: ufw --force enable when: - firewall_enable | bool - "'inactive' in (ufw_current_status.stdout | default(''))" - name: Display UFW status ansible.builtin.command: cmd: ufw status verbose register: ufw_status changed_when: false - name: Show UFW status ansible.builtin.debug: msg: "{{ ufw_status.stdout_lines }}" # ======================================== # 6. Fail2ban Configuration # ======================================== - name: Ensure fail2ban is enabled and started ansible.builtin.systemd: name: fail2ban enabled: yes state: started when: "'fail2ban' in system_base_packages" # ======================================== # 7. System Configuration # ======================================== - name: Configure timezone ansible.builtin.timezone: name: Europe/Berlin - name: Display setup summary ansible.builtin.debug: msg: - "==========================================" - "Initial Server Setup Complete" - "==========================================" - "Deploy User: {{ deploy_user }}" - "SSH Key-only Auth: {{ ssh_key_only_auth }}" - "Firewall: {{ 'Enabled' if firewall_enable else 'Disabled' }}" - "Fail2ban: {{ 'Enabled' if 'fail2ban' in system_base_packages else 'Disabled' }}" - "==========================================" - "Next Steps:" - "1. Test SSH connection: ssh {{ deploy_user }}@{{ ansible_host }}" - "2. Install Docker: ansible-playbook playbooks/install-docker.yml" - "3. Deploy Infrastructure: ansible-playbook playbooks/setup-infrastructure.yml" - "==========================================" handlers: - name: restart sshd ansible.builtin.systemd: name: sshd state: restarted