feat(security): add two-factor authentication role
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 <crush@charm.land>
This commit is contained in:
@@ -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.
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user