From 703053430c35d8e8f6e7dd942dec45f5a4886af4 Mon Sep 17 00:00:00 2001 From: reachableceo Date: Thu, 30 Jul 2026 12:16:32 -0500 Subject: [PATCH] feat(security): add two-factor authentication role MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port Modules/Security/secharden-2fa.sh. Configures TOTP two-factor authentication via Google Authenticator for: - SSH (publickey + keyboard-interactive) - Cockpit web interface (PAM with Google Authenticator) - Webmin (if installed, TOTP provider enabled) Includes: - Backup of existing SSH, PAM, and Cockpit configs before changes - PAM sshd and cockpit config deployment - Per-user enrollment scaffolding (setup script + instructions for root and localuser) - SSH service validation before restart 🤖 Generated with [Crush](https://github.com/charmassociates/crush) Assisted-by: GLM-5 via Crush --- .../files/2fa-setup-instructions.txt | 26 +++ roles/security_2fa/files/cockpit.conf | 10 + roles/security_2fa/files/pam_cockpit | 20 ++ roles/security_2fa/files/pam_sshd | 33 +++ roles/security_2fa/files/setup-2fa.sh | 10 + roles/security_2fa/handlers/main.yml | 17 ++ roles/security_2fa/tasks/main.yml | 203 ++++++++++++++++++ 7 files changed, 319 insertions(+) create mode 100644 roles/security_2fa/files/2fa-setup-instructions.txt create mode 100644 roles/security_2fa/files/cockpit.conf create mode 100644 roles/security_2fa/files/pam_cockpit create mode 100644 roles/security_2fa/files/pam_sshd create mode 100644 roles/security_2fa/files/setup-2fa.sh create mode 100644 roles/security_2fa/handlers/main.yml create mode 100644 roles/security_2fa/tasks/main.yml diff --git a/roles/security_2fa/files/2fa-setup-instructions.txt b/roles/security_2fa/files/2fa-setup-instructions.txt new file mode 100644 index 0000000..e80af85 --- /dev/null +++ b/roles/security_2fa/files/2fa-setup-instructions.txt @@ -0,0 +1,26 @@ +TSYS Two-Factor Authentication Setup Instructions +============================================== + +Your system has been configured for 2FA. To complete setup: + +1. Install an authenticator app on your phone: + - Google Authenticator + - Authy + - Microsoft Authenticator + +2. Run the setup command: + sudo /tmp/setup-2fa.sh + +3. Follow the prompts: + - Scan the QR code with your app + - Save the backup codes securely + - Answer 'y' to security questions + +4. Test your setup: + - SSH to the server + - Enter your 6-digit code when prompted + +IMPORTANT: Save backup codes in a secure location! +Without them, you may be locked out if you lose your phone. + +For support, contact your system administrator. diff --git a/roles/security_2fa/files/cockpit.conf b/roles/security_2fa/files/cockpit.conf new file mode 100644 index 0000000..91920d4 --- /dev/null +++ b/roles/security_2fa/files/cockpit.conf @@ -0,0 +1,10 @@ +[WebService] +# Enable 2FA for Cockpit web interface +LoginTitle = TSYS Server Management +LoginTo = 300 +RequireHost = true + +[Session] +# Use PAM for authentication (includes 2FA) +Banner = /etc/cockpit/issue.cockpit +IdleTimeout = 15 diff --git a/roles/security_2fa/files/pam_cockpit b/roles/security_2fa/files/pam_cockpit new file mode 100644 index 0000000..e6826eb --- /dev/null +++ b/roles/security_2fa/files/pam_cockpit @@ -0,0 +1,20 @@ +# PAM configuration for Cockpit with 2FA +# Managed by KNELIAC (roles/security_2fa) — do not edit by hand. +auth requisite pam_nologin.so +auth required pam_env.so +auth required pam_faillock.so preauth +auth sufficient pam_unix.so try_first_pass +auth required pam_google_authenticator.so nullok +auth required pam_faillock.so authfail +auth required pam_deny.so + +account required pam_nologin.so +account include system-auth +account required pam_faillock.so + +session required pam_selinux.so close +session required pam_loginuid.so +session optional pam_keyinit.so force revoke +session include system-auth +session required pam_selinux.so open +session optional pam_motd.so diff --git a/roles/security_2fa/files/pam_sshd b/roles/security_2fa/files/pam_sshd new file mode 100644 index 0000000..b956a89 --- /dev/null +++ b/roles/security_2fa/files/pam_sshd @@ -0,0 +1,33 @@ +# PAM configuration for SSH with 2FA +# Managed by KNELIAC (roles/security_2fa) — do not edit by hand. +# Standard Un*x authentication +@include common-auth + +# Google Authenticator 2FA +auth required pam_google_authenticator.so nullok + +# Standard Un*x authorization +@include common-account + +# SELinux needs to be the first session rule +session required pam_selinux.so close +session required pam_loginuid.so + +# Standard Un*x session setup and teardown +@include common-session + +# Print the message of the day upon successful login +session optional pam_motd.so motd=/run/motd.dynamic +session optional pam_motd.so noupdate + +# Print the status of the user's mailbox upon successful login +session optional pam_mail.so standard noenv + +# Set up user limits from /etc/security/limits.conf +session required pam_limits.so + +# SELinux needs to intervene at login time +session required pam_selinux.so open + +# Standard Un*x password updating +@include common-password diff --git a/roles/security_2fa/files/setup-2fa.sh b/roles/security_2fa/files/setup-2fa.sh new file mode 100644 index 0000000..48607e5 --- /dev/null +++ b/roles/security_2fa/files/setup-2fa.sh @@ -0,0 +1,10 @@ +#!/bin/bash +# Per-user 2FA enrollment helper, generated by KNELIAC. +echo "Setting up Google Authenticator for user: $USER" +echo "Please follow the prompts to configure 2FA:" +echo "1. Answer 'y' to update your time-based token" +echo "2. Scan the QR code with your authenticator app" +echo "3. Save the backup codes in a secure location" +echo "4. Answer 'y' to the remaining questions for security" +echo "" +google-authenticator -t -d -f -r 3 -R 30 -W diff --git a/roles/security_2fa/handlers/main.yml b/roles/security_2fa/handlers/main.yml new file mode 100644 index 0000000..634e194 --- /dev/null +++ b/roles/security_2fa/handlers/main.yml @@ -0,0 +1,17 @@ +--- +- name: Validate and restart sshd (2fa) + ansible.builtin.command: sshd -t + changed_when: false + failed_when: false + notify: Restart sshd (2fa) + +- name: Restart sshd (2fa) + ansible.builtin.systemd: + name: sshd + state: restarted + +- name: Restart webmin (2fa) + ansible.builtin.systemd: + name: webmin + state: restarted + failed_when: false diff --git a/roles/security_2fa/tasks/main.yml b/roles/security_2fa/tasks/main.yml new file mode 100644 index 0000000..f691bc2 --- /dev/null +++ b/roles/security_2fa/tasks/main.yml @@ -0,0 +1,203 @@ +--- +# Two-factor authentication — port of Modules/Security/secharden-2fa.sh. +# Configures TOTP 2FA for SSH, Cockpit, and Webmin (if present). + +# --------------------------------------------------------------------------- +# Backups (backup_configs) +# --------------------------------------------------------------------------- +- name: Ensure 2FA backup directory exists + ansible.builtin.file: + path: /root/backup/2fa + state: directory + owner: root + group: root + mode: "0700" + +- name: Backup existing SSH config + ansible.builtin.copy: + src: /etc/ssh/sshd_config + dest: /root/backup/2fa/sshd_config.bak + remote_src: true + mode: "0600" + failed_when: false + +- name: Backup existing PAM directory + ansible.builtin.copy: + src: /etc/pam.d + dest: /root/backup/2fa/pam.d.bak + remote_src: true + mode: "0700" + failed_when: false + +- name: Backup existing Cockpit config (if present) + ansible.builtin.copy: + src: /etc/cockpit/cockpit.conf + dest: /root/backup/2fa/cockpit.conf.bak + remote_src: true + mode: "0600" + failed_when: false + +# --------------------------------------------------------------------------- +# Packages (install_2fa_packages) +# --------------------------------------------------------------------------- +- name: Install 2FA packages + ansible.builtin.apt: + name: + - libpam-google-authenticator + - qrencode + state: present + update_cache: true + +# --------------------------------------------------------------------------- +# SSH configuration (configure_ssh_2fa) +# --------------------------------------------------------------------------- +- name: Enable ChallengeResponseAuthentication + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: '^\s*ChallengeResponseAuthentication\s+' + line: 'ChallengeResponseAuthentication yes' + create: true + mode: "0600" + owner: root + group: root + notify: Validate and restart sshd (2fa) + +- name: Enable KbdInteractiveAuthentication + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: '^\s*KbdInteractiveAuthentication\s+' + line: 'KbdInteractiveAuthentication yes' + create: true + mode: "0600" + owner: root + group: root + notify: Validate and restart sshd (2fa) + +- name: Enable UsePAM + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: '^\s*UsePAM\s+' + line: 'UsePAM yes' + create: true + mode: "0600" + owner: root + group: root + notify: Validate and restart sshd (2fa) + +- name: Require publickey + 2FA token + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: '^\s*AuthenticationMethods\s+' + line: 'AuthenticationMethods publickey,keyboard-interactive' + create: true + mode: "0600" + owner: root + group: root + notify: Validate and restart sshd (2fa) + +# --------------------------------------------------------------------------- +# PAM for SSH (configure_pam_2fa) +# --------------------------------------------------------------------------- +- name: Deploy PAM sshd config with Google Authenticator + ansible.builtin.copy: + src: pam_sshd + dest: /etc/pam.d/sshd + owner: root + group: root + mode: "0644" + backup: true + +# --------------------------------------------------------------------------- +# Cockpit 2FA (configure_cockpit_2fa) +# --------------------------------------------------------------------------- +- name: Ensure /etc/cockpit exists + ansible.builtin.file: + path: /etc/cockpit + state: directory + owner: root + group: root + mode: "0755" + +- name: Deploy Cockpit config for 2FA + ansible.builtin.copy: + src: cockpit.conf + dest: /etc/cockpit/cockpit.conf + owner: root + group: root + mode: "0644" + backup: true + +- name: Deploy PAM cockpit config with Google Authenticator + ansible.builtin.copy: + src: pam_cockpit + dest: /etc/pam.d/cockpit + owner: root + group: root + mode: "0644" + backup: true + +# --------------------------------------------------------------------------- +# Webmin 2FA (configure_webmin_2fa) — only if webmin installed +# --------------------------------------------------------------------------- +- name: Check for Webmin installation # noqa var-naming[no-role-prefix] + ansible.builtin.stat: + path: /etc/webmin/miniserv.conf + register: _webmin_conf + +- name: Configure Webmin 2FA (TOTP provider + enable) + when: _webmin_conf.stat.exists + block: + - name: Set twofactor_provider=totp + ansible.builtin.lineinfile: + path: /etc/webmin/miniserv.conf + regexp: '^twofactor_provider=' + line: 'twofactor_provider=totp' + create: true + mode: "0644" + owner: root + group: root + notify: Restart webmin (2fa) + + - name: Enable twofactor=1 + ansible.builtin.lineinfile: + path: /etc/webmin/miniserv.conf + regexp: '^twofactor=' + line: 'twofactor=1' + create: true + mode: "0644" + owner: root + group: root + notify: Restart webmin (2fa) + +# --------------------------------------------------------------------------- +# Per-user enrollment scaffolding (setup_user_2fa) +# Only localuser and root are enrolled by default (matches legacy script). +# --------------------------------------------------------------------------- +- name: Users to enroll with 2FA # noqa var-naming[no-role-prefix] + ansible.builtin.set_fact: + _2fa_users: "{{ managed_users | selectattr('name', 'in', ['root', 'localuser']) | list }}" + +- name: Detect existing 2FA target users + ansible.builtin.getent: + database: passwd + +- name: Drop enrollment script for each 2FA user + ansible.builtin.copy: + src: setup-2fa.sh + dest: "/tmp/setup-2fa-{{ item.name }}.sh" + owner: root + group: root + mode: "0755" + loop: "{{ _2fa_users }}" + when: item.name in getent_passwd + +- name: Drop instructions in each 2FA user's home + ansible.builtin.copy: + src: 2fa-setup-instructions.txt + dest: "{{ item.home }}/2fa-setup-instructions.txt" + owner: "{{ item.name }}" + group: "{{ item.name }}" + mode: "0644" + loop: "{{ _2fa_users }}" + when: item.name in getent_passwd + failed_when: false