├── .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 | --------------------------------------------------------------------------------