├── .ansible-lint
├── molecule
├── shared
│ ├── requirements.yml
│ ├── collections.yml
│ ├── prepare.yml
│ └── converge.yml
├── ubuntu
│ └── molecule.yml
└── centos
│ └── molecule.yml
├── vars
├── empty.yml
├── RedHat.yml
├── main.yml
├── Debian.yml
├── Solaris.yml
└── compile_ldap_plugin.yml
├── tests
├── install-default
├── setup-centos
├── setup-ubuntu
└── test.yml
├── templates
├── client_ccd.j2
├── openvpn_logrotate.conf.j2
├── crl-cron.sh.j2
├── selinux_module.te.j2
├── ca.conf.j2
├── revoke.sh.j2
├── client.ovpn.j2
├── ldap.conf.j2
└── server.conf.j2
├── files
├── openssl-client.ext
├── openssl-server.ext
├── dh.pem
└── openssl-ca.ext
├── meta
└── main.yml
├── tasks
├── selinux.yml
├── uninstall.yml
├── cert_sync_detection.yml
├── ufw.yml
├── set_facts.yml
├── revocation.yml
├── iptables.yml
├── firewalld.yml
├── firewall.yml
├── main.yml
├── install.yml
├── client_keys.yml
├── compile_ldap_plugin.yml
├── config.yml
└── server_keys.yml
├── CONTRIBUTING.md
├── .github
└── workflows
│ └── molecule-test.yml
├── LICENSE
├── handlers
└── main.yml
├── CHANGELOG.md
├── .gitignore
├── CODE_OF_CONDUCT.md
├── defaults
└── main.yml
└── README.md
/.ansible-lint:
--------------------------------------------------------------------------------
1 | exclude_paths:
2 | - tests/test.yml
3 |
--------------------------------------------------------------------------------
/molecule/shared/requirements.yml:
--------------------------------------------------------------------------------
1 | ---
2 | roles: []
3 |
--------------------------------------------------------------------------------
/vars/empty.yml:
--------------------------------------------------------------------------------
1 | ---
2 | # This file intentionally does not define any variables.
3 |
--------------------------------------------------------------------------------
/tests/install-default:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -ev
3 | sudo docker pull ${DOCKER_TAG}
4 |
--------------------------------------------------------------------------------
/vars/RedHat.yml:
--------------------------------------------------------------------------------
1 | ---
2 | iptables_save_command: "iptables-save > /etc/sysconfig/iptables"
3 |
--------------------------------------------------------------------------------
/templates/client_ccd.j2:
--------------------------------------------------------------------------------
1 | # {{ ansible_managed }}
2 | {% for line in item.value -%}
3 | {{ line }}
4 | {% endfor -%}
5 |
--------------------------------------------------------------------------------
/vars/main.yml:
--------------------------------------------------------------------------------
1 | ---
2 | # vars file for openvpn
3 | openvpn_config_file: "openvpn_{{ openvpn_proto }}_{{ openvpn_port }}"
4 |
--------------------------------------------------------------------------------
/vars/Debian.yml:
--------------------------------------------------------------------------------
1 | ---
2 | iptables_save_command: /usr/sbin/netfilter-persistent save
3 | iptables_services_package_name: iptables
4 |
--------------------------------------------------------------------------------
/molecule/shared/collections.yml:
--------------------------------------------------------------------------------
1 | ---
2 | collections:
3 | - name: ansible.posix
4 | - name: community.docker
5 | - name: community.general
6 |
--------------------------------------------------------------------------------
/tests/setup-centos:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -ev
3 | sudo docker exec ${OS} yum -y install epel-release
4 | sudo docker exec ${OS} yum -y install ansible
5 |
--------------------------------------------------------------------------------
/templates/openvpn_logrotate.conf.j2:
--------------------------------------------------------------------------------
1 | ## {{ ansible_managed }}
2 | ##
3 |
4 | {{ openvpn_log_dir }}/{{ openvpn_log_file }} {
5 | {{ openvpn_logrotate_config }}
6 | }
7 |
--------------------------------------------------------------------------------
/vars/Solaris.yml:
--------------------------------------------------------------------------------
1 | ---
2 | openvpn_config_file: "openvpn"
3 | openvpn_base_dir: /opt/local/etc/openvpn
4 | openvpn_key_dir: /opt/local/etc/openvpn/keys
5 | openvpn_use_ldap: false
6 |
--------------------------------------------------------------------------------
/tests/setup-ubuntu:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -ev
3 | sudo docker exec ${OS} apt update
4 | sudo docker exec ${OS} apt -yq install python-pip libssl-dev libffi-dev python-dev
5 | sudo docker exec ${OS} pip install ansible --quiet
6 |
--------------------------------------------------------------------------------
/files/openssl-client.ext:
--------------------------------------------------------------------------------
1 | # X509 extensions for a client
2 |
3 | basicConstraints = CA:FALSE
4 | subjectKeyIdentifier = hash
5 | authorityKeyIdentifier = keyid,issuer:always
6 | extendedKeyUsage = clientAuth
7 | keyUsage = digitalSignature
8 |
9 |
--------------------------------------------------------------------------------
/molecule/shared/prepare.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Prepare
3 | hosts: all
4 | tasks:
5 | - name: Update Apt Cache
6 | ansible.builtin.apt:
7 | update_cache: true
8 | become: true
9 | when: ansible_os_family == "Debian"
10 |
--------------------------------------------------------------------------------
/files/openssl-server.ext:
--------------------------------------------------------------------------------
1 | # X509 extensions for a server
2 |
3 | basicConstraints = CA:FALSE
4 | subjectKeyIdentifier = hash
5 | authorityKeyIdentifier = keyid,issuer:always
6 | extendedKeyUsage = serverAuth
7 | keyUsage = digitalSignature,keyEncipherment
8 |
9 |
--------------------------------------------------------------------------------
/molecule/shared/converge.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Converge
3 | hosts: all
4 | become: true
5 | gather_facts: true
6 | vars:
7 | ci_build: true
8 | tasks:
9 | - name: Run aovpn.openvpn
10 | ansible.builtin.include_role:
11 | name: aovpn.openvpn
12 |
--------------------------------------------------------------------------------
/tests/test.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Test
3 | hosts: 127.0.0.1
4 | connection: local
5 | vars:
6 | ci_build: true
7 | openvpn_use_pregenerated_dh_params: true
8 | roles:
9 | - role: ansible-role-openvpn
10 | clients:
11 | - alpha
12 | - omega
13 |
--------------------------------------------------------------------------------
/templates/crl-cron.sh.j2:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | nextUpdate=$(date --date "$(openssl crl -in {{ openvpn_key_dir }}/ca-crl.pem -noout -nextupdate | cut -d'=' -f2)" +%s)
4 | now=$(date +%s)
5 |
6 | if [ $(( (nextUpdate - now) / 86400 )) -le 10 ]; then
7 | sh {{ openvpn_key_dir }}/revoke.sh
8 | fi
9 |
--------------------------------------------------------------------------------
/templates/selinux_module.te.j2:
--------------------------------------------------------------------------------
1 | module {{ openvpn_selinux_module }} 1.0;
2 |
3 | require {
4 | type openvpn_t;
5 | type unreserved_port_t;
6 | class udp_socket name_bind;
7 | class tcp_socket name_bind;
8 | }
9 |
10 | #============= openvpn_t ==============
11 | allow openvpn_t unreserved_port_t:udp_socket name_bind;
12 | allow openvpn_t unreserved_port_t:tcp_socket name_bind;
13 |
--------------------------------------------------------------------------------
/files/dh.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN DH PARAMETERS-----
2 | MIIBCAKCAQEA1N2TYCNX9NwobK4Tf2XJNhjqYdPTYSh2cFwVHYdszYq8LhDvS69x
3 | yoDfO+zHNjnSsv4TGVvzoLS02un0qB9uhKA8rbou41RGKMn5ZfZ0RWoWMDh6WKTX
4 | yMafDo6DInBtRiSCuxyH3FfXMBRoYhIwrNGsLVDQRpn6Yj6rBWXiixdZgW2PrVys
5 | hhxcvKX8nlIuoUrwpkwSGp75gtEqnqUhgPqfIL3cA1tVU8NUdoaf40u54ScbK6cD
6 | 8Puy41Kp6rY4lbSO5VHIRauzfimDHBpssDMIbdhxKAhugwdJtqSq+zPI2EKRenHX
7 | aLdKtAkBBCzUc+SgMd0fwLeUOGtGANdjswIBAg==
8 | -----END DH PARAMETERS-----
9 |
--------------------------------------------------------------------------------
/templates/ca.conf.j2:
--------------------------------------------------------------------------------
1 |
2 | [ ca ]
3 | default_ca = CA_default
4 |
5 | [ CA_default ]
6 | dir = {{ openvpn_key_dir }}
7 | certs = $dir/
8 | new_certs_dir = $dir/
9 | database = $dir/index.txt
10 | crlnumber = $dir/crl_number
11 | certificate = $dir/ca.crt
12 | private_key = $dir/ca-key.pem
13 | default_days = 3650
14 | default_crl_days = 30
15 | default_md = sha256
16 | preserve = no
17 |
18 | [ crl_ext ]
19 | authorityKeyIdentifier=keyid:always,issuer:always
20 |
21 |
--------------------------------------------------------------------------------
/files/openssl-ca.ext:
--------------------------------------------------------------------------------
1 | # X509 extensions for a ca
2 |
3 | # Note that basicConstraints will be overridden by Easy-RSA when defining a
4 | # CA_PATH_LEN for CA path length limits. You could also do this here
5 | # manually as in the following example in place of the existing line:
6 | #
7 | # basicConstraints = CA:TRUE, pathlen:1
8 |
9 | basicConstraints = CA:TRUE
10 | subjectKeyIdentifier = hash
11 | authorityKeyIdentifier = keyid:always,issuer:always
12 | keyUsage = cRLSign, keyCertSign
13 |
14 |
--------------------------------------------------------------------------------
/meta/main.yml:
--------------------------------------------------------------------------------
1 | ---
2 | galaxy_info:
3 | role_name: openvpn
4 | namespace: aovpn
5 | author: AOVPN Team
6 | description: Ansible role to install and configure openvpn.
7 |
8 | license: MIT
9 | min_ansible_version: "2.9"
10 |
11 | platforms:
12 | - name: EL
13 | versions:
14 | - "7"
15 | - "8"
16 | - name: Ubuntu
17 | versions:
18 | - focal
19 | - jammy
20 | galaxy_tags:
21 | - networking
22 | - openvpn
23 |
24 | dependencies: []
25 |
--------------------------------------------------------------------------------
/tasks/selinux.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: SELinux - check if module was loaded
3 | ansible.builtin.command: semodule --list-modules
4 | register: semodule_loaded
5 | changed_when: 'openvpn_selinux_module not in semodule_loaded.stdout'
6 | notify:
7 | - Build and install policy
8 |
9 | - name: SELinux - copy type enforcement file
10 | ansible.builtin.template:
11 | src: "selinux_module.te.j2"
12 | dest: /var/lib/selinux/{{ openvpn_selinux_module }}.te
13 | mode: "0644"
14 | notify:
15 | - Build and install policy
16 |
--------------------------------------------------------------------------------
/molecule/ubuntu/molecule.yml:
--------------------------------------------------------------------------------
1 | ---
2 | dependency:
3 | name: galaxy
4 | enabled: true
5 | options:
6 | role-file: molecule/shared/requirements.yml
7 | requirements-file: molecule/shared/collections.yml
8 | driver:
9 | name: docker
10 | lint: |
11 | ansible-lint
12 | platforms:
13 | - name: ubuntu2404
14 | image: geerlingguy/docker-ubuntu2404-ansible
15 | privileged: true
16 | pre_build_image: true
17 | cgroupns_mode: host
18 | command: ""
19 | volumes:
20 | - /sys/fs/cgroup:/sys/fs/cgroup:rw
21 | provisioner:
22 | name: ansible
23 | playbooks:
24 | converge: ../shared/converge.yml
25 | prepare: ../shared/prepare.yml
26 | verifier:
27 | name: ansible
28 |
--------------------------------------------------------------------------------
/molecule/centos/molecule.yml:
--------------------------------------------------------------------------------
1 | ---
2 | dependency:
3 | name: galaxy
4 | enabled: true
5 | options:
6 | role-file: molecule/shared/requirements.yml
7 | requirements-file: molecule/shared/collections.yml
8 | driver:
9 | name: docker
10 | lint: |
11 | ansible-lint
12 | platforms:
13 | - name: Rockylinux9
14 | image: geerlingguy/docker-rockylinux9-ansible:latest
15 | privileged: true
16 | pre_build_image: true
17 | cgroupns_mode: host
18 | command: ""
19 | volumes:
20 | - /sys/fs/cgroup:/sys/fs/cgroup:rw
21 | provisioner:
22 | name: ansible
23 | playbooks:
24 | converge: ../shared/converge.yml
25 | prepare: ../shared/prepare.yml
26 | verifier:
27 | name: ansible
28 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to ansible-role-openvpn
2 |
3 | ## How to contribute
4 |
5 | If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement".
6 |
7 | 1. Fork the Project
8 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
9 | 3. Push to the Branch (`git push origin feature/AmazingFeature`)
10 | 4. Open a Pull Request
11 |
12 | ## Code of Conduct
13 |
14 | We expect all contributors to follow our [Code of Conduct](CODE_OF_CONDUCT.md) when participating in our community.
15 |
16 | ## License
17 |
18 | aovpn/ansible-role-openvpn is released under the [MIT License](LICENSE). By contributing to this project, you agree to license your contributions under the same license.
19 |
--------------------------------------------------------------------------------
/tasks/uninstall.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Disable openvpn auto-start & start
3 | ansible.builtin.service:
4 | name: "{{ openvpn_service_name }}"
5 | enabled: false
6 | state: stopped
7 |
8 | - name: Wipe out config directory
9 | ansible.builtin.file:
10 | path: "{{ openvpn_base_dir }}"
11 | state: absent
12 |
13 | - name: Remove openvpn logrotate config file
14 | ansible.builtin.file:
15 | path: /etc/logrotate.d/openvpn.conf
16 | state: absent
17 |
18 | - name: Uninstall OpenVPN
19 | ansible.builtin.package:
20 | name: "{{ openvpn_package_name }}"
21 | state: absent
22 |
23 | - name: Uninstall LDAP plugin
24 | ansible.builtin.package:
25 | name: "{{ openvpn_ldap_plugin_package_name }}"
26 | state: absent
27 | when: openvpn_use_ldap
28 |
29 | - name: Terminate playbook
30 | ansible.builtin.fail:
31 | msg: "OpenVPN uninstalled, playbook stopped"
32 |
--------------------------------------------------------------------------------
/templates/revoke.sh.j2:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 |
4 | CADIR="{{ openvpn_key_dir }}"
5 |
6 | cd ${CADIR}
7 |
8 | gen_crl () {
9 | # regenerate the certificate revocation list
10 | openssl ca -gencrl -config ca.conf -out ca-crl.pem
11 | }
12 |
13 | revoke () {
14 | # revoke a client certificate
15 | openssl ca -config ca.conf -revoke "${1}"
16 | }
17 |
18 | crl_out () {
19 | # show certificate revocation list
20 | openssl crl -in ca-crl.pem -noout -text
21 | }
22 |
23 | crl_verify () {
24 | # verify that a certificate has been revoked
25 | temp_pem="${mktemp}"
26 |
27 | cat ca.crt ca-crl.pem > ${temp_pem}
28 |
29 | openssl verify -extended_crl -verbose -CAfile ${temp_pem} -crl_check "${1}"
30 |
31 | rm -f ${temp_pem}
32 | }
33 |
34 | gen_crl
35 |
36 | if [ "${1}" ]
37 | then
38 | revoke "${1}"
39 | gen_crl
40 | fi
41 |
42 |
43 | # vim: autoindent expandtab shiftwidth=2
44 |
--------------------------------------------------------------------------------
/vars/compile_ldap_plugin.yml:
--------------------------------------------------------------------------------
1 | ---
2 | compile_develop_packages:
3 | - autoconf
4 | - automake
5 | - glibc-devel
6 | - libtool
7 | - make
8 | - pkgconf
9 | - pkgconf-m4
10 | - pkgconf-pkg-config
11 | - openldap-devel
12 | - openvpn-devel
13 | - openssl-devel
14 | - gcc-objc
15 | - gcc-objc++
16 |
17 | re2c_version: 2.0.3
18 | re2c_bin_path: /usr/local/bin/re2c
19 |
20 | openvpn_auth_ldap_version: 2.0.4
21 | openvpn_auth_ldap_plugin_dir_path: "{{ (ansible_machine == 'x86_64') | ternary('/usr/lib64/openvpn/plugin', '/usr/lib/openvpn/plugin') }}"
22 | openvpn_auth_ldap_bin_path: "{{ openvpn_auth_ldap_plugin_dir_path }}/lib/openvpn-auth-ldap.so"
23 |
24 | compile_source_dir: /usr/local/src
25 |
26 | compile_cleanup_dev_packages: true
27 |
28 | gcc_objc_repo:
29 | base_url: https://dl.cloudsmith.io/public/csi/gcc/rpm/el/8/$basearch
30 | key: https://dl.cloudsmith.io/public/csi/gcc/cfg/gpg/gpg.7E04A007BA668C3C.key
31 |
--------------------------------------------------------------------------------
/.github/workflows/molecule-test.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: Molecule Test
3 |
4 | on:
5 | push:
6 | branches:
7 | - main
8 | pull_request:
9 | branches:
10 | - main
11 |
12 | jobs:
13 | lint:
14 | name: Lint
15 | runs-on: ubuntu-latest
16 | steps:
17 | - name: checkout
18 | uses: actions/checkout@v4
19 |
20 | - name: Run ansible lint
21 | uses: ansible/ansible-lint@v24.7.0
22 |
23 | molecule:
24 | needs:
25 | - lint
26 | runs-on: ubuntu-latest
27 | strategy:
28 | fail-fast: false
29 | matrix:
30 | scenario:
31 | - ubuntu
32 | - centos
33 | steps:
34 | - name: Checkout
35 | uses: actions/checkout@v4
36 | with:
37 | path: "${{ github.repository }}"
38 |
39 | - name: Molecule
40 | uses: gofrolist/molecule-action@v2
41 | with:
42 | molecule_command: converge
43 | molecule_args: --scenario-name ${{ matrix.scenario }}
44 | env:
45 | ANSIBLE_FORCE_COLOR: '1'
46 |
--------------------------------------------------------------------------------
/tasks/cert_sync_detection.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: "[cert sync] Get existing certs"
3 | ansible.builtin.find:
4 | paths: "{{ openvpn_key_dir }}"
5 | patterns: "*.csr"
6 | excludes: "server.csr"
7 | register: openvpn_existing_cert
8 |
9 | # 1. Get list of file from find module
10 | # 2. Extract path attribute from dict list
11 | # 3. Keep only basename
12 | # 4. Remove extension
13 | - name: "[cert sync] Create list of existing client with existing certs"
14 | ansible.builtin.set_fact:
15 | openvpn_existing_client: "{{ openvpn_existing_cert.files | map(attribute='path') | map('basename') | map('replace', '.csr', '') | sort }}"
16 | when: (openvpn_existing_cert.files | length) > 0
17 |
18 | # Make difference between 2 list to have only cert to revoke
19 | - name: "[cert sync] Create list of cert to revoke"
20 | ansible.builtin.set_fact:
21 | openvpn_cert_sync_revoke: "{{ (openvpn_existing_client | default([])) | difference(clients | sort) }}"
22 |
23 | - name: "[cert sync] Debug: Certs to revoke (skipped if none)"
24 | ansible.builtin.debug:
25 | msg: "Will revoke additional certs: {{ openvpn_cert_sync_revoke | join(', ') }}"
26 | when: openvpn_cert_sync_revoke | length > 0
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Nikolai Mishin, Alexander Sharov, Anastasiia Kozlova
4 | Copyright (c) 2019 Kyle Lexmond
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 |
--------------------------------------------------------------------------------
/tasks/ufw.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Start ufw service
3 | ansible.builtin.service:
4 | name: ufw
5 | enabled: true
6 | state: started
7 |
8 | - name: Enable ufw
9 | community.general.ufw:
10 | direction: incoming
11 | state: enabled
12 | policy: allow
13 |
14 | - name: Enable forwarding - ufw
15 | ansible.builtin.lineinfile:
16 | dest: /etc/default/ufw
17 | regexp: "^DEFAULT_FORWARD_POLICY="
18 | line: DEFAULT_FORWARD_POLICY="ACCEPT"
19 |
20 | - name: Allow incoming VPN connections - ufw
21 | community.general.ufw:
22 | direction: in
23 | proto: "{{ openvpn_proto }}"
24 | to_port: "{{ openvpn_port | string }}"
25 | rule: allow
26 |
27 | - name: Accept packets from VPN tunnel adaptor - ufw
28 | community.general.ufw:
29 | direction: in
30 | interface: tun0
31 | rule: allow
32 |
33 | - name: Setup nat table rules with MASQUERADE - ufw
34 | ansible.builtin.blockinfile:
35 | dest: /etc/ufw/before.rules
36 | state: present
37 | insertbefore: \*filter
38 | block: |
39 | # OpenVPN config
40 | *nat
41 | :POSTROUTING ACCEPT [0:0]
42 | -A POSTROUTING -s {{ openvpn_server_network }}/24 -j MASQUERADE
43 | COMMIT
44 | notify:
45 | - Restart ufw
46 |
--------------------------------------------------------------------------------
/tasks/set_facts.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Check systemd existence as Docker Guest
3 | ansible.builtin.stat:
4 | path: /bin/systemctl
5 | when: ansible_virtualization_role is defined and ansible_virtualization_type == "docker" and ansible_virtualization_role == "guest"
6 | register: docker_stat_result
7 |
8 | - name: Set systemd openvpn service name
9 | ansible.builtin.set_fact:
10 | openvpn_service_name: "openvpn@{{ openvpn_config_file }}.service"
11 | when: ansible_service_mgr == "systemd" or (docker_stat_result.stat is defined and docker_stat_result.stat.exists)
12 |
13 | # Separate OpenVPN into client and server for CentOS/Rocky/RHEL
14 | - name: Set service name for CentOS/Rocky/RHEL
15 | ansible.builtin.set_fact:
16 | openvpn_service_name: "openvpn-server@{{ openvpn_config_file }}.service"
17 | when:
18 | - ansible_distribution == "CentOS" or ansible_distribution == "Rocky" or ansible_distribution == "RedHat"
19 | - (ansible_distribution_version | int) >= 8
20 |
21 | - name: Set OpenVPN base path for CentOS/Rocky/RHEL
22 | ansible.builtin.set_fact:
23 | openvpn_base_dir: "/etc/openvpn/server"
24 | when:
25 | - ansible_distribution == "CentOS" or ansible_distribution == "Rocky" or ansible_distribution == "RedHat"
26 | - (ansible_distribution_version | int) >= 8
27 |
--------------------------------------------------------------------------------
/tasks/revocation.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Remove client config
3 | ansible.builtin.file:
4 | path: "{{ openvpn_ovpn_dir }}/{{ item }}-{{ inventory_hostname }}.ovpn"
5 | state: absent
6 | force: true
7 | with_items:
8 | - '{{ openvpn_revoke_these_certs }}'
9 | - '{{ openvpn_cert_sync_revoke | default([]) }}'
10 |
11 | - name: Revoke certificates
12 | ansible.builtin.command: sh revoke.sh {{ item }}.crt
13 | changed_when: true
14 | args:
15 | chdir: "{{ openvpn_key_dir }}"
16 | with_items:
17 | - '{{ openvpn_revoke_these_certs }}'
18 | - '{{ openvpn_cert_sync_revoke | default([]) }}'
19 |
20 | - name: Remove client key
21 | ansible.builtin.file:
22 | path: "{{ openvpn_key_dir }}/{{ item }}.key"
23 | state: absent
24 | force: true
25 | with_items:
26 | - '{{ openvpn_revoke_these_certs }}'
27 | - '{{ openvpn_cert_sync_revoke | default([]) }}'
28 |
29 | - name: Remove client csr
30 | ansible.builtin.file:
31 | path: "{{ openvpn_key_dir }}/{{ item }}.csr"
32 | state: absent
33 | force: true
34 | with_items:
35 | - '{{ openvpn_revoke_these_certs }}'
36 | - '{{ openvpn_cert_sync_revoke | default([]) }}'
37 |
38 | - name: Remove client cert
39 | ansible.builtin.file:
40 | path: "{{ openvpn_key_dir }}/{{ item }}.crt"
41 | state: absent
42 | force: true
43 | with_items:
44 | - '{{ openvpn_revoke_these_certs }}'
45 | - '{{ openvpn_cert_sync_revoke | default([]) }}'
46 |
--------------------------------------------------------------------------------
/templates/client.ovpn.j2:
--------------------------------------------------------------------------------
1 | client
2 |
3 | tls-client
4 | auth {{ openvpn_auth_alg }}
5 | cipher {{ openvpn_cipher }}
6 | remote-cert-tls server
7 | {% if openvpn_use_1_3_tls | bool %}
8 | tls-version-min 1.3
9 | {% else %}
10 | tls-version-min 1.2
11 | {% endif %}
12 |
13 | proto {{ openvpn_proto }}
14 | remote {{ openvpn_server_hostname }} {{ openvpn_port }}
15 | dev tun
16 |
17 | resolv-retry {{ openvpn_resolv_retry }}
18 | nobind
19 | keepalive {{ openvpn_keepalive_ping }} {{ openvpn_keepalive_timeout }}
20 | {% if openvpn_compression is not undefined and openvpn_compression != "" %}
21 | compress {{ openvpn_compression }}
22 | {% endif %}
23 | persist-key
24 | persist-tun
25 | verb 3
26 |
27 | {% if openvpn_use_ldap %}
28 | auth-user-pass
29 | {% endif %}
30 |
31 | {% for option in openvpn_addl_client_options %}
32 | {{ option }}
33 | {% endfor %}
34 |
35 | route-method exe
36 | route-delay 2
37 | {% if openvpn_client_register_dns %}
38 | register-dns
39 | {% endif %}
40 |
41 | {% if tls_auth_required %}
42 | key-direction 1
43 | {% endif %}
44 |
45 | {{ ca_cert.content|b64decode }}
46 |
47 |
48 | {% if tls_auth_required %}
49 |
50 | {{ tls_auth.content|b64decode }}
51 |
52 | {% endif %}
53 |
54 |
55 | {{ item.0.content|b64decode }}
56 |
57 |
58 |
59 | {{ item.1.content|b64decode }}
60 |
61 |
62 | {% if openvpn_verify_cn|bool %}
63 | verify-x509-name OpenVPN-Server-{{ inventory_hostname[:49] }} name
64 | {% endif %}
65 |
--------------------------------------------------------------------------------
/tasks/iptables.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Install iptables-persistent (Debian/Ubuntu)
3 | ansible.builtin.package:
4 | name: "{{ iptables_persistent_package_name }}"
5 | state: present
6 | when: ansible_os_family == "Debian"
7 |
8 | - name: Allow VPN forwarding - input - iptables
9 | ansible.builtin.iptables:
10 | chain: FORWARD
11 | source: "{{ openvpn_server_network }}/24"
12 | jump: ACCEPT
13 | action: insert
14 | comment: "Allow VPN forwarding for inbound traffic"
15 | notify: "save iptables"
16 |
17 | - name: Allow VPN forwarding - output - iptables
18 | ansible.builtin.iptables:
19 | chain: FORWARD
20 | destination: "{{ openvpn_server_network }}/24"
21 | jump: ACCEPT
22 | action: insert
23 | comment: "Allow VPN forwarding for outbound traffic"
24 | notify: "save iptables"
25 |
26 | - name: Allow incoming VPN connections - iptables
27 | ansible.builtin.iptables:
28 | chain: INPUT
29 | protocol: "{{ openvpn_proto }}"
30 | destination_port: "{{ openvpn_port }}"
31 | jump: ACCEPT
32 | action: insert
33 | comment: "Allow incoming VPN connection"
34 | notify: "save iptables"
35 |
36 | - name: Accept packets from VPN tunnel adaptor - iptables
37 | ansible.builtin.iptables:
38 | chain: INPUT
39 | in_interface: tun0
40 | jump: ACCEPT
41 | action: insert
42 | comment: "Accept packets from VPN tunnel adaptor"
43 | notify: "save iptables"
44 |
45 | - name: Perform NAT readdressing with MASQUERADE - iptables
46 | ansible.builtin.iptables:
47 | table: nat
48 | chain: POSTROUTING
49 | source: "{{ openvpn_server_network }}/24"
50 | jump: MASQUERADE
51 | action: insert
52 | comment: "Perform NAT readdressing"
53 | notify: "save iptables"
54 |
--------------------------------------------------------------------------------
/handlers/main.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Restart openvpn
3 | ansible.builtin.service:
4 | name: "{{ openvpn_service_name }}"
5 | state: restarted
6 | # Github Actions doesn't allow entrypoints, so PID 1 isn't an init system
7 | when: ansible_service_mgr != "tail"
8 |
9 | - name: Restart iptables
10 | ansible.builtin.service:
11 | name: "{{ iptables_service }}"
12 | state: restarted
13 |
14 | - name: Restart firewalld
15 | ansible.builtin.service:
16 | name: firewalld
17 | state: restarted
18 |
19 | - name: Restart ufw
20 | ansible.builtin.service:
21 | name: ufw
22 | state: restarted
23 |
24 | - name: Enable iptables
25 | ansible.builtin.systemd:
26 | name: "{{ iptables_service }}"
27 | state: started
28 | enabled: true
29 | when:
30 | - ansible_distribution != "Debian"
31 |
32 | - name: Save iptables rules (Debian/Ubuntu and CentOS/RHEL)
33 | ansible.builtin.shell:
34 | cmd: "{{ iptables_save_command }}" # noqa command-instead-of-shell
35 | register: openvpn_iptables_save
36 | changed_when:
37 | - openvpn_iptables_save.rc == 0
38 | failed_when:
39 | - openvpn_iptables_save.rc != 0
40 | when: ansible_os_family == 'Debian' or ansible_os_family == 'RedHat'
41 | listen: "save iptables"
42 |
43 | - name: Build and install policy
44 | ansible.builtin.command: "{{ item }}"
45 | register: openvpn_install_policy
46 | changed_when:
47 | - openvpn_install_policy.rc == 0
48 | failed_when:
49 | - openvpn_install_policy.rc != 0
50 | args:
51 | chdir: /var/lib/selinux
52 | with_items:
53 | - "checkmodule -M -m -o {{ openvpn_selinux_module }}.mod {{ openvpn_selinux_module }}.te"
54 | - "semodule_package -o {{ openvpn_selinux_module }}.pp -m {{ openvpn_selinux_module }}.mod"
55 | - "semodule -i {{ openvpn_selinux_module }}.pp"
56 |
--------------------------------------------------------------------------------
/templates/ldap.conf.j2:
--------------------------------------------------------------------------------
1 |
2 | # LDAP server URL
3 | URL {{ ldap.url }}
4 |
5 | {% if not ldap.anonymous_bind %}
6 | # Bind DN (If your LDAP server doesn't support anonymous binds)
7 | BindDN {{ ldap.bind_dn }}
8 | # Bind Password
9 | Password {{ ldap.bind_password }}
10 | {% endif %}
11 |
12 | # Network timeout (in seconds)
13 | Timeout 15
14 |
15 | # Enable Start TLS
16 | TLSEnable {{ ldap.tls_enable }}
17 |
18 | # Follow LDAP Referrals (anonymously)
19 | FollowReferrals no
20 |
21 | {% if ldap.tls_ca_cert_file is defined %}
22 | # TLS CA Certificate File
23 | TLSCACertFile {{ ldap.tls_ca_cert_file }}
24 | {% endif %}
25 |
26 | # TLS CA Certificate Directory
27 | TLSCACertDir /etc/ssl/certs
28 |
29 | # Client Certificate and key
30 | # If TLS client authentication is required
31 | {% if ldap.tls_cert_file is defined %}
32 | TLSCertFile {{ ldap.tls_cert_file }}
33 | {% endif %}
34 | {% if ldap.tls_key_file is defined %}
35 | TLSKeyFile {{ ldap.tls_key_file }}
36 | {% endif %}
37 |
38 | # Cipher Suite
39 | # The defaults are usually fine here
40 | # TLSCipherSuite ALL:!ADH:@STRENGTH
41 |
42 |
43 |
44 | # Base DN
45 | BaseDN "{{ ldap.base_dn }}"
46 |
47 | # User Search Filter
48 | SearchFilter "{{ ldap.search_filter }}"
49 |
50 | {% if ldap.require_group %}
51 | # Require Group Membership
52 | RequireGroup True
53 | # Add non-group members to a PF table (disabled)
54 | #PFTable ips_vpn_users
55 |
56 |
57 | BaseDN "{{ ldap.group_base_dn }}"
58 | SearchFilter "{{ ldap.group_search_filter }}"
59 | MemberAttribute uniqueMember
60 | # Add group members to a PF table (disabled)
61 | #PFTable ips_vpn_eng
62 |
63 | {% endif %}
64 |
65 |
--------------------------------------------------------------------------------
/tasks/firewalld.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Enable firewalld
3 | ansible.builtin.service:
4 | name: firewalld
5 | enabled: true
6 | masked: false
7 | state: started
8 |
9 | - name: Enable OpenVPN Port (firewalld)
10 | ansible.posix.firewalld:
11 | port: "{{ openvpn_port }}/{{ openvpn_proto }}"
12 | zone: "{{ firewalld_default_interface_zone }}"
13 | permanent: true
14 | immediate: true
15 | state: enabled
16 |
17 | - name: Set tun0 interface to internal
18 | ansible.posix.firewalld:
19 | interface: tun0
20 | zone: internal
21 | permanent: true
22 | immediate: true
23 | state: enabled
24 |
25 | - name: Set default interface to external
26 | ansible.posix.firewalld:
27 | interface: "{{ ansible_default_ipv4.interface }}"
28 | zone: "{{ firewalld_default_interface_zone }}"
29 | permanent: true
30 | immediate: true
31 | state: enabled
32 |
33 | - name: Enable masquerading on external zone
34 | ansible.posix.firewalld:
35 | masquerade: true
36 | zone: "{{ firewalld_default_interface_zone }}"
37 | permanent: true
38 | state: enabled
39 | # Workaround ansible issue: https://github.com/ansible/ansible/pull/21693
40 | # immediate: true
41 | notify:
42 | - Restart firewalld
43 |
44 | # workaround for --permanent not working on non-NetworkManager managed ifaces
45 | # https://bugzilla.redhat.com/show_bug.cgi?id=1112742
46 | - name: Check existence of ifcfg-{{ ansible_default_ipv4.interface }}
47 | ansible.builtin.stat:
48 | path: "/etc/sysconfig/network-scripts/ifcfg-{{ ansible_default_ipv4.interface }}"
49 | register: ifcfg
50 |
51 | - name: Persist default interface in ifcfg file
52 | ansible.builtin.lineinfile:
53 | dest: /etc/sysconfig/network-scripts/ifcfg-{{ ansible_default_ipv4.interface }}
54 | regexp: "^ZONE="
55 | line: "ZONE={{ firewalld_default_interface_zone }}"
56 | when: ifcfg.stat.exists
57 |
--------------------------------------------------------------------------------
/tasks/firewall.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Check for firewalld
3 | ansible.builtin.command: which firewall-cmd
4 | register: firewalld
5 | check_mode: false
6 | changed_when: false # Never report as changed
7 | failed_when: false
8 |
9 | - name: Check for ufw
10 | ansible.builtin.command: which ufw
11 | register: ufw
12 | check_mode: false
13 | changed_when: false # Never report as changed
14 | failed_when: false
15 |
16 | - name: Check for iptables
17 | ansible.builtin.command: which iptables
18 | register: iptables
19 | check_mode: false
20 | changed_when: false # Never report as changed
21 | failed_when: false
22 |
23 | - name: Debug firewalls
24 | ansible.builtin.debug:
25 | msg: "Firewalld: {{ firewalld.rc }}, iptables: {{ iptables.rc }}, ufw: {{ ufw.rc }}"
26 | tags:
27 | - never
28 | - debug
29 |
30 | - name: Fail on both firewalld & ufw
31 | ansible.builtin.fail:
32 | msg: "Both FirewallD and UFW are detected, firewall situation is unknown"
33 | when: openvpn_firewall == 'auto' and firewalld.rc == 0 and ufw.rc == 0
34 |
35 | - name: Installing iptables due to no firewall detected
36 | ansible.builtin.package:
37 | name: "{{ iptables_services_package_name }}"
38 | state: present
39 | register: iptables_installed
40 | when: firewalld.rc != 0 and ufw.rc != 0 and iptables.rc != 0
41 | notify: "Enable iptables"
42 |
43 | - name: Echo iptables_installed
44 | ansible.builtin.debug:
45 | msg: "{{ iptables_installed }}"
46 | tags:
47 | - never
48 | - debug
49 |
50 | - name: Add port rules (iptables)
51 | ansible.builtin.include_tasks: iptables.yml
52 | when: >-
53 | (openvpn_firewall == 'iptables')
54 | or
55 | (openvpn_firewall == 'auto' and firewalld.rc != 0 and ufw.rc != 0 and iptables.rc == 0)
56 | or
57 | (iptables_installed.stdout is defined)
58 |
59 | - name: Add port rules (firewalld)
60 | ansible.builtin.include_tasks: firewalld.yml
61 | when: >-
62 | (openvpn_firewall == 'firewalld')
63 | or
64 | (openvpn_firewall == 'auto' and firewalld.rc == 0 and ufw.rc != 0)
65 |
66 | - name: Add port rules (ufw)
67 | ansible.builtin.include_tasks: ufw.yml
68 | when: >-
69 | (openvpn_firewall == 'ufw')
70 | or
71 | (openvpn_firewall == 'auto' and firewalld.rc != 0 and ufw.rc == 0)
72 |
--------------------------------------------------------------------------------
/tasks/main.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Include vars for OpenVPN installation
3 | ansible.builtin.include_vars: "{{ item }}"
4 | with_first_found:
5 | - "../vars/{{ ansible_distribution }}{{ ansible_distribution_major_version }}.yml"
6 | - "../vars/{{ ansible_distribution }}.yml"
7 | - "../vars/{{ ansible_os_family }}.yml"
8 | - "../vars/empty.yml"
9 |
10 | - name: Set facts
11 | ansible.builtin.import_tasks: set_facts.yml
12 |
13 | - name: Uninstall OpenVPN
14 | ansible.builtin.import_tasks: uninstall.yml
15 | when: openvpn_uninstall
16 |
17 | - name: Install OpenVPN
18 | ansible.builtin.import_tasks: install.yml
19 | tags:
20 | - install
21 |
22 | - name: Copy or Generate server keys
23 | ansible.builtin.import_tasks: server_keys.yml
24 |
25 | # ignoreerrors is required for CentOS/RHEL 6
26 | # http://serverfault.com/questions/477718/sysctl-p-etc-sysctl-conf-returns-error
27 | - name: Enable ipv4 forwarding
28 | ansible.posix.sysctl:
29 | name: net.ipv4.ip_forward
30 | value: '1'
31 | ignoreerrors: true
32 | failed_when: false
33 | when: not ci_build
34 |
35 | - name: Enable ipv6 forwarding
36 | ansible.posix.sysctl:
37 | name: net.ipv6.conf.all.forwarding
38 | value: '1'
39 | ignoreerrors: true
40 | when: openvpn_server_ipv6_network is defined and not ci_build
41 |
42 | - name: Detect firewall type
43 | ansible.builtin.import_tasks: firewall.yml
44 | when:
45 | - not ci_build
46 | - manage_firewall_rules
47 | tags:
48 | - firewall
49 |
50 | - name: Configure SELinux
51 | ansible.builtin.import_tasks: selinux.yml
52 | when:
53 | - ansible_selinux.status == "enabled"
54 |
55 | - name: Compare existing certs against 'clients' variable
56 | ansible.builtin.import_tasks: cert_sync_detection.yml
57 | when: openvpn_sync_certs
58 | tags:
59 | - sync_certs
60 |
61 | - name: Generate client configs
62 | ansible.builtin.import_tasks: client_keys.yml
63 | when: clients is defined
64 |
65 | - name: Generate revocation list and clean up
66 | ansible.builtin.import_tasks: revocation.yml
67 | when: >-
68 | (openvpn_revoke_these_certs is defined)
69 | or
70 | (openvpn_sync_certs and cert_sync_certs_to_revoke.stdout_lines | length > 0)
71 |
72 | - name: Configure OpenVPN server
73 | ansible.builtin.import_tasks: config.yml
74 |
--------------------------------------------------------------------------------
/tasks/install.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Install EPEL for CentOS
3 | ansible.builtin.package:
4 | name: "{{ epel_package_name }}"
5 | state: present
6 | when: ansible_distribution == "CentOS" or ansible_distribution=="Rocky"
7 |
8 | - name: Install EPEL for RHEL
9 | ansible.builtin.dnf:
10 | name: https://dl.fedoraproject.org/pub/epel/epel-release-latest-{{ ansible_distribution_major_version }}.noarch.rpm
11 | state: present
12 | disable_gpg_check: true
13 | when: ansible_distribution=="RedHat" and ansible_distribution_major_version == "8"
14 |
15 | - name: Enable extra repos for RHEL 8
16 | community.general.rhsm_repository:
17 | name: "codeready-builder-for-rhel-8-{{ ansible_architecture }}-rpms"
18 | state: enabled
19 | when: ansible_distribution=="RedHat" and ansible_distribution_major_version == "8"
20 |
21 | - name: Enable extra repos for RHEL 7
22 | community.general.rhsm_repository:
23 | name: "{{ item }}"
24 | state: enabled
25 | with_items:
26 | - "rhel-*-optional-rpms"
27 | - "rhel-*-extras-rpms"
28 | - "rhel-ha-for-rhel-*-server-rpms"
29 | when: ansible_distribution=="RedHat" and ansible_distribution_major_version == "7"
30 |
31 | - name: Update repositories for Debian/Ubuntu
32 | ansible.builtin.apt:
33 | update_cache: true
34 | cache_valid_time: 3600
35 | when: ansible_distribution == "Debian" or ansible_distribution=="Ubuntu"
36 |
37 | - name: Install openvpn
38 | ansible.builtin.package:
39 | name: "{{ item }}"
40 | state: present
41 | with_items:
42 | - "{{ openvpn_package_name }}"
43 | - "{{ openssl_package_name }}"
44 |
45 | - name: Install LDAP plugin
46 | become: true
47 | ansible.builtin.package:
48 | name: "{{ openvpn_ldap_plugin_package_name }}"
49 | state: present
50 | when:
51 | - openvpn_use_ldap
52 | - ansible_distribution == "CentOS" and ansible_distribution_major_version != "8" or ansible_distribution != "CentOS"
53 |
54 | - name: Compile LDAP plugin
55 | ansible.builtin.include_tasks: compile_ldap_plugin.yml
56 | when:
57 | - openvpn_use_ldap
58 | - ansible_distribution == "CentOS" and ansible_distribution_major_version == "8"
59 |
60 | # RHEL has the group 'nobody', 'Debian/Ubuntu' have 'nogroup'
61 | # standardize on 'nogroup'
62 | - name: Ensure group 'nogroup' is present
63 | ansible.builtin.group:
64 | name: nogroup
65 | state: present
66 | system: true
67 |
--------------------------------------------------------------------------------
/tasks/client_keys.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Create openvpn ovpn file directory
3 | ansible.builtin.file:
4 | path: "{{ openvpn_ovpn_dir }}"
5 | state: directory
6 | mode: "0755"
7 |
8 | - name: Copy openssl client extensions
9 | ansible.builtin.copy:
10 | src: openssl-client.ext
11 | dest: "{{ openvpn_key_dir }}"
12 | owner: root
13 | group: root
14 | mode: "0400"
15 |
16 | - name: Generate client key
17 | ansible.builtin.command: >-
18 | openssl req -nodes -newkey rsa:{{ openvpn_rsa_bits }} -keyout {{ item }}.key -out {{ item }}.csr
19 | -days 3650 -subj /CN=OpenVPN-Client-{{ inventory_hostname[:24] }}-{{ item[:24] }}/
20 | args:
21 | chdir: "{{ openvpn_key_dir }}"
22 | creates: "{{ item }}.key"
23 | with_items:
24 | - "{{ clients }}"
25 |
26 | - name: Protect client keys
27 | ansible.builtin.file:
28 | path: "{{ openvpn_key_dir }}/{{ item }}.key"
29 | mode: "0400"
30 | with_items:
31 | - "{{ clients }}"
32 |
33 | - name: Sign client key
34 | ansible.builtin.command: openssl x509 -req -in {{ item }}.csr -out {{ item }}.crt -CA ca.crt -CAkey ca-key.pem -sha256 -days 3650 -extfile openssl-client.ext
35 | args:
36 | chdir: "{{ openvpn_key_dir }}"
37 | creates: "{{ item }}.crt"
38 | with_items:
39 | - "{{ clients }}"
40 |
41 | - name: Register server ca key
42 | ansible.builtin.slurp:
43 | src: "{{ openvpn_key_dir }}/ca.crt"
44 | register: ca_cert
45 |
46 | - name: Register tls-auth key
47 | ansible.builtin.slurp:
48 | src: "{{ openvpn_key_dir }}/ta.key"
49 | register: tls_auth
50 |
51 | - name: Register client certs
52 | ansible.builtin.slurp:
53 | src: "{{ openvpn_key_dir }}/{{ item }}.crt"
54 | with_items:
55 | - "{{ clients }}"
56 | register: client_certs
57 |
58 | - name: Register client keys
59 | ansible.builtin.slurp:
60 | src: "{{ openvpn_key_dir }}/{{ item }}.key"
61 | with_items:
62 | - "{{ clients }}"
63 | register: client_keys
64 |
65 | - name: Generate client config
66 | no_log: "{{ openvpn_client_config_no_log }}"
67 | ansible.builtin.template:
68 | src: client.ovpn.j2
69 | dest: "{{ openvpn_ovpn_dir }}/{{ item.0.item }}-{{ inventory_hostname }}.ovpn"
70 | owner: root
71 | group: root
72 | mode: "0400"
73 | with_together:
74 | - "{{ client_certs.results }}"
75 | - "{{ client_keys.results }}"
76 |
77 | - name: Fetch client config
78 | ansible.builtin.fetch:
79 | src: "{{ openvpn_ovpn_dir }}/{{ item }}-{{ inventory_hostname }}.ovpn"
80 | dest: "{{ openvpn_fetch_client_configs_dir }}/{{ item }}/{{ inventory_hostname }}{{ openvpn_fetch_client_configs_suffix }}.ovpn"
81 | flat: true
82 | when: openvpn_fetch_client_configs
83 | with_items:
84 | - "{{ clients }}"
85 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Version 2.0 (2016-04-11)
2 | ## Improving TLS Security
3 | 1. Added `auth SHA256` so MACs on the individual packets are done with SHA256 instead of SHA1.
4 |
5 | 2. Added `tls-version-min 1.2` to drop SSL3 + TLS v1.0 support. This breaks older clients (2.3.2+), but those versions have been out for a while.
6 |
7 | 3. Restricted the `tls-cipher`s allowed to a subset of Mozilla's modern cipher list + DHE for older clients. ECDSA support is included for when ECDSA keys can be used.
8 |
9 | 4. New keys are 2048 bit by default, downgraded from 4096 bit. This is based on Mozilla's SSL guidance, combined with the expectation of being able to use ECDSA keys in a later revision of this playbook.
10 |
11 | 5. As part of the move to 2048 bit keys, the 4096 bit DH parameters are no longer distributed. It was originally distributed since generating it took ~75 minutes, but the new 2048 bit parameters take considerably less time.
12 |
13 | Points 2 & 3 are gated by the `openvpn_use_modern_tls` variable, which defaults to `true`.
14 |
15 | ## Adding Cert Validations
16 | OpenVPN has at least two kinds of certification validation available: (Extended) Key Usage checks, and certificate content validation.
17 |
18 | ### EKU
19 | Previously only the client was verifying that the server cert had the correct usage, now the verification is bi-directional.
20 |
21 | ### Certificate content
22 | Added the ability to verify the common name that is part of each certificate. This required changing the common names that each certificate is generated with, which means that the ability to wipe out the existing keys was added as well.
23 |
24 | Again, both these changes are gated by a variable (`openvpn_verify_cn`). Because this requires rather large client changes, it is off by default.
25 |
26 | ## Wiping out & reinstalling
27 | Added the ability to wipe out & reinstall OpenVPN. Currently it leaves firewall rules behind, but other than that everything is removed.
28 |
29 | Use `ansible-playbook -v openvpn.yml --extra-vars="openvpn_uninstall=true" --tags uninstall` to just run the uninstall portion.
30 |
31 | ## Connect over IPv6
32 | Previously, you had to explicitly use `udp6` or `tcp6` to use IPv6. OpenVPN isn't dual stacked if you use plain `udp`/`tcp`, which results in being unable to connect to the OpenVPN server if it has an AAAA record, on your device has a functional IPv6 connection, since the client will choose which stack to use if you just use plain `udp`/`tcp`.
33 |
34 | Since this playbook is only on Linux, which supports IPv4 connections on IPv6 sockets, the server config is now IPv6 by default (https://github.com/OpenVPN/openvpn/blob/master/README.IPv6#L50), by means of using `{{ openvpn_proto }}6` in the server template. Specifying a `*6` protocol for `openvpn_proto` is now an error, and will cause OpenVPN to fail to start.
35 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ### JetBrains template
2 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
3 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
4 |
5 | # User-specific stuff
6 | .idea/**/workspace.xml
7 | .idea/**/tasks.xml
8 | .idea/**/usage.statistics.xml
9 | .idea/**/dictionaries
10 | .idea/**/shelf
11 |
12 | # AWS User-specific
13 | .idea/**/aws.xml
14 |
15 | # Generated files
16 | .idea/**/contentModel.xml
17 |
18 | # Sensitive or high-churn files
19 | .idea/**/dataSources/
20 | .idea/**/dataSources.ids
21 | .idea/**/dataSources.local.xml
22 | .idea/**/sqlDataSources.xml
23 | .idea/**/dynamic.xml
24 | .idea/**/uiDesigner.xml
25 | .idea/**/dbnavigator.xml
26 |
27 | # Gradle
28 | .idea/**/gradle.xml
29 | .idea/**/libraries
30 |
31 | # Gradle and Maven with auto-import
32 | # When using Gradle or Maven with auto-import, you should exclude module files,
33 | # since they will be recreated, and may cause churn. Uncomment if using
34 | # auto-import.
35 | # .idea/artifacts
36 | # .idea/compiler.xml
37 | # .idea/jarRepositories.xml
38 | # .idea/modules.xml
39 | # .idea/*.iml
40 | # .idea/modules
41 | # *.iml
42 | # *.ipr
43 |
44 | # CMake
45 | cmake-build-*/
46 |
47 | # Mongo Explorer plugin
48 | .idea/**/mongoSettings.xml
49 |
50 | # File-based project format
51 | *.iws
52 |
53 | # IntelliJ
54 | out/
55 |
56 | # mpeltonen/sbt-idea plugin
57 | .idea_modules/
58 |
59 | # JIRA plugin
60 | atlassian-ide-plugin.xml
61 |
62 | # Cursive Clojure plugin
63 | .idea/replstate.xml
64 |
65 | # SonarLint plugin
66 | .idea/sonarlint/
67 |
68 | # Crashlytics plugin (for Android Studio and IntelliJ)
69 | com_crashlytics_export_strings.xml
70 | crashlytics.properties
71 | crashlytics-build.properties
72 | fabric.properties
73 |
74 | # Editor-based Rest Client
75 | .idea/httpRequests
76 |
77 | # Android studio 3.1+ serialized cache file
78 | .idea/caches/build_file_checksums.ser
79 |
80 | ### Linux template
81 | *~
82 |
83 | # temporary files which can be created if a process still has a handle open of a deleted file
84 | .fuse_hidden*
85 |
86 | # KDE directory preferences
87 | .directory
88 |
89 | # Linux trash folder which might appear on any partition or disk
90 | .Trash-*
91 |
92 | # .nfs files are created when an open file is removed but is still being accessed
93 | .nfs*
94 |
95 | ### Ansible template
96 | *.retry
97 |
98 | ### Windows template
99 | # Windows thumbnail cache files
100 | Thumbs.db
101 | Thumbs.db:encryptable
102 | ehthumbs.db
103 | ehthumbs_vista.db
104 |
105 | # Dump file
106 | *.stackdump
107 |
108 | # Folder config file
109 | [Dd]esktop.ini
110 |
111 | # Recycle Bin used on file shares
112 | $RECYCLE.BIN/
113 |
114 | # Windows Installer files
115 | *.cab
116 | *.msi
117 | *.msix
118 | *.msm
119 | *.msp
120 |
121 | # Windows shortcuts
122 | *.lnk
123 |
124 | ### macOS template
125 | # General
126 | .DS_Store
127 | .AppleDouble
128 | .LSOverride
129 |
130 | # Icon must end with two \r
131 | Icon
132 |
133 | # Thumbnails
134 | ._*
135 |
136 | # Files that might appear in the root of a volume
137 | .DocumentRevisions-V100
138 | .fseventsd
139 | .Spotlight-V100
140 | .TemporaryItems
141 | .Trashes
142 | .VolumeIcon.icns
143 | .com.apple.timemachine.donotpresent
144 |
145 | # Directories potentially created on remote AFP share
146 | .AppleDB
147 | .AppleDesktop
148 | Network Trash Folder
149 | Temporary Items
150 | .apdisk
151 |
152 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socioeconomic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at [project discussions](https://github.com/aovpn/ansible-role-openvpn/discussions). All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/tasks/compile_ldap_plugin.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Gather specific variables
3 | ansible.builtin.include_vars: "../vars/compile_ldap_plugin.yml"
4 |
5 | - name: Check package re2c already exists
6 | become: true
7 | ansible.builtin.stat:
8 | path: "{{ re2c_bin_path }}"
9 | register: re2c_bin
10 |
11 | - name: Check package openvpn-auth-ldap already exists
12 | become: true
13 | ansible.builtin.stat:
14 | path: "{{ openvpn_auth_ldap_bin_path }}"
15 | register: openvpn_auth_ldap_bin
16 |
17 | - name: Compile LDAP
18 | when: not openvpn_auth_ldap_bin.stat.exists or not re2c_bin.stat.exists
19 | block:
20 | - name: Install gcc objc repo
21 | become: true
22 | ansible.builtin.yum_repository:
23 | name: csi-gcc
24 | description: gcc compiler suite, with Objective-C which is removed from official Red Hat EL8 releases.
25 | baseurl: "{{ gcc_objc_repo.base_url }}"
26 | gpgkey: "{{ gcc_objc_repo.key }}"
27 | gpgcheck: true
28 | enabled: true
29 |
30 | - name: Install dev packages
31 | become: true
32 | ansible.builtin.package:
33 | name: "{{ compile_develop_packages }}"
34 | state: present
35 |
36 | - name: Install re2c
37 | when: not re2c_bin.stat.exists
38 | block:
39 | - name: Download and unpack re2c
40 | become: true
41 | ansible.builtin.unarchive:
42 | src: "https://github.com/skvadrik/re2c/archive/{{ re2c_version }}.tar.gz"
43 | dest: "{{ compile_source_dir }}"
44 | creates: "{{ compile_source_dir }}/re2c-{{ re2c_version }}"
45 | remote_src: true
46 |
47 | - name: Compile re2c
48 | become: true
49 | ansible.builtin.shell: |
50 | autoreconf -i -W all
51 | ./configure
52 | make
53 | make install
54 | args:
55 | chdir: "{{ compile_source_dir }}/re2c-{{ re2c_version }}"
56 | creates: "{{ re2c_bin_path }}"
57 |
58 | - name: Install openvpn-auth-ldap
59 | when: not openvpn_auth_ldap_bin.stat.exists
60 | block:
61 | - name: Download and unpack openvpn-auth-ldap
62 | become: true
63 | ansible.builtin.unarchive:
64 | src: "https://github.com/threerings/openvpn-auth-ldap/archive/auth-ldap-{{ openvpn_auth_ldap_version }}.tar.gz"
65 | dest: "{{ compile_source_dir }}"
66 | creates: "{{ compile_source_dir }}/openvpn-auth-ldap-auth-ldap-{{ openvpn_auth_ldap_version }}"
67 | remote_src: true
68 |
69 | - name: Create module directory
70 | become: true
71 | ansible.builtin.file:
72 | path: "{{ openvpn_auth_ldap_bin_path | dirname }}"
73 | owner: root
74 | group: root
75 | mode: "0750"
76 | state: directory
77 |
78 | - name: Compile
79 | become: true
80 | environment:
81 | PATH: "{{ re2c_bin_path | dirname }}:{{ lookup('env', 'PATH') }}"
82 | ansible.builtin.shell: |
83 | autoconf
84 | autoheader
85 | ./configure --prefix={{ openvpn_auth_ldap_plugin_dir_path }} --with-openvpn=/sbin/openvpn CFLAGS="-fPIC" OBJCFLAGS="-std=gnu11"
86 | make
87 | make install
88 | args:
89 | chdir: "{{ compile_source_dir }}/openvpn-auth-ldap-auth-ldap-{{ openvpn_auth_ldap_version }}"
90 | creates: "{{ openvpn_auth_ldap_bin_path }}"
91 |
92 | - name: Cleanup dev packages
93 | become: true
94 | ansible.builtin.package:
95 | name: "{{ compile_develop_packages }}"
96 | state: absent
97 | when:
98 | - compile_cleanup_dev_packages
99 |
100 | - name: Remove gcc objc repo
101 | become: true
102 | ansible.builtin.yum_repository:
103 | name: csi-gcc
104 | state: absent
105 | when:
106 | - compile_cleanup_dev_packages
107 |
--------------------------------------------------------------------------------
/tasks/config.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Create openvpn config file
3 | ansible.builtin.template:
4 | src: server.conf.j2
5 | dest: "{{ openvpn_base_dir }}/{{ openvpn_config_file }}.conf"
6 | owner: root
7 | group: root
8 | mode: "0644"
9 | notify:
10 | - Restart openvpn
11 |
12 | - name: Copy up script if defined
13 | ansible.builtin.copy:
14 | src: "{{ openvpn_script_up }}"
15 | dest: "{{ openvpn_base_dir }}/up.sh"
16 | mode: a+x
17 | when: openvpn_script_up is defined
18 |
19 | - name: Copy down script if defined
20 | ansible.builtin.copy:
21 | src: "{{ openvpn_script_down }}"
22 | dest: "{{ openvpn_base_dir }}/down.sh"
23 | mode: a+x
24 | when: openvpn_script_down is defined
25 |
26 | - name: Copy client-connect script if defined
27 | ansible.builtin.copy:
28 | src: "{{ openvpn_script_client_connect }}"
29 | dest: "{{ openvpn_base_dir }}/client_connect.sh"
30 | mode: a+x
31 | when: openvpn_script_client_connect is defined
32 |
33 | - name: Copy client-disconnect script if defined
34 | ansible.builtin.copy:
35 | src: "{{ openvpn_script_client_disconnect }}"
36 | dest: "{{ openvpn_base_dir }}/client_disconnect.sh"
37 | mode: a+x
38 | when: openvpn_script_client_disconnect is defined
39 |
40 | - name: Ensure auth folder exist in openvpn dir
41 | ansible.builtin.file:
42 | path: "{{ openvpn_base_dir }}/auth"
43 | state: directory
44 | mode: "0755"
45 | when: openvpn_use_ldap
46 |
47 | - name: Delete auth folder in openvpn dir
48 | ansible.builtin.file:
49 | path: "{{ openvpn_base_dir }}/auth"
50 | state: absent
51 | when: not openvpn_use_ldap
52 |
53 | - name: Install LDAP config
54 | ansible.builtin.template:
55 | src: ldap.conf.j2
56 | dest: "{{ openvpn_base_dir }}/auth/ldap.conf"
57 | owner: root
58 | group: root
59 | mode: "0644"
60 | when: openvpn_use_ldap
61 |
62 | - name: Create log directory
63 | ansible.builtin.file:
64 | dest: "{{ openvpn_log_dir }}"
65 | owner: root
66 | group: root
67 | mode: "0755"
68 |
69 | - name: Copy openvpn logrotate config file
70 | ansible.builtin.template:
71 | src: openvpn_logrotate.conf.j2
72 | dest: /etc/logrotate.d/openvpn-{{ openvpn_config_file }}.conf
73 | owner: root
74 | group: root
75 | mode: "0400"
76 | when: ansible_os_family != 'Solaris'
77 |
78 | - name: Create client config directory
79 | ansible.builtin.file:
80 | state: directory
81 | path: "{{ openvpn_base_dir }}/{{ openvpn_client_config_dir }}"
82 | owner: root
83 | group: root
84 | mode: "0755"
85 | when: openvpn_client_config
86 |
87 | - name: Create client configs
88 | ansible.builtin.template:
89 | src: client_ccd.j2
90 | dest: "{{ openvpn_base_dir }}/{{ openvpn_client_config_dir }}/{{ item.key }}"
91 | owner: root
92 | group: root
93 | mode: "0644"
94 | when: openvpn_client_config
95 | with_dict: "{{ openvpn_client_configs }}"
96 |
97 | - name: List client config directory
98 | ansible.builtin.command: "ls -1 {{ openvpn_base_dir }}/{{ openvpn_client_config_dir }}"
99 | register: __ccd_contents
100 | changed_when: false
101 | when: openvpn_client_config
102 |
103 | - name: Delete undeclared configs in client config directory
104 | ansible.builtin.file:
105 | path: "{{ openvpn_base_dir }}/{{ openvpn_client_config_dir }}/{{ item }}"
106 | state: absent
107 | when:
108 | - item not in openvpn_client_configs.keys() | list
109 | - openvpn_client_config
110 | with_items: "{{ __ccd_contents.stdout_lines | default([]) }}"
111 |
112 | - name: Delete client config directory
113 | ansible.builtin.file:
114 | path: "{{ openvpn_base_dir }}/{{ openvpn_client_config_dir }}"
115 | state: absent
116 | when: not openvpn_client_config
117 |
118 | - name: Setup openvpn auto-start & start
119 | ansible.builtin.service:
120 | name: "{{ openvpn_service_name }}"
121 | enabled: true
122 | state: started
123 | # Github Actions doesn't allow entrypoints, so PID 1 isn't an init system
124 | when: ansible_service_mgr != "tail"
125 |
--------------------------------------------------------------------------------
/defaults/main.yml:
--------------------------------------------------------------------------------
1 | ---
2 | ## Packaging defaults
3 |
4 | epel_package_name: epel-release
5 | iptables_persistent_package_name: iptables-persistent
6 | iptables_services_package_name: iptables-services
7 | openssl_package_name: openssl
8 | openvpn_ldap_plugin_package_name: openvpn-auth-ldap
9 | openvpn_package_name: openvpn
10 | python_firewall_package_name: python-firewall
11 |
12 |
13 | ## Defaults for the role operation
14 |
15 | clients: []
16 |
17 | # Directories
18 | openvpn_base_dir: /etc/openvpn
19 | openvpn_key_dir: /etc/openvpn/keys
20 | openvpn_ovpn_dir: "{{ openvpn_base_dir }}"
21 |
22 | # Config fetch settings
23 | openvpn_fetch_client_configs: true
24 | openvpn_fetch_client_configs_dir: /tmp/ansible
25 | openvpn_fetch_client_configs_suffix: ""
26 |
27 | # Firewall
28 | firewalld_default_interface_zone: public
29 | iptables_service: iptables
30 | manage_firewall_rules: true
31 | openvpn_firewall: auto
32 |
33 | # Misc
34 | ci_build: false
35 | openvpn_client_config_no_log: true
36 | openvpn_revoke_these_certs: []
37 | openvpn_selinux_module: my-openvpn-server
38 | openvpn_service_name: openvpn
39 | openvpn_sync_certs: false
40 | openvpn_uninstall: false
41 | openvpn_use_ldap: false
42 |
43 |
44 | ## Defaults for openvpn
45 |
46 | # Networking
47 | openvpn_client_register_dns: false
48 | openvpn_client_to_client: false
49 | openvpn_custom_dns: []
50 | openvpn_dns_servers: []
51 | openvpn_dualstack: true
52 | openvpn_keepalive_ping: 5
53 | openvpn_keepalive_timeout: 30
54 | openvpn_port: 1194
55 | openvpn_proto: udp
56 | openvpn_redirect_gateway: true
57 | openvpn_resolv_retry: 5
58 | openvpn_server_hostname: "{{ inventory_hostname }}"
59 | openvpn_server_netmask: 255.255.255.0
60 | openvpn_server_network: 10.9.0.0
61 | openvpn_set_dns: true
62 | openvpn_tun_mtu:
63 |
64 | # Security
65 | openvpn_auth_alg: SHA256
66 | openvpn_cipher: AES-256-CBC
67 | openvpn_duplicate_cn: false
68 | openvpn_rsa_bits: 2048
69 | openvpn_use_crl: false
70 | openvpn_use_1_3_tls: false
71 | openvpn_use_pregenerated_dh_params: false
72 | openvpn_verify_cn: false
73 | tls_auth_required: true
74 | openvpn_script_security: 1
75 |
76 | # Operations
77 | openvpn_addl_client_options: []
78 | openvpn_addl_server_options: []
79 | openvpn_compression: lzo
80 | openvpn_enable_management: false
81 | openvpn_ifconfig_pool_persist_file: ipp.txt
82 | openvpn_management_bind: /var/run/openvpn/management unix
83 | openvpn_management_client_user: root
84 | openvpn_push: []
85 | openvpn_service_group: nogroup
86 | openvpn_service_user: nobody
87 | openvpn_status_version: 1
88 |
89 | # Client config - settings the server will push
90 | openvpn_client_config: false
91 | openvpn_client_config_dir: ccd
92 | openvpn_client_configs: {}
93 | # Example:
94 | # openvpn_client_configs:
95 | # client1:
96 | # - ifconfig-push 10.0.0.2 255.255.255.0
97 | # - push "route 192.168.1.0 255.255.255.0"
98 | # - push "dhcp-option DOMAIN example.com"
99 | # - iroute 192.168.4.0 255.255.255.0
100 | #
101 | # OR
102 | #
103 | # openvpn_client_configs:
104 | # client1:
105 | # - ifconfig-push 10.0.0.2 255.255.255.0
106 | # - push "route 192.168.1.0 255.255.255.0"
107 | # - iroute 192.168.2.0 255.255.255.0
108 | # - iroute 192.168.4.0 255.255.255.0
109 |
110 | ## Logrotate configuration
111 |
112 | openvpn_log_dir: /var/log
113 | openvpn_log_file: openvpn.log
114 | openvpn_logrotate_config: |
115 | rotate 4
116 | weekly
117 | missingok
118 | notifempty
119 | sharedscripts
120 | copytruncate
121 | delaycompress
122 |
123 |
124 | ## LDAP defaults
125 |
126 | ldap:
127 | url: ldap://host.example.com
128 | anonymous_bind: false
129 | bind_dn: uid=Manager,ou=People,dc=example,dc=com
130 | bind_password: mysecretpassword
131 | tls_enable: false
132 | tls_ca_cert_file: "{{ openvpn_base_dir }}/auth/ca.pem"
133 | base_dn: ou=People,dc=example,dc=com
134 | search_filter: (&(uid=%u)(accountStatus=active))
135 | require_group: false
136 | group_base_dn: ou=Groups,dc=example,dc=com
137 | group_search_filter: (|(cn=developers)(cn=artists))
138 | # verify_client_cert is in the readme, but not set here
139 | # because the task checks for the _existence_ of the default
140 | # future change will set it
141 |
--------------------------------------------------------------------------------
/templates/server.conf.j2:
--------------------------------------------------------------------------------
1 | # {{ ansible_managed }}
2 |
3 | {% if openvpn_local is defined %}
4 | local {{ openvpn_local }}
5 | {% endif %}
6 | port {{ openvpn_port }}
7 | {% if openvpn_dualstack %}
8 | proto {{ openvpn_proto }}6
9 | {% else %}
10 | proto {{ openvpn_proto }}
11 | {% endif %}
12 | dev tun
13 |
14 | ca {{ openvpn_key_dir }}/ca.crt
15 | cert {{ openvpn_key_dir }}/server.crt
16 | key {{ openvpn_key_dir }}/server.key
17 | dh {{ openvpn_key_dir }}/dh.pem
18 | {% if openvpn_crl_path is defined %}
19 | crl-verify {{ openvpn_crl_path }}
20 | {% endif %}
21 | {% if openvpn_use_crl|bool %}
22 | crl-verify {{ openvpn_key_dir }}/ca-crl.pem
23 | {% endif %}
24 | {% if tls_auth_required %}
25 | tls-auth {{ openvpn_key_dir }}/ta.key 0
26 | {% endif %}
27 | tls-server
28 | auth {{ openvpn_auth_alg | default('SHA256') }}
29 | cipher {{ openvpn_cipher }}
30 | {% if openvpn_tun_mtu %}
31 | tun-mtu {{ openvpn_tun_mtu }}
32 | {% endif %}
33 | {% if openvpn_use_1_3_tls | bool %}
34 | tls-version-min 1.3
35 | {% else %}
36 | tls-version-min 1.2
37 | {% endif %}
38 | {# Using Mozilla's modern cipher list + DHE for older clients #}
39 | {% if openvpn_use_1_3_tls | bool %}
40 | tls-ciphersuites TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256
41 | {% else %}
42 | tls-cipher TLS-ECDHE-ECDSA-WITH-AES-256-GCM-SHA384:TLS-ECDHE-RSA-WITH-AES-256-GCM-SHA384:TLS-DHE-RSA-WITH-AES-256-GCM-SHA384:TLS-ECDHE-ECDSA-WITH-AES-256-CBC-SHA384:TLS-ECDHE-RSA-WITH-AES-256-CBC-SHA384:TLS-DHE-RSA-WITH-AES-256-CBC-SHA256
43 | {% endif %}
44 | {% if openvpn_duplicate_cn|bool %}
45 | duplicate-cn
46 | {% endif %}
47 | {% if openvpn_client_to_client|bool %}
48 | client-to-client
49 | {% endif %}
50 |
51 | server {{ openvpn_server_network }} {{ openvpn_server_netmask }}
52 | {% if openvpn_server_ipv6_network is defined %}
53 | server-ipv6 {{ openvpn_server_ipv6_network }}
54 | {% endif %}
55 | {% if openvpn_topology is defined %}
56 | topology {{ openvpn_topology }}
57 | {% endif %}
58 | ifconfig-pool-persist {{ openvpn_ifconfig_pool_persist_file }}
59 | {% if openvpn_client_config %}
60 | client-config-dir {{ openvpn_client_config_dir }}
61 | {% endif %}
62 |
63 | {% if openvpn_redirect_gateway|bool %}
64 | push "redirect-gateway def1 bypass-dhcp"
65 | {% endif %}
66 | {% if openvpn_set_dns %}
67 | {% if openvpn_custom_dns %}
68 | {% for srv in openvpn_dns_servers %}
69 | push "dhcp-option DNS {{ srv }}"
70 | {% endfor %}
71 | {% else %}
72 | push "dhcp-option DNS 1.0.0.1"
73 | push "dhcp-option DNS 1.1.1.1"
74 | push "dhcp-option DNS 8.8.8.8"
75 | push "dhcp-option DNS 8.8.4.4"
76 | {% endif %}
77 | {% endif %}
78 | {% if openvpn_push is defined %}
79 | {% for opt in openvpn_push %}
80 | push "{{ opt }}"
81 | {% endfor %}
82 | {% endif %}
83 | keepalive {{ openvpn_keepalive_ping }} {{ openvpn_keepalive_timeout }}
84 | {% if openvpn_compression is not undefined and openvpn_compression != "" %}
85 | compress {{ openvpn_compression }}
86 | {% endif %}
87 | persist-key
88 | persist-tun
89 | user {{ openvpn_service_user }}
90 | group {{ openvpn_service_group }}
91 |
92 | {% for option in openvpn_addl_server_options %}
93 | {{ option }}
94 | {% endfor %}
95 |
96 | status status-{{ openvpn_config_file }}.log
97 | status-version {{ openvpn_status_version }}
98 | log-append {{ openvpn_log_dir }}/{{ openvpn_log_file }}
99 | verb 3
100 |
101 | {% if openvpn_verify_cn|bool %}
102 | verify-x509-name OpenVPN-Client-{{ inventory_hostname[:24] }} name-prefix
103 | remote-cert-tls client
104 | {% endif %}
105 |
106 | {% if openvpn_enable_management|bool %}
107 | management {{ openvpn_management_bind }}
108 | {% if openvpn_management_client_user %}
109 | management-client-user {{ openvpn_management_client_user }}
110 | {% endif %}
111 | {% endif %}
112 |
113 | {% if openvpn_use_ldap|bool %}
114 | ### LDAP AUTH ###
115 | {% if ansible_os_family == 'Debian' %}
116 | plugin /usr/lib/openvpn/openvpn-auth-ldap.so "{{ openvpn_base_dir }}/auth/ldap.conf"
117 | {% elif ansible_machine == "x86_64" %}
118 | plugin /usr/lib64/openvpn/plugin/lib/openvpn-auth-ldap.so "{{ openvpn_base_dir }}/auth/ldap.conf"
119 | {% else %}
120 | plugin /usr/lib/openvpn/plugin/lib/openvpn-auth-ldap.so "{{ openvpn_base_dir }}/auth/ldap.conf"
121 | {% endif %}
122 | {% if ldap.verify_client_cert is defined %}
123 | verify-client-cert {{ ldap.verify_client_cert }}
124 | {% else %}
125 | client-cert-not-required
126 | {% endif %}
127 | {% endif %}
128 |
129 | script-security {{ openvpn_script_security }}
130 |
131 | {% if openvpn_script_up is defined %}
132 | up {{ openvpn_base_dir }}/up.sh
133 | {% endif %}
134 | {% if openvpn_script_down is defined %}
135 | down {{ openvpn_base_dir }}/down.sh
136 | {% endif %}
137 |
138 |
139 | {% if openvpn_script_client_connect is defined %}
140 | client-connect {{ openvpn_base_dir }}/client_connect.sh
141 | {% endif %}
142 | {% if openvpn_script_client_disconnect is defined %}
143 | client-disconnect {{ openvpn_base_dir }}/client_disconnect.sh
144 | {% endif %}
145 |
--------------------------------------------------------------------------------
/tasks/server_keys.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Create openvpn key directory
3 | ansible.builtin.file:
4 | path: "{{ openvpn_key_dir }}"
5 | state: directory
6 | mode: "0755"
7 |
8 | - name: Copy openssl server/ca extensions
9 | ansible.builtin.copy:
10 | src: "{{ item }}"
11 | dest: "{{ openvpn_key_dir }}"
12 | owner: root
13 | group: root
14 | mode: "0400"
15 | with_items:
16 | - openssl-server.ext
17 | - openssl-ca.ext
18 |
19 | - name: Copy CA key
20 | ansible.builtin.copy:
21 | content: "{{ openvpn_ca_key.key }}"
22 | dest: "{{ openvpn_key_dir }}/ca-key.pem"
23 | mode: "0400"
24 | when: openvpn_ca_key is defined
25 |
26 | - name: Copy CA cert
27 | ansible.builtin.copy:
28 | content: "{{ openvpn_ca_key.crt }}"
29 | dest: "{{ openvpn_key_dir }}/ca.crt"
30 | mode: "0444"
31 | when: openvpn_ca_key is defined
32 |
33 | - name: Generate CA key
34 | ansible.builtin.command: >-
35 | openssl req -nodes -newkey rsa:{{ openvpn_rsa_bits }} -keyout ca-key.pem -out ca-csr.pem
36 | -days 3650 -subj /CN=OpenVPN-CA-{{ inventory_hostname[:53] }}/
37 | args:
38 | chdir: "{{ openvpn_key_dir }}"
39 | creates: ca-key.pem
40 | when: openvpn_ca_key is not defined
41 |
42 | - name: Protect CA key
43 | ansible.builtin.file:
44 | path: "{{ openvpn_key_dir }}/ca-key.pem"
45 | mode: "0400"
46 | when: openvpn_ca_key is not defined
47 |
48 | - name: Sign CA key
49 | ansible.builtin.command: openssl x509 -req -in ca-csr.pem -out ca.crt -CAcreateserial -signkey ca-key.pem -sha256 -days 3650 -extfile openssl-ca.ext
50 | args:
51 | chdir: "{{ openvpn_key_dir }}"
52 | creates: ca.crt
53 | when: openvpn_ca_key is not defined
54 |
55 | - name: Generate server key
56 | ansible.builtin.command: >-
57 | openssl req -nodes -newkey rsa:{{ openvpn_rsa_bits }} -keyout server.key -out server.csr
58 | -days 3650 -subj /CN=OpenVPN-Server-{{ inventory_hostname[:49] }}/
59 | args:
60 | chdir: "{{ openvpn_key_dir }}"
61 | creates: server.key
62 |
63 | - name: Protect server key
64 | ansible.builtin.file:
65 | path: "{{ openvpn_key_dir }}/server.key"
66 | mode: "0400"
67 |
68 | - name: Sign server key
69 | ansible.builtin.command: "openssl x509 -req -in server.csr -out server.crt -CA ca.crt
70 | -CAkey ca-key.pem -sha256 -days 3650 -CAcreateserial -extfile openssl-server.ext"
71 | args:
72 | chdir: "{{ openvpn_key_dir }}"
73 | creates: server.crt
74 |
75 | - name: Copy tls-auth key
76 | ansible.builtin.copy:
77 | content: "{{ openvpn_tls_auth_key }}"
78 | dest: "{{ openvpn_key_dir }}/ta.key"
79 | mode: "0400"
80 | when: openvpn_tls_auth_key is defined
81 |
82 | - name: Generate tls-auth key
83 | ansible.builtin.command: openvpn --genkey --secret ta.key
84 | args:
85 | chdir: "{{ openvpn_key_dir }}"
86 | creates: ta.key
87 | when: openvpn_tls_auth_key is not defined
88 |
89 | # not a security issue, params aren't secret, just not generated by an attacker
90 | # per http://security.stackexchange.com/questions/42415/openvpn-dhparam/42418#42418
91 | - name: Copy pre-generated DH params
92 | ansible.builtin.copy:
93 | src: dh.pem
94 | dest: "{{ openvpn_key_dir }}"
95 | owner: root
96 | group: root
97 | mode: "0400"
98 | when: openvpn_use_pregenerated_dh_params|bool
99 |
100 | # Alternatively, if you're concerned about logjam attacks
101 | - name: Generate dh params
102 | ansible.builtin.command: openssl dhparam -out {{ openvpn_key_dir }}/dh.pem {{ openvpn_rsa_bits }}
103 | args:
104 | chdir: "{{ openvpn_key_dir }}"
105 | creates: dh.pem
106 | when: not (openvpn_use_pregenerated_dh_params|bool)
107 |
108 | - name: Install ca.conf config file
109 | ansible.builtin.template:
110 | src: ca.conf.j2
111 | dest: "{{ openvpn_key_dir }}/ca.conf"
112 | owner: root
113 | group: root
114 | mode: "0744"
115 |
116 | - name: Create initial certificate revocation list sequence number
117 | ansible.builtin.shell: "echo 00 > crl_number"
118 | args:
119 | chdir: "{{ openvpn_key_dir }}"
120 | creates: crl_number
121 |
122 | - name: Generate tls-auth key
123 | ansible.builtin.command: openvpn --genkey --secret ta.key
124 | args:
125 | chdir: "{{ openvpn_key_dir }}"
126 | creates: ta.key
127 | when: openvpn_tls_auth_key is not defined
128 |
129 | - name: Install revocation script
130 | ansible.builtin.template:
131 | src: revoke.sh.j2
132 | dest: "{{ openvpn_key_dir }}/revoke.sh"
133 | owner: root
134 | group: root
135 | mode: "0744"
136 |
137 | - name: Check if certificate revocation list database exists
138 | ansible.builtin.stat:
139 | path: "{{ openvpn_key_dir }}/index.txt"
140 | register: file_result
141 |
142 | - name: Create certificate revocation list database if required
143 | ansible.builtin.file:
144 | path: "{{ openvpn_key_dir }}/index.txt"
145 | state: touch
146 | mode: "0644"
147 | when: not file_result.stat.exists
148 |
149 | - name: Set up certificate revocation list
150 | ansible.builtin.command: sh revoke.sh
151 | args:
152 | chdir: "{{ openvpn_key_dir }}"
153 | creates: "{{ openvpn_key_dir }}/ca-crl.pem"
154 |
155 | - name: Install crl-cron script
156 | ansible.builtin.template:
157 | src: crl-cron.sh.j2
158 | dest: "{{ openvpn_base_dir }}/crl-cron.sh"
159 | owner: root
160 | group: root
161 | mode: "0744"
162 |
163 | # This should eventually be switched to use a systemd timer
164 | # eg /usr/local/lib/systemd/system/openvpn-crl.timer
165 | - name: Check for crontab
166 | ansible.builtin.command: which crontab
167 | register: crontab
168 | check_mode: false
169 | changed_when: false
170 | failed_when: false
171 |
172 | - name: Install cronie
173 | ansible.builtin.package:
174 | name: cronie
175 | state: present
176 | when: ansible_os_family == "RedHat" and crontab.rc != 0
177 |
178 | - name: Add cron to check every Saturday if the CRL needs to be renewed
179 | ansible.builtin.cron:
180 | name: "check if CRL will expire soon"
181 | special_time: weekly
182 | job: "sh {{ openvpn_base_dir }}/crl-cron.sh"
183 | cron_file: /etc/cron.d/openvpn-crl
184 | user: root
185 | when: not ci_build
186 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | OpenVPN
2 | =========
3 | This role installs OpenVPN from default repositories, configures it as a server, sets up networking and firewalls (primarily firewalld, ufw and iptables - best effort), and generates client configuration files. It also optionally supports LDAP authentication.
4 |
5 | Supported Operating Systems:
6 | - Ubuntu 24.04 and higher
7 | - CentOS 9 and higher
8 | - Debian 11 and higher
9 |
10 | # Requirements
11 | OpenVPN must be available as a package in yum/dnf/apt! For CentOS users, this role will run `yum install epel-release` to ensure OpenVPN is available.
12 |
13 | Ubuntu Precise has a [weird bug](https://bugs.launchpad.net/ubuntu/+source/iptables-persistent/+bug/1002078) that might cause the `iptables-persistent` installation to fail. There is a [workaround](https://forum.linode.com/viewtopic.php?p=58233#p58233).
14 |
15 | ## Ansible Core 2.10 and Higher
16 | With Ansible 2.10, modules have been moved into collections. Aside from Ansible's built-in modules, additional collections are required for certain module like seboolean (now ansible.posix.seboolean). The required collections are:
17 |
18 | - ansible.posix
19 | - community.general (if using ufw)
20 |
21 | Installation:
22 | ```bash
23 | ansible-galaxy collection install ansible.posix
24 | ansible-galaxy collection install community.general
25 | ```
26 | If you're using a standard Ansible distribution, you won't need to install any additional collections.
27 |
28 | # Support Notes/Expectations
29 | This role is supported by a group of enthusiasts on a best-effort basis. Feel free to open an issue and contribute a related pull request with a fix.
30 |
31 | # Role Variables
32 | ## Role options
33 | These options change how the role works. This is a catch-all group, specific groups are broken out below.
34 |
35 | | Variable | Type | Choices | Default | Comment |
36 | |------------------------------|---------|-------------|-------------------|-------------------------------------------------------------------------------|
37 | | clients | list | | [] | List of clients (kinda users) to add to OpenVPN |
38 | | openvpn_base_dir | string | | /etc/openvpn | Path where your OpenVPN config will be stored |
39 | | openvpn_client_config_no_log | boolean | true, false | true | Prevent client configuration files to be logged to stdout by Ansible |
40 | | openvpn_key_dir | string | | /etc/openvpn/keys | Path where your server private keys and CA will be stored |
41 | | openvpn_ovpn_dir | string | | /etc/openvpn | Path where your client configurations will be stored |
42 | | openvpn_revoke_these_certs | list | | [] | List of client certificates to revoke. |
43 | | openvpn_selinux_module | string | | my-openvpn-server | Set the SELinux module name |
44 | | openvpn_service_name | string | | openvpn | Name of the service. Used by systemctl to start the service |
45 | | openvpn_sync_certs | boolean | true, false | false | Revoke certificates not explicitly defined in 'clients' |
46 | | openvpn_uninstall | boolean | true, false | false | Set to true to uninstall the OpenVPN service |
47 | | openvpn_use_ldap | boolean | true, false | false | Active LDAP backend for authentication. Client certificate not needed anymore |
48 |
49 | ### Config fetching
50 | Change these options if you need to adjust how the configs are download to your local system
51 |
52 | | Variable | Type | Choices | Default | Comment |
53 | |-------------------------------------|---------|-------------|--------------|-------------------------------------------------------------------------------------------------------------------------------------------|
54 | | openvpn_fetch_client_configs | boolean | true, false | true | Download generated client configurations to the local system |
55 | | openvpn_fetch_client_configs_dir | string | | /tmp/ansible | If openvpn_fetch_client_configs is true, the local directory to download the client config files into |
56 | | openvpn_fetch_client_configs_suffix | string | | "" | If openvpn_fetch_client_configs is true, the suffix to append to the downloaded client config files before the trailing `.ovpn` extension |
57 |
58 | ### Firewall
59 | Change these options if you need to force a particular firewall or change how the playbook interacts with the firewall.
60 |
61 | | Variable | Type | Choices | Default | Comment |
62 | |----------------------------------|---------|--------------------------------|----------|-------------------------------------------------------------------------------------------------------------|
63 | | firewalld_default_interface_zone | string | | public | Firewalld zone where the "ansible_default_ipv4.interface" will be pushed into |
64 | | iptables_service | string | | iptables | Override the iptables service name |
65 | | manage_firewall_rules | boolean | true, false | true | Allow playbook to manage iptables |
66 | | openvpn_firewall | string | auto, firewalld, ufw, iptables | auto | The firewall software to configure network rules. "auto" will attempt to detect it by inspecting the system |
67 | ## OpenVPN Config Options
68 | These options change how OpenVPN itself works.
69 | ### Networking
70 | | Variable | Type | Choices | Default | Comment |
71 | |-----------------------------|--------------|-------------------|----------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------|
72 | | openvpn_client_register_dns | boolean | true, false | true | Add `register-dns` option to client config (Windows only). |
73 | | openvpn_client_to_client | boolean | true, false | false | Set to true if you want clients to access each other. |
74 | | openvpn_custom_dns | list[string] | | [] | List of DNS servers, only applied if `openvpn_set_dns` is set to true |
75 | | openvpn_dualstack | boolean | | true | Whether or not to use a dualstack (IPv4 + v6) socket |
76 | | openvpn_keepalive_ping | int | | 5 | Set `keepalive` ping interval seconds. |
77 | | openvpn_keepalive_timeout | int | | 30 | Set `keepalive` timeout seconds |
78 | | openvpn_local | string | | `unset` | Local host name or IP address for bind. If specified, OpenVPN will bind to this address only. If unspecified, OpenVPN will bind to all interfaces. |
79 | | openvpn_port | int | | 1194 | The port you want OpenVPN to run on. If you have different ports on different servers, I suggest you set the port in your inventory file. |
80 | | openvpn_proto | string | udp, tcp | udp | The protocol you want OpenVPN to use |
81 | | openvpn_redirect_gateway | boolean | true, false | true | OpenVPN gateway push |
82 | | openvpn_resolv_retry | int/string | any int, infinite | 5 | Hostname resolv failure retry seconds. Set "infinite" to retry indefinitely in case of poor connection or laptop sleep mode recovery etc. |
83 | | openvpn_server_hostname | string | | `{{ inventory_hostname }}` | The server name to place in the client configuration file |
84 | | openvpn_server_ipv6_network | string | | `unset` | If set, the network address and prefix of an IPv6 network to assign to clients. If True, IPv4 still used too. |
85 | | openvpn_server_netmask | string | | 255.255.255.0 | Netmask of the private network |
86 | | openvpn_server_network | string | | 10.9.0.0 | Private network used by OpenVPN service |
87 | | openvpn_set_dns | boolean | true, false | true | Will push DNS to the client (Cloudflare and Google) |
88 | | openvpn_tun_mtu | int | | `unset` | Set `tun-mtu` value. Empty for default. |
89 | ### Security
90 | | Variable | Type | Choices | Default | Comment |
91 | |------------------------------------|---------|-------------|-------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------|
92 | | openvpn_auth_alg | string | | SHA256 | Set `auth` authentication algoritm. |
93 | | openvpn_ca_key | dict | | `unset` | Contain "crt" and "key". If not set, CA cert and key will be automatically generated on the target system. |
94 | | openvpn_cipher | string | | AES-256-CBC | Set `cipher` option for server and client. |
95 | | openvpn_crl_path | string | | `unset` | Define a path to the CRL file for server revocation check. |
96 | | openvpn_duplicate_cn | boolean | true, false | false | Add `duplicate-cn` option to server config - this allows clients to connect multiple times with the one key. NOTE: client ip addresses won't be static anymore! |
97 | | openvpn_rsa_bits | int | | 2048 | Number of bits used to protect generated certificates |
98 | | openvpn_script_security | int | | 1 | Set openvpn script security option |
99 | | openvpn_tls_auth_key | string | | `unset` | Single item with a pre-generated TLS authentication key. |
100 | | openvpn_use_crl | boolean | true, false | false | Configure OpenVPN server to honor certificate revocation list. |
101 | | openvpn_use_1_3_tls | boolean | true, false | false | Require a minimum version of TLS 1.3 (TLS 1.2 used by default) |
102 | | openvpn_use_pregenerated_dh_params | boolean | true, false | false | DH params are generted with the install by default |
103 | | openvpn_verify_cn | boolean | true, false | false | Check that the CN of the certificate match the FQDN |
104 | | tls_auth_required | boolean | true, false | true | Ask the client to push the generated ta.key of the server during the connection |
105 | ### Operations
106 | | Variable | Type | Choices | Default | Comment |
107 | |------------------------------------|---------|-------------|--------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
108 | | openvpn_addl_client_options | list | | empty | List of user-defined client options that are not already present in the client template. (e.g. `- mssfix 1400`) |
109 | | openvpn_addl_server_options | list | | empty | List of user-defined server options that are not already present in the server template. (e.g. `- ping-timer-rem`) |
110 | | openvpn_compression | string | | lzo | Set `compress` compression option. Empty for no compression. |
111 | | openvpn_config_file | string | | openvpn_{{ openvpn\_proto }}\_{{ openvpn_port }} | The config file name you want to use (set in vars/main.yml) |
112 | | openvpn_enable_management | boolean | true, false | false | |
113 | | openvpn_ifconfig_pool_persist_file | string | | ipp.txt | |
114 | | openvpn_management_bind | string | | /var/run/openvpn/management unix | The interface to bind on for the management interface. Can be unix or TCP socket. |
115 | | openvpn_management_client_user | string | | root | Use this user when using a Unix socket for management interface. |
116 | | openvpn_push | list | | empty | Set here a list of string that will be inserted into the config file as `push ""`. E.g `- route 10.20.30.0 255.255.255.0` will generate push "route 10.20.30.0 255.255.255.0" |
117 | | openvpn_script_client_connect | string | | `unset` | Path to your openvpn client-connect script |
118 | | openvpn_script_client_disconnect | string | | `unset` | Path to your openvpn client-disconnect script |
119 | | openvpn_script_down | string | | `unset` | Path to your openvpn down script |
120 | | openvpn_script_up | string | | `unset` | Path to your openvpn up script |
121 | | openvpn_service_group | string | | nogroup | Set the openvpn service group. |
122 | | openvpn_service_user | string | | nobody | Set the openvpn service user. |
123 | | openvpn_status_version | int | 1, 2, 3 | 1 | Define the formatting of the openvpn-status.log file where are listed current client connection |
124 | | openvpn_topology | string | | `unset` | the "topology" keyword will be set in the server config with the specified value. |
125 |
126 | ### OpenVPN custom client config (server pushed)
127 | | Variable | Type | Choices | Default | Comment |
128 | |---------------------------|---------|---------|---------|------------------------------------------------------|
129 | | openvpn_client_config | Boolean | | false | Set to true if enable client configuration directory |
130 | | openvpn_client_config_dir | string | | ccd | Path of `client-config-dir` |
131 | | openvpn_client_configs | dict | | {} | Dict of settings custom client configs |
132 |
133 | ## Logrotate
134 | Set your own custom logrotate options.
135 |
136 | | Variable | Type | Choices | Default | Comment |
137 | |--------------------------|--------|---------|-------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------|
138 | | openvpn_log_dir | string | | /var/log | Set location of openvpn log files. This parameter is a part of `log-append` configuration value. |
139 | | openvpn_log_file | string | | openvpn.log | Set log filename. This parameter is a part of `log-append` configuration value. |
140 | | openvpn_logrotate_config | string | | rotate 4
weekly
missingok
notifempty
sharedscripts
copytruncate
delaycompress | Configure logrotate script. |
141 |
142 | ## Packaging
143 | This role pulls in a bunch of different packages. Override the names as necessary.
144 |
145 | | Variable | Type | Choices | Default | Comment |
146 | |----------------------------------|--------|---------|---------------------|-----------------------------------------------------------------------------|
147 | | epel_package_name | string | | epel-release | Name of the epel-release package to install from the package manager |
148 | | iptables_persistent_package_name | string | | iptables-persistent | Name of the iptables-persistent package to install from the package manager |
149 | | iptables_services_package_name | string | | iptables-services | Name of the iptables-services package to install from the package manager |
150 | | openssl_package_name | string | | openssl | Name of the openssl package to install from the package manager |
151 | | openvpn_ldap_plugin_package_name | string | | openvpn-auth-ldap | Name of the openvpn-auth-ldap package to install from the package manager |
152 | | openvpn_package_name | string | | openvpn | Name of the openvpn package to install from the package manager |
153 | | python_firewall_package_name | string | | python-firewall | Name of the python-firewall package to install from the package manager |
154 |
155 | ## LDAP object
156 | | Variable | Type | Choices | Default | Comment |
157 | |---------------------|--------|---------------------------|-----------------------------------------|----------------------------------------------------------------------------------------------|
158 | | ldap | dict | | | Dictionary that contain LDAP configuration |
159 | | url | string | | ldap://host.example.com | Address of you LDAP backend with syntax ldap[s]://host[:port] |
160 | | anonymous_bind | string | False , True | False | This is not an Ansible boolean but a string that will be pushed into the configuration file. |
161 | | bind_dn | string | | uid=Manager,ou=People,dc=example,dc=com | Bind DN used if "anonymous_bind" set to "False" |
162 | | bind_password | string | | mysecretpassword | Password of the bind_dn user |
163 | | tls_enable | string | yes , no | no | Force TLS encryption. Not necessary with ldaps addresses |
164 | | tls_ca_cert_file | string | | /etc/openvpn/auth/ca.pem | Path to the CA ldap backend. This must have been pushed before |
165 | | tls_cert_file | string | | | Path to client authentication certificate |
166 | | tls_key_file | string | | | Path to client authentication key |
167 | | base_dn | string | | ou=People,dc=example,dc=com | Base DN where the backend will look for valid user |
168 | | search_filter | string | | (&(uid=%u)(accountStatus=active)) | Filter the ldap search |
169 | | require_group | string | False , True | | This is not an Ansible boolean but a string that will be pushed into the configuration file. |
170 | | group_base_dn | string | | ou=Groups,dc=example,dc=com | Precise the group to look for. Required if require_group is set to "True" |
171 | | group_search_filter | string | | ((cn=developers)(cn=artists)) | Precise valid groups |
172 | | verify_client_cert | string | none , optional , require | client-cert-not-required | In OpenVPN 2.4+ `client-cert-not-required` is deprecated. Use `verify-client-cert` instead. |
173 |
174 | # Dependencies
175 | Does not depend on any other roles
176 |
177 | # How to use
178 | ## Example Playbook
179 | Assume you've cloned this repository into the `ansible-role-openvpn` directory. Navigate to the directory one level above this using `..`. In this parent directory, create a new playbook named `ovpn.yaml`.
180 | ```yaml
181 | ---
182 | - hosts: all
183 | gather_facts: true
184 | become: true
185 | roles:
186 | - role: ansible-role-openvpn
187 | openvpn_port: 4300
188 | openvpn_sync_certs: true
189 | clients:
190 | - myclient1
191 | - myclient2
192 | ```
193 |
194 | > **Note:** As the role will need to know the remote used platform (32 or 64 bits), you must set `gather_facts` to `true` in your play.
195 |
196 | Each client listed in the playbook will have a separate configuration generated with unique certificates. Ensure you add as many clients as you have users.
197 |
198 | ## Apply Playbook
199 | SSH key-based authentication is assumed. If using password authentication, add `-kK` to the command.
200 | ```bash
201 | ansible-playbook -u USERNAME -i ip.add.re.ss, ./ovpn.yaml
202 | ```
203 | Where:
204 | - USERNAME — replace USERNAME with your SSH username.
205 | - ip.add.re.ss — replace ip.add.re.ss with the IP address (or DNS name) of your OpenVPN server, followed by a comma.
206 |
207 | ## Configuration files for clients
208 | Client configuration files are copied to the machine where you ran the ansible-playbook command. By default, you'll find them in the `/tmp/ansible` directory.
209 |
210 | ## More examples
211 | More examples will be provided in future documentation, which will be linked here. Stay tuned!
212 |
213 | # Contributing
214 |
215 | Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
216 |
217 | Check out our [contributing guide](CONTRIBUTING.md) to get started.
218 |
219 | Don't forget to give the project a star! Thanks again!
220 |
221 | # License
222 | MIT
223 |
224 | # Author Information
225 | Initially written by Kyle Lexmond
226 |
--------------------------------------------------------------------------------