Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 20 additions & 48 deletions roles/users_add/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -245,10 +245,6 @@

write_output "ℹ️ Last boot: $LAST_BOOT\n"

# Check system uptime
UPTIME=$(uptime | awk -F'up ' '{print $2}' | awk -F', load' '{print $1}')
write_output "ℹ️ Uptime: $UPTIME\n"

write_output "==============================\n"
write_output "\n"

Expand Down Expand Up @@ -397,61 +393,37 @@
secret="$base/$user"

# Already enrolled?
if [ -s "$secret" ]; then
exit 0
fi
[ -s "$secret" ] && exit 0

umask 077
mkdir -p "$base"
chmod 0700 "$base"

echo "==============================================================================="
echo "SECURITY REQUIREMENT: Two-Factor Authentication (2FA) Setup"
echo "==============================================================================="
echo "You must set up TOTP-based 2FA (Aegis, Google Authenticator, Authy, Bitwarden, etc.)"
echo "Please scan the QR code below with your authenticator app."
echo "Save the emergency scratch codes in a secure location."
echo ""

# Run enrollment with all options pre-configured
# -t: time-based TOTP
# -d: disallow token reuse (security)
# -f: force write without confirmation
# -r 3: rate limit to 3 attempts
# -R 30: rate limit window of 30 seconds
# -w 3: window of 3 codes (compensates for time skew)
# -s: secret file location
# -e 5: generate 5 emergency codes
# -Q none: suppress google-authenticator QR (we generate our own because it would not display)
# Pipe -1 to skip the code verification prompt (no TTY available in pam_exec)
echo "-1" | google-authenticator -t -d -f -r 3 -R 30 -w 3 -e 5 -Q none -s "$secret"

# Must exist and be non-empty, otherwise deny login
# Create the TOTP secret non-interactively. -1 skips the confirm prompt (no TTY
# here), -Q none suppresses the QR. Options: -t time-based, -d no reuse,
# -f force, -r 3/-R 30 rate limit, -w 3 skew window, -e 5 emergency codes.
echo "-1" | google-authenticator -t -d -f -r 3 -R 30 -w 3 -e 5 -Q none -s "$secret" >/dev/null 2>&1 || true

if [ ! -s "$secret" ]; then
echo ""
echo "2FA enrollment failed or was canceled. Login denied."
echo "2FA enrollment failed. Login denied."
exit 1
fi

chmod 0600 "$secret"

# Extract secret key and generate QR code ourselves using qrencode
secret_key=$(head -1 "$secret")
hostname=$(hostname)
otpauth_url="otpauth://totp/${user}@${hostname}?secret=${secret_key}&issuer=${hostname}"

echo ""
echo "Scan this QR code with your authenticator app:"
echo ""
echo "$otpauth_url" | qrencode -t ANSIUTF8
echo ""
echo "Or manually enter this secret key: $secret_key"
echo ""
echo "Your emergency scratch codes (save these securely):"
grep -E '^[0-9]{8}$' "$secret" | sed 's/^/ /'
echo ""
echo "2FA setup successful! Please use your TOTP app for future logins."
echo "==============================================================================="
host=$(hostname)
scratch=$(grep -E '^[0-9]{8}$' "$secret" | tr '\n' ' ')

# IMPORTANT: pam_exec emits ONE PAM message per output line, and SSH's
# keyboard-interactive conversation caps that at 32 (PAM_MAX_NUM_MSG); going
# over aborts the login with "sshpam_query: too many query messages". A compact
# QR (margin 1) plus the 3 text lines below stays well under that limit. If you
# extend this, keep TOTAL output lines under ~30.
echo "=== 2FA setup: ${user}@${host} -- scan the QR, then enter the 6-digit code ==="
printf 'otpauth://totp/%s@%s?secret=%s&issuer=%s' "$user" "$host" "$secret_key" "$host" \
| qrencode -t ANSIUTF8 -m 1
echo "Manual key: ${secret_key}"
echo "Backup codes: ${scratch}"

exit 0
when: enable_2fa | default(false)
Expand Down
31 changes: 31 additions & 0 deletions setup-playbook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,37 @@
- "Number of users to create: {{ SSH_USERLIST | length }}"
tags: [info, always]

# Debian cloud images ship apt sources using the "mirror+file" method
# (URIs: mirror+file:///etc/apt/mirrors/*.list). That method is killed by the
# APT seccomp sandbox this playbook enables (APT::Sandbox::Seccomp "1"), so apt
# update dies with "Method file has died unexpectedly". Point apt at the real
# mirrors instead, which works fine under the sandbox.
- name: Use direct Debian mirrors instead of the cloud mirror+file method
become: true
when: ansible_facts.os_family == "Debian"
tags: [apt, sources]
block:
- name: Find apt sources using the mirror+file method
ansible.builtin.find:
paths:
- /etc/apt/sources.list.d
patterns:
- "*.sources"
- "*.list"
contains: 'mirror\+file:///etc/apt/mirrors/'
read_whole_file: true
register: apt_mirror_sources

- name: Point apt at deb.debian.org directly
ansible.builtin.replace:
path: "{{ item.path }}"
regexp: 'mirror\+file:///etc/apt/mirrors/([\w-]+)\.list'
replace: 'https://deb.debian.org/\1'
loop: "{{ apt_mirror_sources.files }}"
loop_control:
label: "{{ item.path }}"
when: apt_mirror_sources.matched | int > 0

tasks:
# ===================================================================
# USER MANAGEMENT SECTION
Expand Down
Loading