Skip to content
Open
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
7 changes: 7 additions & 0 deletions defaults/main/mfa.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
openvpn_mfa_enabled: false
openvpn_mfa_issuer: "MFAtest"
openvpn_mfa_oathtool_package_name: oathtool
openvpn_mfa_script: oath.sh.j2
openvpn_mfa_oathsecrets_file: oath.secrets
mfa_new_users_map: []
18 changes: 16 additions & 2 deletions tasks/config.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
---
- name: Create user
user:
name: "{{ item }}"
with_items:
- "{{ openvpn_service_user }}"
- "{{ openvpn_management_client_user }}"

- name: Create openvpn config file
template:
src: server.conf.j2
Expand Down Expand Up @@ -42,13 +49,13 @@
path: "{{ openvpn_base_dir }}/auth"
state: directory
mode: 0755
when: openvpn_use_ldap
when: openvpn_use_ldap or openvpn_mfa_enabled

- name: Delete auth folder in openvpn dir
file:
path: "{{ openvpn_base_dir }}/auth"
state: absent
when: not openvpn_use_ldap
when: not openvpn_use_ldap and not openvpn_mfa_enabled

- name: Install LDAP config
template:
Expand All @@ -59,6 +66,13 @@
mode: "0644"
when: openvpn_use_ldap

- name: Install MFA script
template:
src: "{{ openvpn_mfa_script }}"
dest: "{{ openvpn_base_dir }}/auth/oath.sh"
mode: a+rx
when: openvpn_mfa_enabled

- name: Create log directory
file:
dest: "{{ openvpn_log_dir }}"
Expand Down
14 changes: 14 additions & 0 deletions tasks/gen_oath_secret.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
- name: generate userhash for user
shell: head -10 /dev/urandom | sha256sum | cut -b 1-30
register: userhash

- name: generate secret for mfa authenticator
shell: oathtool --totp -v "{{ userhash.stdout }}" | grep Base32 | awk '{print $3}'
register: oathsecret_base32

- debug:
msg: "Send this to the user {{ client_username }}: {{ oathsecret_base32.stdout }}"

- name: create new var
set_fact:
mfa_new_users_map: '{{ mfa_new_users_map | default([]) + [{"username": client_username, "secret": userhash.stdout }] }}'
7 changes: 7 additions & 0 deletions tasks/install.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@
- "{{ openvpn_package_name }}"
- "{{ openssl_package_name }}"

- name: Install oathtool
package:
name: "{{ openvpn_mfa_oathtool_package_name }}"
state: present
when:
- openvpn_mfa_enabled

- name: Install LDAP plugin
become: true
package:
Expand Down
4 changes: 4 additions & 0 deletions tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,7 @@

- name: Configure OpenVPN server
import_tasks: config.yml

- name: Generate user MFA secrets
import_tasks: mfa.yml
when: clients is defined and openvpn_mfa_enabled
48 changes: 48 additions & 0 deletions tasks/mfa.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
- name: ensure oath secret file exists
copy:
content: ""
dest: "{{ openvpn_base_dir }}/{{ openvpn_mfa_oathsecrets_file }}"
force: no
owner: "{{ openvpn_service_user }}"
mode: 0700

- name: fetch clients from oath secrets file
read_csv:
path: "{{ openvpn_base_dir }}/{{ openvpn_mfa_oathsecrets_file }}"
dialect: unix
fieldnames: username,secret
delimiter: ':'
register: oath_secrets_db

- name: delete clients in db those are not in clients list
set_fact:
cleaned_db: '{{ oath_secrets_db.list | selectattr("username", "in", clients) | list }}'
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please prefix this with openvpn_mfa_


- name: check for newly added clients/mfa users
set_fact:
mfa_new_users: "{{ mfa_new_users | default([]) + [item] }}"
when: 'item not in {{ cleaned_db | map(attribute="username") | list }}'
with_items: "{{ clients }}"

