ansible-playbook playbooks/add_clients.yml --ask-vault-pass -e"clients_to_add=joedoe"
When the playbook is done, look in the fetched_creds directory for a folder with the client name, e.g: joedoe, take the file joedoe-servername-pki-embedded.ovpn, this will be your OpenVPN config file. The passphrase to login is stored in the file servername_pk_pass.txt.
The Openvpn server is on port 443, and its configuration is /etc/openvpn/tcp-443.conf. To start/stop/restart/status the server instance, use the systemctl command, for example:
sudo systemctl start openvpn@tcp-443
Server configuration options are stored locally in ldsi/group_vars/all.yml. After making changes, run the install playbook to update the configuration on the remote server.
ansible-playbook --ask-vault-pass playbooks/install.yml
Ansible role and playbooks for installing openvpn and managing clients.
This is a fork of ansible-openvpn-hardened.
ansible-openvpn-hardened has a lot of cool features when it comes to openvpn and a nice approach to managing client keys. It is however more geared towards configuring a complete system. It is basically setting up a secured, hardened box that runs openvpn from scratch. So it is not really possible to only use parts of it or plug this into one of your existing hosts or.
This project is about setting up openvpn on any kind of system and touching only the parts that are necessary by making as few as possible assumptions about your system / not messing with your current configuration while keeping almost all the cool features found in ansible-openvpn-hardened.
Features kept:
- setting up a OpenVPN PKI using easyrsa3 with certificate revokation, DH parameters and HMAC
- generates multiple OpenVPN client configurations
- support for certificate signing request
- convenient client management via ansible playbooks, e.g. will fetch client config files
- use only TLS ciphers that implement perfect forward secrecy
- OpenVPN configuration aims to enhance security, e.g. use of
tls-auth
,verify-x509-name
orpush block-outside-dns
- CA password is not stored on the OpenVPN host
Features this project adds to that:
- Client state syncing (optional)
- support for running on and managing multiple hosts at once
- support for names (CA, clients) with whitespace
- made more things configurable, e.g. setting the DN_mode or the OpenVPN CN or adding arbitrary OpenVPN configuration to server and/or clients
Things I stripped from ansible-openvpn-hardened, because they are not directly related to OpenVPN or might not be desired by users who run this on existing systems. So this project wil NOT:
- upgrade all packages and install software to periodically update them
- remove any of the installed packages
- install dnsmasq
- modify the iptables firewall rules
- reboot after
install.yml
playbook is finished - install auditd, aide or any other software used for hardening or auditing
Features still to come:
- more configuration options for openvpn
- IPv6 support
However I also had to switch back to running openvpn as a privileged user for now because I ran into too many problems with that, might be changed in the future.
The following Linux distros are tested:
- CentOS 7.2
- Ubuntu 16.04
- Debian 8.7
Other distros and versions may work too. If support for another distro is desired, submit an issue ticket. Pull requests are always welcome.
Copy the sample Ansible inventory and variables to edit for your setup. (I will use my_project
as an example for the rest of this documentation)
cp -r inventories/sample inventories/my_project
Edit the inventory hosts (hosts.ini
) to target your desired host. You can also change the configuration variables in (group_vars/all.yml
), the defaults are however sufficient for this quickstart example.
It is also possible to target multiple hosts each using different variables.
OpenVPN requires some firewall rules to forward packets. By default NO firewall rules will be written/altered.
However you can set load_iptables_rules
to true
and a generated script, that you can find at /etc/openvpn/openvpn_iptables_rules.sh
on the host (after installation finished) will load the minimum required rules into ip(v4)tables. If you opt to not do this you can set the firewall rules by hand. OpenVPN will need at least the MASQUERADE
rule from that script to work.
Run the install playbook
ansible-playbook -i inventories/my_project/hosts.ini playbooks/install.yml
The OpenVPN server is now up and running. Time to add some clients.
When you run the sync_clients.yml
playboook it will sync the desired state (which clients are in the valid_clients
list, by default "phone" and "laptop") with the current state (which clients are currently valid on the OpenVPN host).
Clients that are not desired but currently valid will be revoked.
Clients that are desired but currently not present on the OpenVPN host will be created/added.
NOTE: Once you revoke a client, it is NOT possible to make it valid again, so I suggest using somewhat unique names as valid_clients
.
By default once you run the sync_clients.yml
playbook it will first tell you which clients it will add and revoke before doing it, you will have to manually confirm before it proceeds. You can disable this prompt by setting prompt_before_syncing_clients
to false
.
ansible-playbook playbooks/sync_clients.yml -i inventories/my_project/hosts.ini
After the playbook finished, the credentials will be in the fetched_creds/
directory after the playbook finished succesfully.
You'll be prompted for the private key passphrase, this is stored in a file ending in .txt
in the client directory you just entered in the step above.
Try connecting to the OpenVPN server:
cd fetched_creds/[inventory_hostname]/[client name]/
openvpn [client name]-pki-embedded.ovpn
With the sync_clients.yml
playbook you can maintain state of your clients, even on different hosts, see Targeting multiple hosts and State Management.
To add clients, you can also run the add_clients.yml
playbook. It needs a list named clients_to_add
, see the file used for the tests on how this looks like.
ansible-playbook playbooks/add_clients.yml -i inventories/my_project/hosts.ini -e "@test/ansible-vars/02_add_clients.yml"
The credentials will be in the fetched_creds/
directory after the playbook finished succesfully. Try connecting to the OpenVPN server:
cd fetched_creds/[inventory_hostname]/[client name]/
openvpn [client name]-pki-embedded.ovpn
You'll be prompted for the private key passphrase, this is stored in a file ending in .txt
in the client directory you just entered in the step above.
Three different OpenVPN configuration files are provided because OpenVPN clients on different platforms have different requirements for how the PKI information is referenced by the .ovpn file. This is just for convenience. All the configuration information and PKI info is the same, it's just formatted differently to support different OpenVPN clients.
- PKI embedded - the easiest if your client supports it. Only one file required and all the PKI information is embedded.
XYZ-pki-embedded.ovpn
- PKCS#12 - all the PKI information is stored in the PKCS#12 file and referenced by the config. This can be more secure on Android where the OS can store the information in the PKCS#12 file in hardware backed encrypted storage.
XYZ-pkcs.ovpn
XYZ.p12
- PKI files - if the above two fail, all clients should support this. All of the PKI information is stored in separate files and referenced by the config.
XYZ-pki-files.ovpn
- OpenVPN configurationca.pem
- CA certificateXYZ.key
- client private keyXYZ.pem
- client certificate
All private keys (embedded in config, pkcs, and .key) are encrypted with a passphrase to facilitate secure distribution to client devices.
For maximum security when copying the PKI files and configs to client devices don't copy the .txt file containing the randomly generated passphrase. Enter the passphrase manually onto the device after the key has been transferred.
Entering a pass phrase every time the client is started can be annoying. There are a few options to make this less burdensome after the keys have been securely distributed to the client devices.
- When starting the client, use
openvpn --config [config] --askpass [pass.txt]
if you don't want to enter the password for the private key
From the OpenVPN man page:
If file is specified, read the password from the first line of file. Keep in mind that storing your password in a file to a certain extent invalidates the extra security provided by using an encrypted key.
Clients can also be added using a certificate signing request, CSR. This is useful if you intend to use keys generated and stored in a TPM. Generating the CSR will depend on your hardware, OS, TPM software, etc. If you're interested in this feature, you can probably figure this out (though .travis.yml
has an example of generating a CSR with openssl). This blog post shows how to create private key stored in a TPM and generate a CSR on Windows.
The variable csr_path
specifies the local path to the CSR. cn
specifies the common name specified when the CSR was created.
ansible-playbook -e "csr_path=~/test.csr [email protected]" playbooks/add_clients.yml
This will generate the client's signed certificate and put it in fetched_creds/[server ip]/[cn]/
as well as a nearly complete .ovpn
client configuration file. You'll need to add references to or embed your private key and signed certificate. This will vary based on how your private key is stored. If your following the guide in the blog post mentioned above you'd do this using the OpenVPN option cryptoapicert
.
It is possible to not only target multiple hosts but also use different groups and apply certain configuration variables to that group only. An example:
Consider this hosts.ini
inventory:
[production]
bastion-prod-us-east-1
bastion-prod-us-east-2
[qa]
bastion-qa
[openvpn:children]
production
qa
You can now create a file production.yml
in group_vars/
:
openvpn_key_country: "US"
openvpn_key_province: "Ohio"
openvpn_key_city: "Cleveland"
openvpn_key_org: "FOOBAR CORPORATION"
openvpn_key_ou: "Operations"
openvpn_key_email: "[email protected]"
This configuration will now be applied to hosts in the production
group only and will override variables from the all.yml
.
You can even do this on a per-host level, which will override group-level variables.
E.g. create a file host_vars/bastion-prod-us-east-1.yml
:
openvpn_key_ou: "Operations Unit B"
Further reading: Ansible variable documentation, especially section: "Precedence".
This also comes in handy when managing clients with the sync_clients.yml
playbook because you can then configure which clients are valid on a per-host or per-group basis.
You can use this to manage state by committing and continously updating configuration, especially for client syncing. There are different approaches you can take, here are two suggestions:
- Manage all configuration files (all
inventories/
files) on a separate location, e.g. inside of Jenkins and once these change, trigger a run of the playbook(s), especiallysync_clients.yml
. Disadvantage: You can not easily run this from anywhere else since the configuration files are missing. - Encrypt all configuration files (e.g. using
ansible-vault
), commit them to source control and trigger a run of the playbook(s) after a new commit is pushed.
To revoke clients access, you can run the revoke_clients.yml
playbook. It needs a list named clients_to_revoke
, see the file used for the tests on how this looks like.
ansible-playbook playbooks/revoke_clients.yml -e "@test/ansible-vars/03_revoke_clients.yml"
There is documentation on the most important variables in all.yml.
For the full server configuration, see etc_openvpn_server.conf.j2
-
tls-auth
aids in mitigating risk of denial-of-service attacks. Additionally, when combined with usage of UDP at the transport layer (the default configuration used by ansible-openvpn-hardened), it complicates attempts to port scan the OpenVPN server because any unsigned packets can be immediately dropped without sending anything back to the scanner.-
From the OpenVPN hardening guide:
The tls-auth option uses a static pre-shared key (PSK) that must be generated in advance and shared among all peers. This features adds "extra protection" to the TLS channel by requiring that incoming packets have a valid signature generated using the PSK key... The primary benefit is that an unauthenticated client cannot cause the same CPU/crypto load against a server as the junk traffic can be dropped much sooner. This can aid in mitigating denial-of-service attempts.
-
-
push block-outside-dns
used by OpenVPN server to fix a potential dns leak on Windows 10 -
tls-cipher
limits allowable TLS ciphers to a subset that supports perfect forward secrecy-
From wikipedia:
Forward secrecy protects past sessions against future compromises of secret keys or passwords. If forward secrecy is used, encrypted communications and sessions recorded in the past cannot be retrieved and decrypted should long-term secret keys or passwords be compromised in the future, even if the adversary actively interfered.
-
-
cipher
set toAES-256-CBC
by default -
2048
bit RSA key size by default.- This can be increased to
4096
by changingopenvpn_key_size
indefaults/main.yml
if you don't mind extra processing time. Consensus seems to be that 2048 is sufficient for all but the most sensitive data.
- This can be increased to
For the full client configuration, see client_common.ovpn.j2
verify-x509-name
prevents MitM attacks by verifying the server name in the supplied certificate matches the clients configuration.persist-tun
prevents the traffic from leaking out over the default interface during interruptions and reconnection attempts by keeping the tun device up until connectivity is restored.
- easy-rsa is used to manage the public key infrastructure.
- OpenVPN is configured to read the CRL generated by easy-rsa so that a single client's access can be revoked without having to reissue credentials to all of the clients.
- The private keys generated for the clients and CA are all protected with a randomly generated passphrase to facilitate secure distribution to client devices.
OpenVPN requires some firewall rules to forward packets.
By default NO firewall rules will be written/altered.
However you can set load_iptables_rules
to true
and a generated script, that you can find at /etc/openvpn/openvpn_iptables_rules.sh
on the host (after installation finished) will load the minimum required rules into ip(v4)tables. If you opt to not do this you can set the firewall rules by hand. OpenVPN will need at least the MASQUERADE
rule from that script.
Credentials are generated during the install process and are saved as yml formatted files in the Ansible file hierarchy so they can be used without requiring the playbook caller to take any action. The locations are below.
- CA Private key passphrase - saved in
inventories/my_project/host_vars/[inventory_hostname].yml
Contributions via pull request, feedback, bug reports are all welcome.
- Generate ssh keypair
ssh-keygen -t rsa -f ./test/id_rsa -q -N ""
- Set environment variables, you can change these to target different distributions, see the
matrix
section of the Travis build file.
export docker_concurrent_containers=1;
export distribution=debian; export version=8.7;
export docker_build_image=yes;
export run_opts='--detach --privileged -p 11194:1194/udp --volume=/sys/fs/cgroup:/sys/fs/cgroup';
export init=/bin/systemd;
export ssh=ssh;
- Create vars directories and copy variables
mkdir test/host_vars test/group_vars
cp inventories/sample/group_vars/all.yml test/group_vars/all.yml
- Create the container(s)
./test/docker-setup.sh
Now we have a Debian container running, that has systemd and ssh installed. You could set up a whole bunch of containers this way and test multi-host configurations.
- Install OpenVPN
We can now run the install.yml
playbook.
ansible-playbook playbooks/install.yml --diff --private-key test/id_rsa -i test/docker-inventory -e "load_iptables_rules=true" -e "openvpn_key_size=1024" -e "@test/ansible-vars/01_install_${distribution}.yml"
You can run all other playbooks as well now. Try connecting to the OpenVPN at UDP port 11194 (IPv4) after adding some clients.