1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
|
---
# ✅ Existing local file vault secret
# ✅ Existing local file no vault
# ✅ No local file vault secret
# ✅ No local file no vault
# Sets up google authenticator and integrates with login
# Ensure we have the user's group
- when: groupname is not defined
block:
- name: Get passwd DB entry for {{ username }}
ansible.builtin.getent:
database: passwd
key: "{{ username }}"
register: user_pw
- name: Get group DB entry for {{ username }}
ansible.builtin.getent:
database: group
key: "{{ user_pw.ansible_facts.getent_passwd[username][2] }}"
register: user_gid
- name: Set groupname fact to {{ user_gid.ansible_facts.getent_group.keys()|first }}
set_fact:
groupname: "{{ user_gid.ansible_facts.getent_group.keys()|first }}"
#
# block
# Install packages
- name: Install google authenticator packages
ansible.builtin.apt:
state: present
update_cache: true
install_recommends: no
name: "{{ google_auth_packages }}"
become: yes
#
# If google_auth_config is defined then use those values to build .google_authenticator etc.
#
- name: Check for existing /home/{{ username }}/.google_authenticator config
ansible.builtin.stat:
path: "/home/{{ username }}/.google_authenticator"
register: google_auth_config_local
# Only generate a new config if no existing local one
- when: not google_auth_config_local.stat.exists # No existing secret
block:
# Create new .google_authenticator from vault config if vault defined
- name: Create .google_authenticator file
ansible.builtin.copy:
content: "{{ google_auth_config | selectattr('name', 'equalto', inventory_hostname) | map(attribute='secret') | first }}"
dest: "/home/{{ username }}/.google_authenticator"
mode: '0400'
owner: "{{ username }}"
group: "{{ username }}"
when:
- google_auth_config != "NEW" # vault_google_auth_config exists
- google_auth_config | selectattr('name', 'equalto', inventory_hostname) | list | length > 0 # vault entry for THIS host exists
# If no vault config defined, or no entry defined for this hostname
- when: google_auth_config == "NEW" or google_auth_config | selectattr('name', 'equalto', inventory_hostname) | list | length == 0
block:
# Generate secret
- name: Generate secret for google authenticator
ansible.builtin.command: "/usr/bin/google-authenticator --time-based --disallow-reuse --label=email_{{ inventory_hostname }} --qr-mode=UTF8 --rate-limit=3 --rate-time=30 --secret=/home/{{ username }}/.google_authenticator --window-size=3 --force --quiet"
args:
creates: "/home/{{ username }}/.google_authenticator"
register: google_auth_create
# Set .google_authenticator to mode 400
- name: Set new secret file permissions
ansible.builtin.file:
path: "/home/{{ username }}/.google_authenticator"
owner: "{{ username }}"
group: "{{ groupname }}"
mode: '0400'
#
# block no vault config or no entry defined
#
# block not local config exists
#
# Now we deal with a .google_authernticator, regardless of whether it already existed
# or was newly created, or was created from a vault config
#
- name: Pulling in /home/{{ username }}/.google_authenticator
ansible.builtin.slurp:
src: "/home/{{ username }}/.google_authenticator"
register: google_auth_file
- name: Set google auth config fact
ansible.builtin.set_fact:
google_auth_config_mine: "{{ google_auth_file['content'] | b64decode }}"
- name: Parse TOTP variable
ansible.builtin.set_fact:
totp_lines: "{{ google_auth_config_mine.split('\n') | map('trim') | list }}"
- name: Filter valid lines
ansible.builtin.set_fact:
valid_lines: "{{ totp_lines | reject('search', '^\"') | list }}"
# Main secret must be 16 or 26 chars. Must be at least 5x scratch codes
- name: Validate secret and scratch codes
ansible.builtin.assert:
that:
- "valid_lines[0] is defined and valid_lines[0] | length in [16, 26] and valid_lines[0] is match('^[A-Z0-9]+$')"
- "valid_lines[1:] | select('match', '^[0-9]{8}$') | list | length >= 5"
fail_msg: "The TOTP variable does not meet the required structure."
success_msg: "The TOTP variable is valid."
# Capture secret key - GOOGLE_SECRET=$(head -1 .google_authenticator
- name: Extract Google Authenticator secret key
ansible.builtin.set_fact:
google_secret_key: "{{ valid_lines[0] }}"
# Capture scratch codes - this creates a var like
# google_scratch_codes:
# - 21528074
# - 86134509
# - 79251446
- name: Extract Google Authenticator scratch codes
ansible.builtin.set_fact:
google_scratch_codes: "{{ valid_lines | select('match', '^[0-9]{8}$') | list }}"
- name: Extract label for {{ inventory_hostname }} if it exists
ansible.builtin.set_fact:
google_auth_label: "{{ google_auth_config | selectattr('name', 'equalto', inventory_hostname) | map(attribute='label') | first }}"
when: >
google_auth_config | selectattr('name', 'equalto', inventory_hostname) | list | length > 0 and
(google_auth_config | selectattr('name', 'equalto', inventory_hostname) | first).get('label') is not none and
(google_auth_config | selectattr('name', 'equalto', inventory_hostname) | first).get('label') != ''
- name: Extract issuer for {{ inventory_hostname }} if it exists
ansible.builtin.set_fact:
google_auth_issuer: "{{ google_auth_config | selectattr('name', 'equalto', inventory_hostname) | map(attribute='issuer') | first }}"
when: >
google_auth_config | selectattr('name', 'equalto', inventory_hostname) | list | length > 0 and
(google_auth_config | selectattr('name', 'equalto', inventory_hostname) | first).get('issuer') is not none and
(google_auth_config | selectattr('name', 'equalto', inventory_hostname) | first).get('issuer') != ''
# Create QR code
- name: Create QR code for secret
ansible.builtin.command: "/usr/bin/qrencode -m 3 -t UTF8 otpauth://totp/{{ google_auth_label }}?secret={{ google_secret_key }}&Issuer={{ google_auth_issuer }}"
register: google_auth_qrcode
- debug:
var: google_secret_key
- debug:
var: google_scratch_codes
- debug:
msg: "{{ google_auth_qrcode.stdout }}"
- become: yes
block:
# Set pam to use google authenticator for ssh
# echo "auth required pam_google_authenticator.so" >> /etc/pam.d/sshd
# echo "auth required pam_google_authenticator.so nullok" >> /etc/pam.d/sshd
# The nullok allows regular login if ~/.google_authenticator doesn't exist
# Putting at beginning of file means it will ask for token FIRST, then password
# This prevents someone from being able to attempt passwords until/unless they have token
- name: Set pam to use google authenticator for ssh
ansible.builtin.lineinfile:
path: /etc/pam.d/sshd
insertafter: BOF
line: 'auth required pam_google_authenticator.so{% if google_auth_nullok %} nullok{% endif %}'
state: present
# Must have at least one SUCCESS answer - nullok makes sshd answer IGNORE
# Adding pam_permit to end ensure that sshd module will answer SUCCESS if nothing else does
# https://github.com/google/google-authenticator-libpam#nullok
- name: Set pam to use pam_permit if nullok is defined
ansible.builtin.lineinfile:
path: /etc/pam.d/sshd
insertafter: EOF
line: 'auth required pam_permit.so'
state: present
when: google_auth_nullok
- name: Modify sshd_config to use google authenticator
ansible.builtin.copy:
dest: /etc/ssh/sshd_config.d/70-google_auth.conf
content: |
#
# For google authenticator
#
ChallengeResponseAuthentication yes
- name: Modify sshd_config to force use of google authenticator
ansible.builtin.copy:
dest: /etc/ssh/sshd_config.d/71-google_auth.conf
content: |
#
# For google authenticator to force use of token always
#
PasswordAuthentication no
# Only when global google_auth_force is true OR specific inventory_hostname has force_auth: true
when: >
google_auth_force == true or
(google_auth_config | selectattr('name', 'equalto', inventory_hostname) | list | length > 0 and
(google_auth_config | selectattr('name', 'equalto', inventory_hostname) | first).get('force_auth') is true)
#
# block system file updates
|