- name: generate oath secrets entry for new clients
include_tasks: gen_oath_secret.yml
loop: "{{ mfa_new_users }}"
loop_control:
loop_var: client_username
when: mfa_new_users is defined

- name: merge new clients and existing client in db
set_fact:
clients_merged: "{{ cleaned_db + mfa_new_users_map }}"

- name: prepare list of username and secret to be written to secret file
set_fact:
username_list: '{{ clients_merged | map(attribute="username") | list }}'
secret_list: '{{ clients_merged | map(attribute="secret") | list }}'

- name: write lines to file
copy:
content: "{{ username_list | zip(secret_list) | map('join', ':') | join('\n') }}"
dest: "{{ openvpn_base_dir }}/{{ openvpn_mfa_oathsecrets_file }}"
owner: "{{ openvpn_service_uesr }}"
mode: 0700
Comment on lines +38 to +48
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be a template that loops over clients_merged instead?

6 changes: 6 additions & 0 deletions tasks/uninstall.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@
state: absent
when: openvpn_use_ldap

- name: Uninstall oathtool
package:
name: "{{ openvpn_mfa_oathtool_package_name }}"
state: absent
when: openvpn_mfa_enabled

- name: Terminate playbook
fail:
msg: "OpenVPN uninstalled, playbook stopped"
4 changes: 4 additions & 0 deletions templates/client.ovpn.j2
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,7 @@ key-direction 1
{% if openvpn_verify_cn|bool %}
verify-x509-name OpenVPN-Server-{{ inventory_hostname[:49] }} name
{% endif %}

{% if openvpn_mfa_enabled|bool %}
auth-user-pass
{% endif %}
36 changes: 36 additions & 0 deletions templates/oath.sh.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/bin/sh
#
# Sample script to verify MFA using oath-tool

passfile=$1

# Get the user/pass from the tmp file
user=$(head -1 $passfile)
pass=$(tail -1 $passfile)

# Find the entry in our oath.secrets file, ignore case
secret=$(grep -i -m 1 "$user:" {{ openvpn_mfa_oathsecrets_file }} | cut -d: -f2)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the path have {{ openvpn_base_dir }}/ as a prefix to make it absolute?


# Calculate the code we should expect
code=$(oathtool --totp $secret)

if [ "$code" = "$pass" ];
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$pass should be validated to be all numeric, 6 digits

then
exit 0
fi
Comment on lines +12 to +20
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is safe-ish. Only user gets interpolated and that has some character sanitization:

To protect against a client passing a maliciously formed username or password string, the username string must consist only of these characters: alphanumeric, underbar (''), dash ('-'), dot ('.'), or at ('@'). The password string can consist of any printable characters except for CR or LF. Any illegal characters in either the username or password string will be converted to underbar ('').

auth-user-pass-verify in https://openvpn.net/community-resources/reference-manual-for-openvpn-2-0/

I'd much prefer this to be in a script to keep the values as values instead of potentially executing them.


# See if we have password and MFA, or just MFA

echo "$pass" | grep -q -i :

if [ $? -eq 0 ];
then
realpass=$(echo "$pass" | cut -d: -f1)
mfatoken=$(echo "$pass" | cut -d: -f2)

# put code here to verify $realpass, the code below the if validates $mfatoken or $pass if false
# exit 0 if the password is correct, the exit below will deny access otherwise
fi

Comment on lines +21 to +34
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this used at all?

# If we make it here, auth hasn't succeeded, don't grant access
exit 1
3 changes: 3 additions & 0 deletions templates/server.conf.j2
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@ client-cert-not-required
{% endif %}
{% endif %}

{% if openvpn_mfa_enabled|bool %}
auth-user-pass-verify "{{ openvpn_base_dir }}/auth/oath.sh" via-file
{% endif %}
script-security {{ openvpn_script_security }}

{% if openvpn_script_up is defined %}
Expand Down