├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── Dockerfile ├── Dockerfile.aarch64 ├── LICENSE ├── README.md ├── alpine └── google-authenticator │ └── APKBUILD ├── bin ├── ovpn_copy_server_files ├── ovpn_genconfig ├── ovpn_getclient ├── ovpn_getclient_all ├── ovpn_initpki ├── ovpn_listclients ├── ovpn_otp_user ├── ovpn_revokeclient ├── ovpn_run └── ovpn_status ├── docs ├── advanced.md ├── backup.md ├── clients.md ├── debug.md ├── docker-compose.md ├── docker-openvpn.te ├── docker.md ├── faqs.md ├── ipv6.md ├── otp.md ├── paranoid.md ├── selinux.md ├── static-ips.md ├── systemd.md └── tcp.md ├── init ├── docker-openvpn@.service └── upstart.init ├── otp └── openvpn └── test ├── README.md ├── client └── wait-for-connect.sh ├── config.sh ├── run.sh └── tests ├── basic └── run.sh ├── client ├── container.sh └── run.sh ├── conf_options ├── container.sh └── run.sh ├── docker-build.sh ├── dual-proto └── run.sh ├── image-name.sh ├── iptables └── run.sh ├── otp └── run.sh ├── paranoid ├── container.sh └── run.sh ├── revocation └── run.sh ├── run-bash-in-container.sh └── run-in-container.sh /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Disallowing packages: openvpn 2 | # If you require these packages, please review the package approval process at: https://github.com/travis-ci/apt-package-whitelist#package-approval-process 3 | #addons: 4 | # apt: 5 | # sources: 6 | # - ubuntu-toolchain-r-test 7 | # packages: 8 | # - openvpn 9 | 10 | services: 11 | - docker 12 | 13 | before_install: 14 | - docker --version 15 | 16 | install: 17 | - git clone https://github.com/docker-library/official-images.git official-images 18 | 19 | # Assist with ci test debugging: 20 | # - DEBUG=1 21 | before_script: 22 | - image="kylemanna/openvpn" 23 | - docker build -t "$image" . 24 | - docker inspect "$image" 25 | - docker run --rm "$image" openvpn --version || true # why does it return 1? 26 | - docker run --rm "$image" openssl version 27 | 28 | script: 29 | - official-images/test/run.sh "$image" 30 | - test/run.sh "$image" 31 | 32 | after_script: 33 | - docker images 34 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to docker-openvpn 2 | 3 | Community contributions are welcome and help move the project along. Please review this document before sending any pull requests. 4 | 5 | Thanks! 6 | 7 | ## Bug Fixes 8 | 9 | All bug fixes are welcome. Please try to add a test if the bug is something that should have been fixed already. Oops. 10 | 11 | ## Feature Additions 12 | 13 | New features are welcome provided that the feature has a general audience and is reasonably simple. The goal of the repository is to support a wide audience and be simple enough. 14 | 15 | Please add new documentation in the `docs` folder for any new features. Pull requests for missing documentation is welcome as well. Keep the `README.md` focused on the most popular use case, details belong in the docs directory. 16 | 17 | If you have a special feature, you're likely to try but it will likely be rejected if not too many people seem interested. 18 | 19 | ## Tests 20 | 21 | In an effort to not repeat bugs (and break less popular features), unit tests are run on [Travis CI](https://travis-ci.org/kylemanna/docker-openvpn). The goal of the tests are to be simple and to be placed in the `test/tests` directory where it will be automatically run. Review existing tests for an example. 22 | 23 | ## Style 24 | 25 | The style of the repo follows that of the Linux kernel, in particular: 26 | 27 | * Pull requests should be rebased to small atomic commits so that the merged history is more coherent 28 | * The subject of the commit should be in the form "`: `" 29 | * More details in the body 30 | * Match surrounding coding style (line wrapping, spaces, etc) 31 | 32 | More details in the [SubmittingPatches](https://www.kernel.org/doc/html/latest/process/submitting-patches.html) document included with the Linux kernel. In particular the following sections: 33 | 34 | * `2) Describe your changes` 35 | * `3) Separate your changes` 36 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Original credit: https://github.com/jpetazzo/dockvpn 2 | 3 | # Smallest base image 4 | FROM alpine:latest 5 | 6 | LABEL maintainer="Kyle Manna " 7 | 8 | # Testing: pamtester 9 | RUN echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing/" >> /etc/apk/repositories && \ 10 | apk add --update openvpn iptables bash easy-rsa openvpn-auth-pam google-authenticator pamtester libqrencode && \ 11 | ln -s /usr/share/easy-rsa/easyrsa /usr/local/bin && \ 12 | rm -rf /tmp/* /var/tmp/* /var/cache/apk/* /var/cache/distfiles/* 13 | 14 | # Needed by scripts 15 | ENV OPENVPN=/etc/openvpn 16 | ENV EASYRSA=/usr/share/easy-rsa \ 17 | EASYRSA_CRL_DAYS=3650 \ 18 | EASYRSA_PKI=$OPENVPN/pki 19 | 20 | VOLUME ["/etc/openvpn"] 21 | 22 | # Internally uses port 1194/udp, remap using `docker run -p 443:1194/tcp` 23 | EXPOSE 1194/udp 24 | 25 | CMD ["ovpn_run"] 26 | 27 | ADD ./bin /usr/local/bin 28 | RUN chmod a+x /usr/local/bin/* 29 | 30 | # Add support for OTP authentication using a PAM module 31 | ADD ./otp/openvpn /etc/pam.d/ 32 | -------------------------------------------------------------------------------- /Dockerfile.aarch64: -------------------------------------------------------------------------------- 1 | # Original credit: https://github.com/jpetazzo/dockvpn 2 | 3 | # Smallest base image 4 | FROM aarch64/alpine:3.5 5 | 6 | LABEL maintainer="Kyle Manna " 7 | 8 | RUN echo "http://dl-4.alpinelinux.org/alpine/edge/community/" >> /etc/apk/repositories && \ 9 | echo "http://dl-4.alpinelinux.org/alpine/edge/testing/" >> /etc/apk/repositories && \ 10 | apk add --update openvpn iptables bash easy-rsa openvpn-auth-pam google-authenticator pamtester && \ 11 | ln -s /usr/share/easy-rsa/easyrsa /usr/local/bin && \ 12 | rm -rf /tmp/* /var/tmp/* /var/cache/apk/* /var/cache/distfiles/* 13 | 14 | # Needed by scripts 15 | ENV OPENVPN /etc/openvpn 16 | ENV EASYRSA /usr/share/easy-rsa 17 | ENV EASYRSA_PKI $OPENVPN/pki 18 | 19 | # Prevents refused client connection because of an expired CRL 20 | ENV EASYRSA_CRL_DAYS 3650 21 | 22 | VOLUME ["/etc/openvpn"] 23 | 24 | # Internally uses port 1194/udp, remap using `docker run -p 443:1194/tcp` 25 | EXPOSE 1194/udp 26 | 27 | CMD ["ovpn_run"] 28 | 29 | ADD ./bin /usr/local/bin 30 | RUN chmod a+x /usr/local/bin/* 31 | 32 | # Add support for OTP authentication using a PAM module 33 | ADD ./otp/openvpn /etc/pam.d/ 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Kyle Manna 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenVPN for Docker 2 | 3 | [![Build Status](https://travis-ci.org/kylemanna/docker-openvpn.svg)](https://travis-ci.org/kylemanna/docker-openvpn) 4 | [![Docker Stars](https://img.shields.io/docker/stars/kylemanna/openvpn.svg)](https://hub.docker.com/r/kylemanna/openvpn/) 5 | [![Docker Pulls](https://img.shields.io/docker/pulls/kylemanna/openvpn.svg)](https://hub.docker.com/r/kylemanna/openvpn/) 6 | [![ImageLayers](https://images.microbadger.com/badges/image/kylemanna/openvpn.svg)](https://microbadger.com/#/images/kylemanna/openvpn) 7 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fkylemanna%2Fdocker-openvpn.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fkylemanna%2Fdocker-openvpn?ref=badge_shield) 8 | 9 | 10 | OpenVPN server in a Docker container complete with an EasyRSA PKI CA. 11 | 12 | Extensively tested on [Digital Ocean $5/mo node](http://bit.ly/1C7cKr3) and has 13 | a corresponding [Digital Ocean Community Tutorial](http://bit.ly/1AGUZkq). 14 | 15 | #### Upstream Links 16 | 17 | * Docker Registry @ [kylemanna/openvpn](https://hub.docker.com/r/kylemanna/openvpn/) 18 | * GitHub @ [kylemanna/docker-openvpn](https://github.com/kylemanna/docker-openvpn) 19 | 20 | ## Quick Start 21 | 22 | * Pick a name for the `$OVPN_DATA` data volume container. It's recommended to 23 | use the `ovpn-data-` prefix to operate seamlessly with the reference systemd 24 | service. Users are encourage to replace `example` with a descriptive name of 25 | their choosing. 26 | 27 | OVPN_DATA="ovpn-data-example" 28 | 29 | * Initialize the `$OVPN_DATA` container that will hold the configuration files 30 | and certificates. The container will prompt for a passphrase to protect the 31 | private key used by the newly generated certificate authority. 32 | 33 | docker volume create --name $OVPN_DATA 34 | docker run -v $OVPN_DATA:/etc/openvpn --rm kylemanna/openvpn ovpn_genconfig -u udp://VPN.SERVERNAME.COM 35 | docker run -v $OVPN_DATA:/etc/openvpn --rm -it kylemanna/openvpn ovpn_initpki 36 | 37 | * Start OpenVPN server process 38 | 39 | docker run -v $OVPN_DATA:/etc/openvpn -d -p 1194:1194/udp --cap-add=NET_ADMIN kylemanna/openvpn 40 | 41 | * Generate a client certificate without a passphrase 42 | 43 | docker run -v $OVPN_DATA:/etc/openvpn --rm -it kylemanna/openvpn easyrsa build-client-full CLIENTNAME nopass 44 | 45 | * Retrieve the client configuration with embedded certificates 46 | 47 | docker run -v $OVPN_DATA:/etc/openvpn --rm kylemanna/openvpn ovpn_getclient CLIENTNAME > CLIENTNAME.ovpn 48 | 49 | ## Next Steps 50 | 51 | ### More Reading 52 | 53 | Miscellaneous write-ups for advanced configurations are available in the 54 | [docs](docs) folder. 55 | 56 | ### Systemd Init Scripts 57 | 58 | A `systemd` init script is available to manage the OpenVPN container. It will 59 | start the container on system boot, restart the container if it exits 60 | unexpectedly, and pull updates from Docker Hub to keep itself up to date. 61 | 62 | Please refer to the [systemd documentation](docs/systemd.md) to learn more. 63 | 64 | ### Docker Compose 65 | 66 | If you prefer to use `docker-compose` please refer to the [documentation](docs/docker-compose.md). 67 | 68 | ## Debugging Tips 69 | 70 | * Create an environment variable with the name DEBUG and value of 1 to enable debug output (using "docker -e"). 71 | 72 | docker run -v $OVPN_DATA:/etc/openvpn -p 1194:1194/udp --cap-add=NET_ADMIN -e DEBUG=1 kylemanna/openvpn 73 | 74 | * Test using a client that has openvpn installed correctly 75 | 76 | $ openvpn --config CLIENTNAME.ovpn 77 | 78 | * Run through a barrage of debugging checks on the client if things don't just work 79 | 80 | $ ping 8.8.8.8 # checks connectivity without touching name resolution 81 | $ dig google.com # won't use the search directives in resolv.conf 82 | $ nslookup google.com # will use search 83 | 84 | * Consider setting up a [systemd service](/docs/systemd.md) for automatic 85 | start-up at boot time and restart in the event the OpenVPN daemon or Docker 86 | crashes. 87 | 88 | ## How Does It Work? 89 | 90 | Initialize the volume container using the `kylemanna/openvpn` image with the 91 | included scripts to automatically generate: 92 | 93 | - Diffie-Hellman parameters 94 | - a private key 95 | - a self-certificate matching the private key for the OpenVPN server 96 | - an EasyRSA CA key and certificate 97 | - a TLS auth key from HMAC security 98 | 99 | The OpenVPN server is started with the default run cmd of `ovpn_run` 100 | 101 | The configuration is located in `/etc/openvpn`, and the Dockerfile 102 | declares that directory as a volume. It means that you can start another 103 | container with the `-v` argument, and access the configuration. 104 | The volume also holds the PKI keys and certs so that it could be backed up. 105 | 106 | To generate a client certificate, `kylemanna/openvpn` uses EasyRSA via the 107 | `easyrsa` command in the container's path. The `EASYRSA_*` environmental 108 | variables place the PKI CA under `/etc/openvpn/pki`. 109 | 110 | Conveniently, `kylemanna/openvpn` comes with a script called `ovpn_getclient`, 111 | which dumps an inline OpenVPN client configuration file. This single file can 112 | then be given to a client for access to the VPN. 113 | 114 | To enable Two Factor Authentication for clients (a.k.a. OTP) see [this document](/docs/otp.md). 115 | 116 | ## OpenVPN Details 117 | 118 | We use `tun` mode, because it works on the widest range of devices. 119 | `tap` mode, for instance, does not work on Android, except if the device 120 | is rooted. 121 | 122 | The topology used is `net30`, because it works on the widest range of OS. 123 | `p2p`, for instance, does not work on Windows. 124 | 125 | The UDP server uses`192.168.255.0/24` for dynamic clients by default. 126 | 127 | The client profile specifies `redirect-gateway def1`, meaning that after 128 | establishing the VPN connection, all traffic will go through the VPN. 129 | This might cause problems if you use local DNS recursors which are not 130 | directly reachable, since you will try to reach them through the VPN 131 | and they might not answer to you. If that happens, use public DNS 132 | resolvers like those of Google (8.8.4.4 and 8.8.8.8) or OpenDNS 133 | (208.67.222.222 and 208.67.220.220). 134 | 135 | 136 | ## Security Discussion 137 | 138 | The Docker container runs its own EasyRSA PKI Certificate Authority. This was 139 | chosen as a good way to compromise on security and convenience. The container 140 | runs under the assumption that the OpenVPN container is running on a secure 141 | host, that is to say that an adversary does not have access to the PKI files 142 | under `/etc/openvpn/pki`. This is a fairly reasonable compromise because if an 143 | adversary had access to these files, the adversary could manipulate the 144 | function of the OpenVPN server itself (sniff packets, create a new PKI CA, MITM 145 | packets, etc). 146 | 147 | * The certificate authority key is kept in the container by default for 148 | simplicity. It's highly recommended to secure the CA key with some 149 | passphrase to protect against a filesystem compromise. A more secure system 150 | would put the EasyRSA PKI CA on an offline system (can use the same Docker 151 | image and the script [`ovpn_copy_server_files`](/docs/paranoid.md) to accomplish this). 152 | * It would be impossible for an adversary to sign bad or forged certificates 153 | without first cracking the key's passphase should the adversary have root 154 | access to the filesystem. 155 | * The EasyRSA `build-client-full` command will generate and leave keys on the 156 | server, again possible to compromise and steal the keys. The keys generated 157 | need to be signed by the CA which the user hopefully configured with a passphrase 158 | as described above. 159 | * Assuming the rest of the Docker container's filesystem is secure, TLS + PKI 160 | security should prevent any malicious host from using the VPN. 161 | 162 | 163 | ## Benefits of Running Inside a Docker Container 164 | 165 | ### The Entire Daemon and Dependencies are in the Docker Image 166 | 167 | This means that it will function correctly (after Docker itself is setup) on 168 | all distributions Linux distributions such as: Ubuntu, Arch, Debian, Fedora, 169 | etc. Furthermore, an old stable server can run a bleeding edge OpenVPN server 170 | without having to install/muck with library dependencies (i.e. run latest 171 | OpenVPN with latest OpenSSL on Ubuntu 12.04 LTS). 172 | 173 | ### It Doesn't Stomp All Over the Server's Filesystem 174 | 175 | Everything for the Docker container is contained in two images: the ephemeral 176 | run time image (kylemanna/openvpn) and the `$OVPN_DATA` data volume. To remove 177 | it, remove the corresponding containers, `$OVPN_DATA` data volume and Docker 178 | image and it's completely removed. This also makes it easier to run multiple 179 | servers since each lives in the bubble of the container (of course multiple IPs 180 | or separate ports are needed to communicate with the world). 181 | 182 | ### Some (arguable) Security Benefits 183 | 184 | At the simplest level compromising the container may prevent additional 185 | compromise of the server. There are many arguments surrounding this, but the 186 | take away is that it certainly makes it more difficult to break out of the 187 | container. People are actively working on Linux containers to make this more 188 | of a guarantee in the future. 189 | 190 | ## Differences from jpetazzo/dockvpn 191 | 192 | * No longer uses serveconfig to distribute the configuration via https 193 | * Proper PKI support integrated into image 194 | * OpenVPN config files, PKI keys and certs are stored on a storage 195 | volume for re-use across containers 196 | * Addition of tls-auth for HMAC security 197 | 198 | ## Originally Tested On 199 | 200 | * Docker hosts: 201 | * server a [Digital Ocean](https://www.digitalocean.com/?refcode=d19f7fe88c94) Droplet with 512 MB RAM running Ubuntu 14.04 202 | * Clients 203 | * Android App OpenVPN Connect 1.1.14 (built 56) 204 | * OpenVPN core 3.0 android armv7a thumb2 32-bit 205 | * OS X Mavericks with Tunnelblick 3.4beta26 (build 3828) using openvpn-2.3.4 206 | * ArchLinux OpenVPN pkg 2.3.4-1 207 | 208 | 209 | ## License 210 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fkylemanna%2Fdocker-openvpn.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fkylemanna%2Fdocker-openvpn?ref=badge_large) 211 | -------------------------------------------------------------------------------- /alpine/google-authenticator/APKBUILD: -------------------------------------------------------------------------------- 1 | # Contributor: Fabio Napoleoni 2 | # Maintainer: 3 | pkgname=google-authenticator 4 | pkgver=20160207 5 | pkgrel=1 6 | pkgdesc="Google Authenticator PAM module" 7 | url="https://github.com/google/google-authenticator" 8 | arch="all" 9 | license="ASL 2.0" 10 | depends= 11 | depends_dev= 12 | makedepends="$depends_dev autoconf automake libtool linux-pam-dev m4 openssl-dev" 13 | install= 14 | subpackages="$pkgname-doc" 15 | source="https://github.com/google/google-authenticator/archive/c0404dcdbda9ab9e4f0b8451ecdd44eee8db2425.zip" 16 | 17 | _builddir="$srcdir"/$pkgname-c0404dcdbda9ab9e4f0b8451ecdd44eee8db2425/libpam 18 | 19 | prepare() { 20 | local i 21 | cd "$_builddir" 22 | for i in $source; do 23 | case $i in 24 | *.patch) msg $i; patch -p1 -i "$srcdir"/$i || return 1;; 25 | esac 26 | done 27 | } 28 | 29 | build() { 30 | cd "$_builddir" 31 | ./bootstrap.sh || return 1 32 | ./configure \ 33 | --build=$CBUILD \ 34 | --host=$CHOST \ 35 | --prefix=/usr \ 36 | --libdir=/lib \ 37 | --sysconfdir=/etc \ 38 | --mandir=/usr/share/man \ 39 | --infodir=/usr/share/info \ 40 | || return 1 41 | 42 | make || return 1 43 | } 44 | 45 | package() { 46 | cd "$_builddir" 47 | make DESTDIR="$pkgdir" install || return 1 48 | } 49 | 50 | md5sums="33d3cbd0488bcb4f50b34b5670deffae c0404dcdbda9ab9e4f0b8451ecdd44eee8db2425.zip" 51 | sha256sums="e32abe693e54195bdb6aca52783e6e1c239e67296876ac59211a59e4608338b8 c0404dcdbda9ab9e4f0b8451ecdd44eee8db2425.zip" 52 | sha512sums="b44a626e6cc5d8e27685f5d39b5d33f49fc7070331db7b458d3ee40723972821bb8ed5458f27a287dc664d162acf1f8f9a36ca3b1bf767f2bbf27d4f538e9872 c0404dcdbda9ab9e4f0b8451ecdd44eee8db2425.zip" 53 | -------------------------------------------------------------------------------- /bin/ovpn_copy_server_files: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ## @licence MIT 3 | ## @author Copyright (C) 2015 Robin Schneider 4 | 5 | set -e 6 | 7 | if [ -z "$OPENVPN" ]; then 8 | export OPENVPN="$PWD" 9 | fi 10 | if ! source "$OPENVPN/ovpn_env.sh"; then 11 | echo "Could not source $OPENVPN/ovpn_env.sh." 12 | exit 1 13 | fi 14 | 15 | TARGET="$OPENVPN/server" 16 | if [ -n "$1" ]; then 17 | TARGET="$1" 18 | fi 19 | mkdir -p "${TARGET}" 20 | 21 | ## Ensure that no other keys then the one for the server is present. 22 | rm -rf "$TARGET/pki/private" "$TARGET/pki/issued" 23 | 24 | FILES=( 25 | "openvpn.conf" 26 | "ovpn_env.sh" 27 | "pki/private/${OVPN_CN}.key" 28 | "pki/issued/${OVPN_CN}.crt" 29 | "pki/dh.pem" 30 | "pki/ta.key" 31 | "pki/ca.crt" 32 | "ccd" 33 | ) 34 | 35 | if [ -f "${OPENVPN}/pki/crl.pem" ]; then 36 | FILES+=("pki/crl.pem") 37 | fi 38 | 39 | # Ensure the ccd directory exists, even if empty 40 | mkdir -p "ccd" 41 | 42 | # rsync isn't available to keep size down 43 | # cp --parents isn't in busybox version 44 | # hack the directory structure with tar 45 | tar cf - -C "${OPENVPN}" "${FILES[@]}" | tar xvf - -C "${TARGET}" 46 | 47 | echo "Created the openvpn configuration for the server: $TARGET" 48 | -------------------------------------------------------------------------------- /bin/ovpn_genconfig: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # Generate OpenVPN configs 5 | # 6 | 7 | TMP_PUSH_CONFIGFILE=$(mktemp -t vpn_push.XXXXXXX) 8 | TMP_ROUTE_CONFIGFILE=$(mktemp -t vpn_route.XXXXXXX) 9 | TMP_EXTRA_CONFIGFILE=$(mktemp -t vpn_extra.XXXXXXX) 10 | 11 | #Traceback on Error and Exit come from https://docwhat.org/tracebacks-in-bash/ 12 | set -eu 13 | 14 | _showed_traceback=f 15 | 16 | traceback() { 17 | # Hide the traceback() call. 18 | local -i start=$(( ${1:-0} + 1 )) 19 | local -i end=${#BASH_SOURCE[@]} 20 | local -i i=0 21 | local -i j=0 22 | 23 | echo "Traceback (last called is first):" 1>&2 24 | for ((i=${start}; i < ${end}; i++)); do 25 | j=$(( $i - 1 )) 26 | local function="${FUNCNAME[$i]}" 27 | local file="${BASH_SOURCE[$i]}" 28 | local line="${BASH_LINENO[$j]}" 29 | echo " ${function}() in ${file}:${line}" 1>&2 30 | done 31 | } 32 | 33 | on_error() { 34 | local _ec="$?" 35 | local _cmd="${BASH_COMMAND:-unknown}" 36 | traceback 1 37 | _showed_traceback=t 38 | echo "The command ${_cmd} exited with exit code ${_ec}." 1>&2 39 | } 40 | trap on_error ERR 41 | 42 | 43 | on_exit() { 44 | echo "Cleaning up before Exit ..." 45 | rm -f $TMP_PUSH_CONFIGFILE 46 | rm -f $TMP_ROUTE_CONFIGFILE 47 | rm -f $TMP_EXTRA_CONFIGFILE 48 | local _ec="$?" 49 | if [[ $_ec != 0 && "${_showed_traceback}" != t ]]; then 50 | traceback 1 51 | fi 52 | } 53 | trap on_exit EXIT 54 | 55 | # Convert 1.2.3.4/24 -> 255.255.255.0 56 | cidr2mask() 57 | { 58 | local i 59 | local subnetmask="" 60 | local cidr=${1#*/} 61 | local full_octets=$(($cidr/8)) 62 | local partial_octet=$(($cidr%8)) 63 | 64 | for ((i=0;i<4;i+=1)); do 65 | if [ $i -lt $full_octets ]; then 66 | subnetmask+=255 67 | elif [ $i -eq $full_octets ]; then 68 | subnetmask+=$((256 - 2**(8-$partial_octet))) 69 | else 70 | subnetmask+=0 71 | fi 72 | [ $i -lt 3 ] && subnetmask+=. 73 | done 74 | echo $subnetmask 75 | } 76 | 77 | # Used often enough to justify a function 78 | getroute() { 79 | echo ${1%/*} $(cidr2mask $1) 80 | } 81 | 82 | usage() { 83 | echo "usage: $0 [-d]" 84 | echo " -u SERVER_PUBLIC_URL" 85 | echo " [-e EXTRA_SERVER_CONFIG ]" 86 | echo " [-E EXTRA_CLIENT_CONFIG ]" 87 | echo " [-f FRAGMENT ]" 88 | echo " [-n DNS_SERVER ...]" 89 | echo " [-p PUSH ...]" 90 | echo " [-r ROUTE ...]" 91 | echo " [-s SERVER_SUBNET]" 92 | echo 93 | echo "optional arguments:" 94 | echo " -2 Enable two factor authentication using Google Authenticator." 95 | echo " -a Authenticate packets with HMAC using the given message digest algorithm (auth)." 96 | echo " -b Disable 'push block-outside-dns'" 97 | echo " -c Enable client-to-client option" 98 | echo " -C A list of allowable TLS ciphers delimited by a colon (cipher)." 99 | echo " -d Disable default route" 100 | echo " -D Do not push dns servers" 101 | echo " -k Set keepalive. Default: '10 60'" 102 | echo " -m Set client MTU" 103 | echo " -N Configure NAT to access external server network" 104 | echo " -t Use TAP device (instead of TUN device)" 105 | echo " -T Encrypt packets with the given cipher algorithm instead of the default one (tls-cipher)." 106 | echo " -z Enable comp-lzo compression." 107 | } 108 | 109 | process_route_config() { 110 | local ovpn_route_config='' 111 | ovpn_route_config="$1" 112 | # If user passed "0" skip this, assume no extra routes 113 | [[ "$ovpn_route_config" == "0" ]] && break; 114 | echo "Processing Route Config: '${ovpn_route_config}'" 115 | [[ -n "$ovpn_route_config" ]] && echo "route $(getroute $ovpn_route_config)" >> "$TMP_ROUTE_CONFIGFILE" 116 | } 117 | 118 | process_push_config() { 119 | local ovpn_push_config='' 120 | ovpn_push_config="$1" 121 | echo "Processing PUSH Config: '${ovpn_push_config}'" 122 | [[ -n "$ovpn_push_config" ]] && echo "push \"$ovpn_push_config\"" >> "$TMP_PUSH_CONFIGFILE" 123 | } 124 | 125 | process_extra_config() { 126 | local ovpn_extra_config='' 127 | ovpn_extra_config="$1" 128 | echo "Processing Extra Config: '${ovpn_extra_config}'" 129 | [[ -n "$ovpn_extra_config" ]] && echo "$ovpn_extra_config" >> "$TMP_EXTRA_CONFIGFILE" 130 | } 131 | 132 | if [ "${DEBUG:-}" == "1" ]; then 133 | set -x 134 | fi 135 | 136 | set -e 137 | 138 | if [ -z "${OPENVPN:-}" ]; then 139 | export OPENVPN="$PWD" 140 | fi 141 | if [ -z "${EASYRSA_PKI:-}" ]; then 142 | export EASYRSA_PKI="$OPENVPN/pki" 143 | fi 144 | 145 | OVPN_AUTH='' 146 | OVPN_CIPHER='' 147 | OVPN_CLIENT_TO_CLIENT='' 148 | OVPN_CN='' 149 | OVPN_COMP_LZO=0 150 | OVPN_DEFROUTE=1 151 | OVPN_DEVICE="tun" 152 | OVPN_DEVICEN=0 153 | OVPN_DISABLE_PUSH_BLOCK_DNS=0 154 | OVPN_DNS=1 155 | OVPN_DNS_SERVERS=() 156 | OVPN_ENV=${OPENVPN}/ovpn_env.sh 157 | OVPN_EXTRA_CLIENT_CONFIG=() 158 | OVPN_EXTRA_SERVER_CONFIG=() 159 | OVPN_FRAGMENT='' 160 | OVPN_KEEPALIVE="10 60" 161 | OVPN_MTU='' 162 | OVPN_NAT=0 163 | OVPN_PORT='' 164 | OVPN_PROTO='' 165 | OVPN_PUSH=() 166 | OVPN_ROUTES=() 167 | OVPN_SERVER=192.168.255.0/24 168 | OVPN_SERVER_URL='' 169 | OVPN_TLS_CIPHER='' 170 | 171 | # Import existing configuration if present 172 | [ -r "$OVPN_ENV" ] && source "$OVPN_ENV" 173 | 174 | # Parse arguments 175 | while getopts ":a:e:E:C:T:r:s:du:bcp:n:k:DNm:f:tz2" opt; do 176 | case $opt in 177 | a) 178 | OVPN_AUTH="$OPTARG" 179 | ;; 180 | e) 181 | mapfile -t TMP_EXTRA_SERVER_CONFIG <<< "$OPTARG" 182 | for i in "${TMP_EXTRA_SERVER_CONFIG[@]}"; do 183 | OVPN_EXTRA_SERVER_CONFIG+=("$i") 184 | done 185 | ;; 186 | E) 187 | mapfile -t TMP_EXTRA_CLIENT_CONFIG <<< "$OPTARG" 188 | for i in "${TMP_EXTRA_CLIENT_CONFIG[@]}"; do 189 | OVPN_EXTRA_CLIENT_CONFIG+=("$i") 190 | done 191 | ;; 192 | C) 193 | OVPN_CIPHER="$OPTARG" 194 | ;; 195 | T) 196 | OVPN_TLS_CIPHER="$OPTARG" 197 | ;; 198 | r) 199 | mapfile -t TMP_ROUTES <<< "$OPTARG" 200 | for i in "${TMP_ROUTES[@]}"; do 201 | OVPN_ROUTES+=("$i") 202 | done 203 | ;; 204 | s) 205 | OVPN_SERVER="$OPTARG" 206 | ;; 207 | d) 208 | OVPN_DEFROUTE=0 209 | OVPN_DISABLE_PUSH_BLOCK_DNS=1 210 | ;; 211 | u) 212 | OVPN_SERVER_URL="$OPTARG" 213 | ;; 214 | b) 215 | OVPN_DISABLE_PUSH_BLOCK_DNS=1 216 | ;; 217 | c) 218 | OVPN_CLIENT_TO_CLIENT=1 219 | ;; 220 | p) 221 | mapfile -t TMP_PUSH <<< "$OPTARG" 222 | for i in "${TMP_PUSH[@]}"; do 223 | OVPN_PUSH+=("$i") 224 | done 225 | ;; 226 | n) 227 | mapfile -t TMP_DNS_SERVERS <<< "$OPTARG" 228 | for i in "${TMP_DNS_SERVERS[@]}"; do 229 | OVPN_DNS_SERVERS+=("$i") 230 | done 231 | ;; 232 | D) 233 | OVPN_DNS=0 234 | ;; 235 | N) 236 | OVPN_NAT=1 237 | ;; 238 | k) 239 | OVPN_KEEPALIVE="$OPTARG" 240 | ;; 241 | m) 242 | OVPN_MTU="$OPTARG" 243 | ;; 244 | t) 245 | OVPN_DEVICE="tap" 246 | ;; 247 | z) 248 | OVPN_COMP_LZO=1 249 | ;; 250 | 2) 251 | OVPN_OTP_AUTH=1 252 | ;; 253 | f) 254 | OVPN_FRAGMENT="$OPTARG" 255 | ;; 256 | \?) 257 | set +x 258 | echo "Invalid option: -$OPTARG" >&2 259 | usage 260 | exit 1 261 | ;; 262 | :) 263 | set +x 264 | echo "Option -$OPTARG requires an argument." >&2 265 | usage 266 | exit 1 267 | ;; 268 | esac 269 | done 270 | 271 | # Create ccd directory for static routes 272 | [ ! -d "${OPENVPN:-}/ccd" ] && mkdir -p ${OPENVPN:-}/ccd 273 | 274 | # Server name is in the form "udp://vpn.example.com:1194" 275 | if [[ "${OVPN_SERVER_URL:-}" =~ ^((udp|tcp|udp6|tcp6)://)?([0-9a-zA-Z\.\-]+)(:([0-9]+))?$ ]]; then 276 | OVPN_PROTO=${BASH_REMATCH[2]}; 277 | OVPN_CN=${BASH_REMATCH[3]}; 278 | OVPN_PORT=${BASH_REMATCH[5]}; 279 | else 280 | set +x 281 | echo "Common name not specified, see '-u'" 282 | usage 283 | exit 1 284 | fi 285 | 286 | # Apply defaults. If dns servers were not defined with -n, use google nameservers 287 | set +u 288 | [ -z "$OVPN_DNS_SERVERS" ] && OVPN_DNS_SERVERS=("8.8.8.8" "8.8.4.4") 289 | [ -z "$OVPN_PROTO" ] && OVPN_PROTO=udp 290 | [ -z "$OVPN_PORT" ] && OVPN_PORT=1194 291 | set -u 292 | [ "${#OVPN_ROUTES[@]}" == "0" ] && [ "$OVPN_DEFROUTE" == "1" ] && OVPN_ROUTES+=("192.168.254.0/24") 293 | 294 | # Preserve config 295 | if [ -f "$OVPN_ENV" ]; then 296 | bak_env=$OVPN_ENV.$(date +%s).bak 297 | echo "Backing up $OVPN_ENV -> $bak_env" 298 | mv "$OVPN_ENV" "$bak_env" 299 | fi 300 | 301 | # Save the current OVPN_ vars to the ovpn_env.sh file 302 | (set | grep '^OVPN_') | while read -r var; do 303 | echo "declare -x $var" >> "$OVPN_ENV" 304 | done 305 | 306 | conf=${OPENVPN:-}/openvpn.conf 307 | if [ -f "$conf" ]; then 308 | bak=$conf.$(date +%s).bak 309 | echo "Backing up $conf -> $bak" 310 | mv "$conf" "$bak" 311 | fi 312 | 313 | # Echo extra client configurations 314 | if [ ${#OVPN_EXTRA_CLIENT_CONFIG[@]} -gt 0 ]; then 315 | for i in "${OVPN_EXTRA_CLIENT_CONFIG[@]}"; do 316 | echo "Processing Extra Client Config: $i" 317 | done 318 | fi 319 | 320 | cat > "$conf" <> "$conf" 350 | [ -n "$OVPN_CIPHER" ] && echo "cipher $OVPN_CIPHER" >> "$conf" 351 | [ -n "$OVPN_AUTH" ] && echo "auth $OVPN_AUTH" >> "$conf" 352 | 353 | [ -n "${OVPN_CLIENT_TO_CLIENT:-}" ] && echo "client-to-client" >> "$conf" 354 | [ "$OVPN_COMP_LZO" == "1" ] && echo "comp-lzo" >> "$conf" 355 | [ "$OVPN_COMP_LZO" == "0" ] && echo "comp-lzo no" >> "$conf" 356 | 357 | [ -n "${OVPN_FRAGMENT:-}" ] && echo "fragment $OVPN_FRAGMENT" >> "$conf" 358 | 359 | # Append route commands 360 | if [ ${#OVPN_ROUTES[@]} -gt 0 ]; then 361 | for i in "${OVPN_ROUTES[@]}"; do 362 | process_route_config "$i" 363 | done 364 | echo -e "\n### Route Configurations Below" >> "$conf" 365 | cat $TMP_ROUTE_CONFIGFILE >> "$conf" 366 | fi 367 | 368 | # Append push commands 369 | [ "$OVPN_DNS" == "1" ] && for i in "${OVPN_DNS_SERVERS[@]}"; do 370 | process_push_config "dhcp-option DNS $i" 371 | done 372 | 373 | if [ "$OVPN_COMP_LZO" == "0" ]; then 374 | process_push_config "comp-lzo no" 375 | fi 376 | 377 | [ ${#OVPN_PUSH[@]} -gt 0 ] && for i in "${OVPN_PUSH[@]}"; do 378 | process_push_config "$i" 379 | done 380 | 381 | echo -e "\n### Push Configurations Below" >> "$conf" 382 | cat $TMP_PUSH_CONFIGFILE >> "$conf" 383 | 384 | # Append optional OTP authentication support 385 | if [ -n "${OVPN_OTP_AUTH:-}" ]; then 386 | echo -e "\n\n# Enable OTP+PAM for user authentication" >> "$conf" 387 | echo "plugin /usr/lib/openvpn/plugins/openvpn-plugin-auth-pam.so openvpn" >> "$conf" 388 | echo "reneg-sec 0" >> "$conf" 389 | fi 390 | 391 | # Append extra server configurations 392 | if [ ${#OVPN_EXTRA_SERVER_CONFIG[@]} -gt 0 ]; then 393 | for i in "${OVPN_EXTRA_SERVER_CONFIG[@]}"; do 394 | process_extra_config "$i" 395 | done 396 | echo -e "\n### Extra Configurations Below" >> "$conf" 397 | cat $TMP_EXTRA_CONFIGFILE >> "$conf" 398 | fi 399 | 400 | set +e 401 | 402 | # Clean-up duplicate configs 403 | if diff -q "${bak_env:-}" "$OVPN_ENV" 2>/dev/null; then 404 | echo "Removing duplicate back-up: $bak_env" 405 | rm -fv "$bak_env" 406 | fi 407 | if diff -q "${bak:-}" "$conf" 2>/dev/null; then 408 | echo "Removing duplicate back-up: $bak" 409 | rm -fv "$bak" 410 | fi 411 | 412 | echo "Successfully generated config" 413 | -------------------------------------------------------------------------------- /bin/ovpn_getclient: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # Get an OpenVPN client configuration file 5 | # 6 | 7 | if [ "$DEBUG" == "1" ]; then 8 | set -x 9 | fi 10 | 11 | set -e 12 | 13 | if [ -z "$OPENVPN" ]; then 14 | export OPENVPN="$PWD" 15 | fi 16 | if ! source "$OPENVPN/ovpn_env.sh"; then 17 | echo "Could not source $OPENVPN/ovpn_env.sh." 18 | exit 1 19 | fi 20 | if [ -z "$EASYRSA_PKI" ]; then 21 | export EASYRSA_PKI="$OPENVPN/pki" 22 | fi 23 | 24 | cn="$1" 25 | parm="$2" 26 | 27 | if [ ! -f "$EASYRSA_PKI/private/${cn}.key" ]; then 28 | echo "Unable to find \"${cn}\", please try again or generate the key first" >&2 29 | exit 1 30 | fi 31 | 32 | get_client_config() { 33 | mode="$1" 34 | echo " 35 | client 36 | nobind 37 | dev $OVPN_DEVICE 38 | remote-cert-tls server 39 | 40 | remote $OVPN_CN $OVPN_PORT $OVPN_PROTO" 41 | if [ "$OVPN_PROTO" == "udp6" ]; then 42 | echo "remote $OVPN_CN $OVPN_PORT udp" 43 | fi 44 | if [ "$OVPN_PROTO" == "tcp6" ]; then 45 | echo "remote $OVPN_CN $OVPN_PORT tcp" 46 | fi 47 | for i in "${OVPN_EXTRA_CLIENT_CONFIG[@]}"; do 48 | echo "$i" 49 | done 50 | if [ "$mode" == "combined" ]; then 51 | echo " 52 | 53 | $(cat $EASYRSA_PKI/private/${cn}.key) 54 | 55 | 56 | $(openssl x509 -in $EASYRSA_PKI/issued/${cn}.crt) 57 | 58 | 59 | $(cat $EASYRSA_PKI/ca.crt) 60 | 61 | key-direction 1 62 | 63 | $(cat $EASYRSA_PKI/ta.key) 64 | 65 | " 66 | elif [ "$mode" == "separated" ]; then 67 | echo " 68 | key ${cn}.key 69 | ca ca.crt 70 | cert ${cn}.crt 71 | tls-auth ta.key 1 72 | " 73 | fi 74 | 75 | if [ "$OVPN_DEFROUTE" != "0" ];then 76 | echo "redirect-gateway def1" 77 | fi 78 | 79 | if [ -n "$OVPN_MTU" ]; then 80 | echo "tun-mtu $OVPN_MTU" 81 | fi 82 | 83 | if [ -n "$OVPN_TLS_CIPHER" ]; then 84 | echo "tls-cipher $OVPN_TLS_CIPHER" 85 | fi 86 | 87 | if [ -n "$OVPN_CIPHER" ]; then 88 | echo "cipher $OVPN_CIPHER" 89 | fi 90 | 91 | if [ -n "$OVPN_AUTH" ]; then 92 | echo "auth $OVPN_AUTH" 93 | fi 94 | 95 | if [ -n "$OVPN_OTP_AUTH" ]; then 96 | echo "auth-user-pass" 97 | echo "auth-nocache" 98 | fi 99 | 100 | if [ "$OVPN_COMP_LZO" == "1" ]; then 101 | echo "comp-lzo" 102 | fi 103 | 104 | if [ -n "$OVPN_OTP_AUTH" ]; then 105 | echo reneg-sec 0 106 | fi 107 | } 108 | 109 | dir="$OPENVPN/clients/$cn" 110 | case "$parm" in 111 | "separated") 112 | mkdir -p "$dir" 113 | get_client_config "$parm" > "$dir/${cn}.ovpn" 114 | cp "$EASYRSA_PKI/private/${cn}.key" "$dir/${cn}.key" 115 | cp "$EASYRSA_PKI/ca.crt" "$dir/ca.crt" 116 | cp "$EASYRSA_PKI/issued/${cn}.crt" "$dir/${cn}.crt" 117 | cp "$EASYRSA_PKI/ta.key" "$dir/ta.key" 118 | ;; 119 | "" | "combined") 120 | get_client_config "combined" 121 | ;; 122 | "combined-save") 123 | mkdir -p "$dir" 124 | get_client_config "combined" > "$dir/${cn}-combined.ovpn" 125 | ;; 126 | *) 127 | echo "This script can produce the client configuration in two formats:" >&2 128 | echo " 1. combined (default): All needed configuration and cryptographic material is in one file (Use \"combined-save\" to write the configuration file in the same path as the separated parameter does)." >&2 129 | echo " 2. separated: Separated files." >&2 130 | echo "Please specify one of those options as second parameter." >&2 131 | ;; 132 | esac 133 | -------------------------------------------------------------------------------- /bin/ovpn_getclient_all: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ## @licence MIT 3 | ## @author Copyright (C) 2015 Robin Schneider 4 | 5 | if [ -z "$OPENVPN" ]; then 6 | export OPENVPN="$PWD" 7 | fi 8 | if ! source "$OPENVPN/ovpn_env.sh"; then 9 | echo "Could not source $OPENVPN/ovpn_env.sh." 10 | exit 1 11 | fi 12 | if [ -z "$EASYRSA_PKI" ]; then 13 | export EASYRSA_PKI="$OPENVPN/pki" 14 | fi 15 | 16 | pushd "$EASYRSA_PKI" 17 | for name in issued/*.crt; do 18 | name=${name%.crt} 19 | name=${name#issued/} 20 | if [ "$name" != "$OVPN_CN" ]; then 21 | ovpn_getclient "$name" separated 22 | ovpn_getclient "$name" combined-save 23 | fi 24 | done 25 | popd 26 | -------------------------------------------------------------------------------- /bin/ovpn_initpki: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # Initialize the EasyRSA PKI 5 | # 6 | 7 | if [ "$DEBUG" == "1" ]; then 8 | set -x 9 | fi 10 | 11 | set -e 12 | 13 | source "$OPENVPN/ovpn_env.sh" 14 | 15 | # Specify "nopass" as arg[2] to make the CA insecure (not recommended!) 16 | nopass=$1 17 | 18 | # Provides a sufficient warning before erasing pre-existing files 19 | easyrsa init-pki 20 | 21 | # CA always has a password for protection in event server is compromised. The 22 | # password is only needed to sign client/server certificates. No password is 23 | # needed for normal OpenVPN operation. 24 | easyrsa build-ca $nopass 25 | 26 | easyrsa gen-dh 27 | openvpn --genkey --secret $EASYRSA_PKI/ta.key 28 | 29 | # Was nice to autoset, but probably a bad idea in practice, users should 30 | # have to explicitly specify the common name of their server 31 | #if [ -z "$cn"]; then 32 | # #TODO: Handle IPv6 (when I get a VPS with IPv6)... 33 | # ip4=$(dig +short myip.opendns.com @resolver1.opendns.com) 34 | # ptr=$(dig +short -x $ip4 | sed -e 's:\.$::') 35 | # 36 | # [ -n "$ptr" ] && cn=$ptr || cn=$ip4 37 | #fi 38 | 39 | # For a server key with a password, manually init; this is autopilot 40 | easyrsa build-server-full "$OVPN_CN" nopass 41 | 42 | # Generate the CRL for client/server certificates revocation. 43 | easyrsa gen-crl 44 | -------------------------------------------------------------------------------- /bin/ovpn_listclients: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -z "$OPENVPN" ]; then 4 | export OPENVPN="$PWD" 5 | fi 6 | if ! source "$OPENVPN/ovpn_env.sh"; then 7 | echo "Could not source $OPENVPN/ovpn_env.sh." 8 | exit 1 9 | fi 10 | if [ -z "$EASYRSA_PKI" ]; then 11 | export EASYRSA_PKI="$OPENVPN/pki" 12 | fi 13 | 14 | cd "$EASYRSA_PKI" 15 | 16 | if [ -e crl.pem ]; then 17 | cat ca.crt crl.pem > cacheck.pem 18 | else 19 | cat ca.crt > cacheck.pem 20 | fi 21 | 22 | echo "name,begin,end,status" 23 | for name in issued/*.crt; do 24 | path=$name 25 | begin=$(openssl x509 -noout -startdate -in $path | awk -F= '{ print $2 }') 26 | end=$(openssl x509 -noout -enddate -in $path | awk -F= '{ print $2 }') 27 | 28 | name=${name%.crt} 29 | name=${name#issued/} 30 | if [ "$name" != "$OVPN_CN" ]; then 31 | # check for revocation or expiration 32 | command="openssl verify -crl_check -CAfile cacheck.pem $path" 33 | result=$($command) 34 | if [ $(echo "$result" | wc -l) == 1 ] && [ "$(echo "$result" | grep ": OK")" ]; then 35 | status="VALID" 36 | else 37 | result=$(echo "$result" | tail -n 1 | grep error | cut -d" " -f2) 38 | case $result in 39 | 10) 40 | status="EXPIRED" 41 | ;; 42 | 23) 43 | status="REVOKED" 44 | ;; 45 | *) 46 | status="INVALID" 47 | esac 48 | fi 49 | echo "$name,$begin,$end,$status" 50 | fi 51 | done 52 | 53 | # Clean 54 | rm cacheck.pem 55 | -------------------------------------------------------------------------------- /bin/ovpn_otp_user: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # Generate OpenVPN users via google authenticator 5 | # 6 | 7 | if ! source "$OPENVPN/ovpn_env.sh"; then 8 | echo "Could not source $OPENVPN/ovpn_env.sh." 9 | exit 1 10 | fi 11 | 12 | if [ "x$OVPN_OTP_AUTH" != "x1" ]; then 13 | echo "OTP authentication not enabled, please regenerate configuration using -2 flag" 14 | exit 1 15 | fi 16 | 17 | if [ -z $1 ]; then 18 | echo "Usage: ovpn_otp_user USERNAME" 19 | exit 1 20 | fi 21 | 22 | # Ensure the otp folder is present 23 | [ -d /etc/openvpn/otp ] || mkdir -p /etc/openvpn/otp 24 | 25 | # Binary is present in image, save an $user.google_authenticator file in /etc/openvpn/otp 26 | if [ "$2" == "interactive" ]; then 27 | # Authenticator will ask for other parameters. User can choose rate limit, token reuse policy and time window policy 28 | # Always use time base OTP otherwise storage for counters must be configured somewhere in volume 29 | google-authenticator --time-based --force -l "${1}@${OVPN_CN}" -s /etc/openvpn/otp/${1}.google_authenticator 30 | else 31 | # Skip confirmation if not running in interctive mode. Essential for integration tests. 32 | google-authenticator --time-based --disallow-reuse --force --rate-limit=3 --rate-time=30 --window-size=3 \ 33 | -l "${1}@${OVPN_CN}" -s /etc/openvpn/otp/${1}.google_authenticator --no-confirm 34 | fi 35 | -------------------------------------------------------------------------------- /bin/ovpn_revokeclient: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # Revoke a client certificate 5 | # 6 | 7 | if [ "$DEBUG" == "1" ]; then 8 | set -x 9 | fi 10 | 11 | set -e 12 | 13 | if [ -z "$OPENVPN" ]; then 14 | export OPENVPN="$PWD" 15 | fi 16 | if ! source "$OPENVPN/ovpn_env.sh"; then 17 | echo "Could not source $OPENVPN/ovpn_env.sh." 18 | exit 1 19 | fi 20 | if [ -z "$EASYRSA_PKI" ]; then 21 | export EASYRSA_PKI="$OPENVPN/pki" 22 | fi 23 | 24 | cn="$1" 25 | 26 | if [ ! -f "$EASYRSA_PKI/private/${cn}.key" ]; then 27 | echo "Unable to find \"${cn}\", please try again or generate the key first" >&2 28 | exit 1 29 | fi 30 | 31 | revoke_client_certificate(){ 32 | easyrsa revoke "$1" 33 | echo "Generating the Certificate Revocation List :" 34 | easyrsa gen-crl 35 | cp -f "$EASYRSA_PKI/crl.pem" "$OPENVPN/crl.pem" 36 | chmod 644 "$OPENVPN/crl.pem" 37 | } 38 | 39 | revoke_client_certificate "$cn" 40 | -------------------------------------------------------------------------------- /bin/ovpn_run: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # Run the OpenVPN server normally 5 | # 6 | 7 | if [ "$DEBUG" == "1" ]; then 8 | set -x 9 | fi 10 | 11 | set -e 12 | 13 | cd $OPENVPN 14 | 15 | # Build runtime arguments array based on environment 16 | USER_ARGS=("${@}") 17 | ARGS=() 18 | 19 | # Checks if ARGS already contains the given value 20 | function hasArg { 21 | local element 22 | for element in "${@:2}"; do 23 | [ "${element}" == "${1}" ] && return 0 24 | done 25 | return 1 26 | } 27 | 28 | # Adds the given argument if it's not already specified. 29 | function addArg { 30 | local arg="${1}" 31 | [ $# -ge 1 ] && local val="${2}" 32 | if ! hasArg "${arg}" "${USER_ARGS[@]}"; then 33 | ARGS+=("${arg}") 34 | [ $# -ge 1 ] && ARGS+=("${val}") 35 | fi 36 | } 37 | 38 | # set up iptables rules and routing 39 | # this allows rules/routing to be altered by supplying this function 40 | # in an included file, such as ovpn_env.sh 41 | function setupIptablesAndRouting { 42 | iptables -t nat -C POSTROUTING -s $OVPN_SERVER -o $OVPN_NATDEVICE -j MASQUERADE 2>/dev/null || { 43 | iptables -t nat -A POSTROUTING -s $OVPN_SERVER -o $OVPN_NATDEVICE -j MASQUERADE 44 | } 45 | for i in "${OVPN_ROUTES[@]}"; do 46 | iptables -t nat -C POSTROUTING -s "$i" -o $OVPN_NATDEVICE -j MASQUERADE 2>/dev/null || { 47 | iptables -t nat -A POSTROUTING -s "$i" -o $OVPN_NATDEVICE -j MASQUERADE 48 | } 49 | done 50 | } 51 | 52 | 53 | addArg "--config" "$OPENVPN/openvpn.conf" 54 | 55 | source "$OPENVPN/ovpn_env.sh" 56 | 57 | mkdir -p /dev/net 58 | if [ ! -c /dev/net/tun ]; then 59 | mknod /dev/net/tun c 10 200 60 | fi 61 | 62 | if [ -d "$OPENVPN/ccd" ]; then 63 | addArg "--client-config-dir" "$OPENVPN/ccd" 64 | fi 65 | 66 | # When using --net=host, use this to specify nat device. 67 | [ -z "$OVPN_NATDEVICE" ] && OVPN_NATDEVICE=eth0 68 | 69 | # Setup NAT forwarding if requested 70 | if [ "$OVPN_DEFROUTE" != "0" ] || [ "$OVPN_NAT" == "1" ] ; then 71 | # call function to setup iptables rules and routing 72 | # this allows rules to be customized by supplying 73 | # a replacement function in, for example, ovpn_env.sh 74 | setupIptablesAndRouting 75 | fi 76 | 77 | # Use a copy of crl.pem as the CRL Needs to be readable by the user/group 78 | # OpenVPN is running as. Only pass arguments to OpenVPN if it's found. 79 | if [ "$EASYRSA_PKI/crl.pem" -nt "$OPENVPN/crl.pem" ]; then 80 | cp -f "$EASYRSA_PKI/crl.pem" "$OPENVPN/crl.pem" 81 | chmod 644 "$OPENVPN/crl.pem" 82 | fi 83 | 84 | if [ -r "$OPENVPN/crl.pem" ]; then 85 | addArg "--crl-verify" "$OPENVPN/crl.pem" 86 | fi 87 | 88 | ip -6 route show default 2>/dev/null 89 | if [ $? = 0 ]; then 90 | echo "Checking IPv6 Forwarding" 91 | if [ "$( CLIENTNAME.ovpn 17 | 18 | * Start the server with: 19 | 20 | docker run -v $PWD:/etc/openvpn -d -p 1194:1194/udp --cap-add=NET_ADMIN kylemanna/openvpn 21 | -------------------------------------------------------------------------------- /docs/backup.md: -------------------------------------------------------------------------------- 1 | # Backing Up Configuration and Certificates 2 | 3 | ## Security 4 | 5 | The resulting archive from this backup contains all credential to impersonate the server at a minimum. If the client's private keys are generated using the EasyRSA utility then it also contains the client certificates that could be used to impersonate said clients. Most importantly, if the certificate authority key is in this archive (as it is given the quick start directions), then a adversary could generate certificates at will. 6 | 7 | I'd recommend encrypting the archive with something strong (e.g. gpg or openssl + AES). For the paranoid keep backup offline. For the [truly paranoid users](/docs/paranoid.md), never keep any keys (i.e. client and certificate authority) in the docker container to begin with :). 8 | 9 | 10 | **TL;DR Protect the resulting archive file. Ensure there is very limited access to it.** 11 | 12 | ## Backup to Archive 13 | 14 | docker run -v $OVPN_DATA:/etc/openvpn --rm kylemanna/openvpn tar -cvf - -C /etc openvpn | xz > openvpn-backup.tar.xz 15 | 16 | ## Restore to New Data Volume 17 | 18 | Creates an volume container named `$OVPN_DATA` to extract the data to. 19 | 20 | docker volume create --name $OVPN_DATA 21 | xzcat openvpn-backup.tar.xz | docker run -v $OVPN_DATA:/etc/openvpn -i kylemanna/openvpn tar -xvf - -C /etc 22 | -------------------------------------------------------------------------------- /docs/clients.md: -------------------------------------------------------------------------------- 1 | # Advanced Client Management 2 | 3 | ## Client Configuration Mode 4 | 5 | The [`ovpn_getclient`](/bin/ovpn_getclient) can produce two different versions of the configuration. 6 | 7 | 1. combined (default): All needed configuration and cryptographic material is in one file (Use "combined-save" to write the configuration file in the same path as the separated parameter does). 8 | 2. separated: Separated files. 9 | 10 | Note that some client software might be picky about which configuration format it accepts. 11 | 12 | ## Client List 13 | 14 | See an overview of the configured clients, including revocation and expiration status: 15 | 16 | docker run --rm -it -v $OVPN_DATA:/etc/openvpn kylemanna/openvpn ovpn_listclients 17 | 18 | The output is generated using `openssl verify`. Error codes from the verification process different from `X509_V_ERR_CERT_HAS_EXPIRED` or `X509_V_ERR_CERT_REVOKED` will show the status `INVALID`. 19 | 20 | ## Batch Mode 21 | 22 | If you have more than a few clients, you will want to generate and update your client configuration in batch. For this task the script [`ovpn_getclient_all`](/bin/ovpn_getclient_all) was written, which writes out the configuration for each client to a separate directory called `clients/$cn`. 23 | 24 | Execute the following to generate the configuration for all clients: 25 | 26 | docker run --rm -it -v $OVPN_DATA:/etc/openvpn --volume /tmp/openvpn_clients:/etc/openvpn/clients kylemanna/openvpn ovpn_getclient_all 27 | 28 | After doing so, you will find the following files in each of the `$cn` directories: 29 | 30 | ca.crt 31 | $cn-combined.ovpn # Combined configuration file format. If your client recognices this file then only this file is needed. 32 | $cn.ovpn # Separated configuration. This configuration file requires the other files ca.crt dh.pem $cn.crt $cn.key ta.key 33 | $cn.crt 34 | $cn.key 35 | ta.key 36 | 37 | ## Revoking Client Certificates 38 | 39 | Revoke `client1`'s certificate and generate the certificate revocation list (CRL) using [`ovpn_revokeclient`](/bin/ovpn_revokeclient) script : 40 | 41 | docker run --rm -it -v $OVPN_DATA:/etc/openvpn kylemanna/openvpn ovpn_revokeclient client1 42 | 43 | The OpenVPN server will read this change every time a client connects (no need to restart server) and deny clients access using revoked certificates. 44 | 45 | You can optionally pass `remove` as second parameter to ovpn_revokeclient to remove the corresponding crt, key and req files : 46 | 47 | docker run --rm -it -v $OVPN_DATA:/etc/openvpn kylemanna/openvpn ovpn_revokeclient client1 remove 48 | -------------------------------------------------------------------------------- /docs/debug.md: -------------------------------------------------------------------------------- 1 | # Debugging 2 | 3 | Random things I do to debug the containers. 4 | 5 | ## Login Shells 6 | 7 | * Create a shell in the running docker container with `docker exec`. 8 | * To modify the data, you can also mount the data container and modify it with 9 | 10 | docker run --rm -it -v $OVPN_DATA:/etc/openvpn kylemanna/openvpn bash -l 11 | 12 | ## Stream OpenVPN Logs 13 | 14 | 1. Get the container's name or container ID: 15 | 16 | root@vpn:~/docker-openvpn# docker ps 17 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 18 | ed335aaa9b82 kylemanna/openvpn:latest ovpn_run 5 minutes ago Up 5 minutes 0.0.0.0:1194->1194/udp sad_lovelace 19 | 20 | 2. Tail the logs: 21 | 22 | root@vpn:~/docker-openvpn# docker logs -f sad_lovelace 23 | + mkdir -p /dev/net 24 | + [ ! -c /dev/net/tun ] 25 | + mknod /dev/net/tun c 10 200 26 | + [ ! -d /etc/openvpn/ccd ] 27 | + iptables -t nat -A POSTROUTING -s 192.168.254.0/24 -o eth0 -j MASQUERADE 28 | + iptables -t nat -A POSTROUTING -s 192.168.255.0/24 -o eth0 -j MASQUERADE 29 | + conf=/etc/openvpn/openvpn.conf 30 | + [ ! -s /etc/openvpn/openvpn.conf ] 31 | + conf=/etc/openvpn/udp1194.conf 32 | + openvpn --config /etc/openvpn/udp1194.conf 33 | Tue Jul 1 06:56:48 2014 OpenVPN 2.3.2 x86_64-pc-linux-gnu [SSL (OpenSSL)] [LZO] [EPOLL] [PKCS11] [eurephia] [MH] [IPv6] built on Mar 17 2014 34 | Tue Jul 1 06:56:49 2014 Diffie-Hellman initialized with 2048 bit key 35 | Tue Jul 1 06:56:49 2014 Control Channel Authentication: using '/etc/openvpn/pki/ta.key' as a OpenVPN static key file 36 | Tue Jul 1 06:56:49 2014 Outgoing Control Channel Authentication: Using 160 bit message hash 'SHA1' for HMAC authentication 37 | Tue Jul 1 06:56:49 2014 Incoming Control Channel Authentication: Using 160 bit message hash 'SHA1' for HMAC authentication 38 | Tue Jul 1 06:56:49 2014 Socket Buffers: R=[212992->131072] S=[212992->131072] 39 | 40 | -------------------------------------------------------------------------------- /docs/docker-compose.md: -------------------------------------------------------------------------------- 1 | # Quick Start with docker-compose 2 | 3 | * Add a new service in docker-compose.yml 4 | 5 | ```yaml 6 | version: '2' 7 | services: 8 | openvpn: 9 | cap_add: 10 | - NET_ADMIN 11 | image: kylemanna/openvpn 12 | container_name: openvpn 13 | ports: 14 | - "1194:1194/udp" 15 | restart: always 16 | volumes: 17 | - ./openvpn-data/conf:/etc/openvpn 18 | ``` 19 | 20 | 21 | * Initialize the configuration files and certificates 22 | 23 | ```bash 24 | docker-compose run --rm openvpn ovpn_genconfig -u udp://VPN.SERVERNAME.COM 25 | docker-compose run --rm openvpn ovpn_initpki 26 | ``` 27 | 28 | * Fix ownership (depending on how to handle your backups, this may not be needed) 29 | 30 | ```bash 31 | sudo chown -R $(whoami): ./openvpn-data 32 | ``` 33 | 34 | * Start OpenVPN server process 35 | 36 | ```bash 37 | docker-compose up -d openvpn 38 | ``` 39 | 40 | * You can access the container logs with 41 | 42 | ```bash 43 | docker-compose logs -f 44 | ``` 45 | 46 | * Generate a client certificate 47 | 48 | ```bash 49 | export CLIENTNAME="your_client_name" 50 | # with a passphrase (recommended) 51 | docker-compose run --rm openvpn easyrsa build-client-full $CLIENTNAME 52 | # without a passphrase (not recommended) 53 | docker-compose run --rm openvpn easyrsa build-client-full $CLIENTNAME nopass 54 | ``` 55 | 56 | * Retrieve the client configuration with embedded certificates 57 | 58 | ```bash 59 | docker-compose run --rm openvpn ovpn_getclient $CLIENTNAME > $CLIENTNAME.ovpn 60 | ``` 61 | 62 | * Revoke a client certificate 63 | 64 | ```bash 65 | # Keep the corresponding crt, key and req files. 66 | docker-compose run --rm openvpn ovpn_revokeclient $CLIENTNAME 67 | # Remove the corresponding crt, key and req files. 68 | docker-compose run --rm openvpn ovpn_revokeclient $CLIENTNAME remove 69 | ``` 70 | 71 | ## Debugging Tips 72 | 73 | * Create an environment variable with the name DEBUG and value of 1 to enable debug output (using "docker -e"). 74 | 75 | ```bash 76 | docker-compose run -e DEBUG=1 -p 1194:1194/udp openvpn 77 | ``` 78 | -------------------------------------------------------------------------------- /docs/docker-openvpn.te: -------------------------------------------------------------------------------- 1 | module docker-openvpn 1.0; 2 | 3 | require { 4 | type svirt_lxc_net_t; 5 | class tun_socket create; 6 | } 7 | 8 | #============= svirt_lxc_net_t ============== 9 | allow svirt_lxc_net_t self:tun_socket create; 10 | 11 | -------------------------------------------------------------------------------- /docs/docker.md: -------------------------------------------------------------------------------- 1 | # Install Latest Docker Service 2 | 3 | Docker included with some distributions lags far behind upstream. This guide aims to provide a quick and reliable way to install or update it. 4 | 5 | It is recommended to use platforms that support systemd as future versions of this docker image may require systemd to help with some tasks: 6 | 7 | * Fedora 8 | * Debian 8.1+ 9 | 10 | ## Debian / Ubuntu 11 | 12 | ### Step 1 — Set Up Docker 13 | 14 | Docker is moving fast and Debian / Ubuntu's long term support (LTS) policy doesn't keep up. To work around this we'll install a PPA that will get us the latest version of Docker. For Debian Jessie users, just install docker.io from jessie-backports. 15 | 16 | Ensure dependencies are installed: 17 | 18 | sudo apt-get update && sudo apt-get install -y apt-transport-https curl 19 | 20 | Add the upstream Docker repository package signing key. The apt-key command uses elevated privileges via sudo, so a password prompt for the user's password may appear: 21 | 22 | curl -L https://get.docker.com/gpg | sudo apt-key add - 23 | 24 | Add the upstream Docker repository to the system list: 25 | 26 | echo deb https://get.docker.io/ubuntu docker main | sudo tee /etc/apt/sources.list.d/docker.list 27 | 28 | Update the package list and install the Docker package: 29 | 30 | sudo apt-get update && sudo apt-get install -y lxc-docker 31 | 32 | Add your user to the `docker` group to enable communication with the Docker daemon as a normal user, where `$USER` is your username. Exit and log in again for the new group to take effect: 33 | 34 | sudo usermod -aG docker $USER 35 | 36 | After **re-logging in** verify the group membership using the id command. The expected response should include docker like the following example: 37 | 38 | uid=1001(test0) gid=1001(test0) groups=1001(test0),27(sudo),999(docker) 39 | 40 | ### Step 2 — Test Docker 41 | 42 | Run a Debian jessie docker container: 43 | 44 | docker run --rm -it debian:jessie bash -l 45 | 46 | Once inside the container you'll see the `root@:/#` prompt signifying that the current shell is in a Docker container. To confirm that it's different from the host, check the version of Debian running in the container: 47 | 48 | cat /etc/issue.net 49 | 50 | Expected result: 51 | 52 | Debian GNU/Linux 8 53 | -------------------------------------------------------------------------------- /docs/faqs.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions 2 | 3 | ## How do I edit `openvpn.conf`? 4 | 5 | Use a Docker image with an editor and connect the volume container: 6 | 7 | docker run -v $OVPN_DATA:/etc/openvpn --rm -it kylemanna/openvpn vi /etc/openvpn/openvpn.conf 8 | 9 | 10 | ## Why not keep everything in one image? 11 | 12 | The run-time image (`kylemanna/openvpn`) is intended to be an ephemeral image. Nothing should be saved in it so that it can be re-downloaded and re-run when updates are pushed (i.e. newer version of OpenVPN or even Debian). The data container contains all this data and is attached at run time providing a safe home. 13 | 14 | If it was all in one container, an upgrade would require a few steps to extract all the data, perform some upgrade import, and re-run. This technique is also prone to people losing their EasyRSA PKI when they forget where it was. With everything in the data container upgrading is as simple as re-running `docker pull kylemanna/openvpn` and then `docker run ... kylemanna/openvpn`. 15 | 16 | ## How do I set up a split tunnel? 17 | 18 | Split tunnels are configurations where only some of the traffic from a client goes to the VPN, with the remainder routed through the normal non-VPN interfaces. You'll want to disable a default route (-d) when you generate the configuration, but still use NAT (-N) to keep network address translation enabled. 19 | 20 | ovpn_genconfig -N -d ... 21 | 22 | ## I need to add some extra configurations to openvpn.conf, How can I do that ? 23 | 24 | You can pass multiple (**-e**) options to `ovpn_genconfig`. For example, if you need to add _'duplicate-cn'_ and _'topology subnet'_ to the server configuration you could do something like this: 25 | 26 | ovpn_genconfig -e 'duplicate-cn' -e 'topology subnet' -u udp://VPN.SERVERNAME.COM 27 | -------------------------------------------------------------------------------- /docs/ipv6.md: -------------------------------------------------------------------------------- 1 | # IPv6 Support 2 | 3 | This is a work in progress, more polish to follow. 4 | 5 | ## Tunnel IPv6 Address To OpenVPN Clients 6 | 7 | This feature is advanced and recommended only for those who already have a functioning IPv4 tunnel and know how IPv6 works. 8 | 9 | Systemd is used to setup a static route and Debian 8.1 or later is recommended as the host distribution. Others probably work, but haven't been tested. 10 | 11 | 12 | ### Step 1 — Setup IPv6 on the Host Machine 13 | 14 | The tutorial uses a free tunnel from [tunnelbroker.net](https://tunnelbroker.net/) to get a /64 and /48 prefix allocated to me. The tunnel endpoint is less then 3 ms away from Digital Ocean's San Francisco datacenter. 15 | 16 | Place the following in `/etc/network/interfaces`. Replace `PUBLIC_IP` with your host's public IPv4 address and replace 2001:db8::2 and 2001:db8::1 with the corresponding tunnel endpoints: 17 | 18 | auto he-ipv6 19 | iface he-ipv6 inet6 v4tunnel 20 | address 2001:db8::2 21 | netmask 64 22 | endpoint 72.52.104.74 23 | local PUBLIC_IP 24 | ttl 255 25 | gateway 2001:db8::1 26 | 27 | Bring the interface up: 28 | 29 | ifup he-ipv6 30 | 31 | Test that IPv6 works on the host: 32 | 33 | ping6 google.com 34 | 35 | If this doesn't work, figure it out. It may be necessary to add an firewall rule to allow IP protocol 41 through the firewall. 36 | 37 | 38 | ### Step 2 — Update Docker's Init To Enable IPv6 Support 39 | 40 | Add the `--ipv6` to the Docker daemon invocation. 41 | 42 | On **Ubuntu** and old versions of Debian Append the `--ipv6` argument to the `DOCKER_OPTS` variable in: 43 | 44 | /etc/default/docker 45 | 46 | On modern **systemd** distributions copy the service file and modify it and reload the service: 47 | 48 | sed -e 's:^\(ExecStart.*\):\1 --ipv6:' /lib/systemd/system/docker.service | tee /etc/systemd/system/docker.service 49 | systemctl restart docker.service 50 | 51 | 52 | ### Step 3 — Setup the systemd Unit File 53 | 54 | Copy the systemd init file from the docker-openvpn /init directory of the repository and install into `/etc/systemd/system/docker-openvpn.service` 55 | 56 | curl -o /etc/systemd/system/docker-openvpn@.service 'https://raw.githubusercontent.com/kylemanna/docker-openvpn/dev/init/docker-openvpn%40.service' 57 | 58 | Edit the file, replace `IP6_PREFIX` value with the value of your /64 prefix. 59 | 60 | vi /etc/systemd/system/docker-openvpn@.service 61 | 62 | Finally, reload systemd so the changes take affect: 63 | 64 | systemctl daemon-reload 65 | 66 | ### Step 4 — Start OpenVPN 67 | 68 | Ensure that OpenVPN has been initialized and configured as described in the top level `README.md`. 69 | 70 | Start the systemd service file specifying the volume container suffix as the instance. For example, `INSTANCE=test0` has a docker volume container named `ovpn-data-test0` and service will create `ovpn-test0` container: 71 | 72 | systemctl start docker-openvpn@test0 73 | 74 | Verify logs if needed: 75 | 76 | systemctl status docker-openvpn@test0 77 | docker logs ovpn-test0 78 | 79 | ### Step 4 — Modify Client Config for IPv6 Default Route 80 | 81 | Append the default route for the public Internet: 82 | 83 | echo "route-ipv6 2000::/3" >> clientname.ovpn 84 | 85 | ### Step 5 — Start up Client 86 | 87 | If all went according to plan, then `ping6 2600::` and `ping6 google.com` should work. 88 | 89 | Fire up a web browser and attempt to navigate to [https://ipv6.google.com](https://ipv6.google.com). 90 | 91 | 92 | ## Connect to the OpenVPN Server Over IPv6 93 | 94 | This feature requires a docker daemon with working IPv6 support. 95 | 96 | This will allow connections over IPv4 and IPv6. 97 | 98 | Generate server configuration with the udp6 or tcp6 protocol: 99 | 100 | docker run -v $OVPN_DATA:/etc/openvpn --rm kylemanna/openvpn ovpn_genconfig -u udp6://VPN.SERVERNAME.COM 101 | docker run -v $OVPN_DATA:/etc/openvpn --rm kylemanna/openvpn ovpn_genconfig -u tcp6://VPN.SERVERNAME.COM 102 | -------------------------------------------------------------------------------- /docs/otp.md: -------------------------------------------------------------------------------- 1 | # Using two factor authentication for users 2 | 3 | Instead of relying on complex passwords for client certificates (that usually get written somewhere) this image 4 | provides support for two factor authentication with OTP devices. 5 | 6 | The most common app that provides OTP generation is Google Authenticator ([iOS](https://itunes.apple.com/it/app/google-authenticator/id388497605?mt=8) and 7 | [Android](https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=it)) you can download it 8 | and use this image to generate user configuration. 9 | 10 | ## Usage 11 | 12 | In order to enable two factor authentication the following steps are required. 13 | 14 | * Choose a more secure [cipher](https://community.openvpn.net/openvpn/wiki/SWEET32) to use because since [OpenVPN 2.3.13](https://community.openvpn.net/openvpn/wiki/ChangesInOpenvpn23#OpenVPN2.3.13) the default openvpn cipher BF-CBC will cause a renegotiated connection every 64 MB of data 15 | 16 | * Generate server configuration with `-2` and `-C $CIPHER` options 17 | 18 | docker run -v $OVPN_DATA:/etc/openvpn --rm kylemanna/openvpn ovpn_genconfig -u udp://vpn.example.com -2 -C $CIPHER 19 | 20 | * Generate your client certificate (possibly without a password since you're using OTP) 21 | 22 | docker run -v $OVPN_DATA:/etc/openvpn --rm -it kylemanna/openvpn easyrsa build-client-full nopass 23 | 24 | * Generate authentication configuration for your client. -t is needed to show QR code, -i is optional for interactive usage 25 | 26 | docker run -v $OVPN_DATA:/etc/openvpn --rm -it kylemanna/openvpn ovpn_otp_user 27 | 28 | The last step will generate OTP configuration for the provided user with the following options 29 | 30 | ``` 31 | google-authenticator --time-based --disallow-reuse --force --rate-limit=3 --rate-time=30 --window-size=3 \ 32 | -l "${1}@${OVPN_CN}" -s /etc/openvpn/otp/${1}.google_authenticator 33 | ``` 34 | 35 | It will also show a shell QR code in terminal you can scan with the Google Authenticator application. It also provides 36 | a link to a google chart url that will display a QR code for the authentication. 37 | 38 | **Do not share QR code (or generated url) with anyone but final user, that is your second factor for authentication 39 | that is used to generate OTP codes** 40 | 41 | Here's an example QR code generated for an hypotetical user@example.com user. 42 | 43 | ![Example QR Code](https://www.google.com/chart?chs=200x200&chld=M|0&cht=qr&chl=otpauth://totp/user@example.com%3Fsecret%3DKEYZ66YEXMXDHPH5) 44 | 45 | Generate client configuration for `` and import it in OpenVPN client. On connection it will prompt for user and password. 46 | Enter your username and a 6 digit code generated by Authenticator app and you're logged in. 47 | 48 | ## TL;DR 49 | 50 | Under the hood this configuration will setup an `openvpn` PAM service configuration (`/etc/pam.d/openvpn`) 51 | that relies on the awesome [Google Authenticator PAM module](https://github.com/google/google-authenticator). 52 | In this configuration the `auth` part of PAM flow is managed by OTP codes and the `account` part is not enforced 53 | because you're likely dealing with virtual users and you do not want to create a system account for every VPN user. 54 | 55 | `ovpn_otp_user` script will store OTP credentials under `/etc/openvpn/otp/.google_authentication`. In this 56 | way when you take a backup OTP users are included as well. 57 | 58 | Finally it will enable the openvpn plugin `openvpn-plugin-auth-pam.so` in server configuration and append the 59 | `auth-user-pass` directive in client configuration. 60 | 61 | ## Debug 62 | 63 | If something is not working you can verify your PAM setup with these commands 64 | 65 | ``` 66 | # Start a shell in container 67 | docker run -v $OVPN_DATA:/etc/openvpn --rm -it kylemanna/openvpn bash 68 | # Then in container you have pamtester utility already installed 69 | which pamtester 70 | # To check authentication use this command that will prompt for a valid code from Authenticator APP 71 | pamtester -v openvpn authenticate 72 | ``` 73 | 74 | In the last command `` should be replaced by the exact string you used in the ovpn_otp_user command. 75 | 76 | If you configured everything correctly you should get authenticated by entering a OTP code from the app. 77 | -------------------------------------------------------------------------------- /docs/paranoid.md: -------------------------------------------------------------------------------- 1 | # Advanced security 2 | 3 | ## Keep the CA root key safe 4 | As mentioned in the [backup section](/docs/backup.md), there are good reasons to not generate the CA and/or leave it on the server. This document describes how you can generate the CA and all your certificates on a secure machine and then copy only the needed files (which never includes the CA root key obviously ;) ) to the server(s) and clients. 5 | 6 | Execute the following commands. Note that you might want to change the volume `$PWD` or use a data docker container for this. 7 | 8 | docker run --net=none --rm -t -i -v $PWD:/etc/openvpn kylemanna/openvpn ovpn_genconfig -u udp://VPN.SERVERNAME.COM 9 | docker run --net=none --rm -t -i -v $PWD:/etc/openvpn kylemanna/openvpn ovpn_initpki 10 | docker run --net=none --rm -t -i -v $PWD:/etc/openvpn kylemanna/openvpn ovpn_copy_server_files 11 | 12 | The [`ovpn_copy_server_files`](/bin/ovpn_copy_server_files) script puts all the needed configuration in a subdirectory which defaults to `$OPENVPN/server`. All you need to do now is to copy this directory to the server and you are good to go. 13 | 14 | ## Crypto Hardening 15 | 16 | If you want to select the ciphers used by OpenVPN the following parameters of the `ovpn_genconfig` might interest you: 17 | 18 | -T Encrypt packets with the given cipher algorithm instead of the default one (tls-cipher). 19 | -C A list of allowable TLS ciphers delimited by a colon (cipher). 20 | -a Authenticate packets with HMAC using the given message digest algorithm (auth). 21 | 22 | 23 | The following options have been tested successfully: 24 | 25 | docker run -v $OVPN_DATA:/etc/openvpn --net=none --rm kylemanna/openvpn ovpn_genconfig -C 'AES-256-CBC' -a 'SHA384' 26 | 27 | Changing the `tls-cipher` option seems to be more complicated because some clients (namely NetworkManager in Debian Jessie) seem to have trouble with this. Running `openvpn` manually also did not solve the issue: 28 | 29 | TLS Error: TLS key negotiation failed to occur within 60 seconds (check your network connectivity) 30 | TLS Error: TLS handshake failed 31 | 32 | ## EasyRSA and 4096 bit RSA Keys 33 | 34 | EasyRSA will generate 4096 bit RSA keys when the `-e EASYRSA_KEY_SIZE=4096` argument is added to `ovpn_initpki` and `easyrsa build-client-full` commands. 35 | 36 | docker run -e EASYRSA_KEY_SIZE=4096 -v $OVPN_DATA:/etc/openvpn --rm -it kylemanna/openvpn ovpn_initpki 37 | docker run -e EASYRSA_KEY_SIZE=4096 -v $OVPN_DATA:/etc/openvpn --rm -it kylemanna/openvpn easyrsa build-client-full CLIENTNAME nopass 38 | 39 | ## Logging and stdout 40 | 41 | Because you are running within Docker, remember that any command that generates output to stdout may also log that output through Docker's log-driver mechanism. That may mean that e.g. keying material generated by `ovpn_getclient` will be logged somewhere that you don't want it to be logged. 42 | 43 | A simple way to avoid having Docker log output for a given command is to run with `--log-driver=none`, e.g 44 | 45 | docker run -v $OVPN_DATA:/etc/openvpn --log-driver=none --rm kylemanna/openvpn ovpn_getclient USER > USER.ovpn 46 | 47 | ## Additional Resources 48 | 49 | Have a look at the [Applied-Crypto-Hardening](https://github.com/BetterCrypto/Applied-Crypto-Hardening/tree/master/src/configuration/VPNs/OpenVPN) project for more examples. 50 | -------------------------------------------------------------------------------- /docs/selinux.md: -------------------------------------------------------------------------------- 1 | # For hosts that use SELinux 2 | 3 | Try this [policy file](docker-openvpn.te) 4 | 5 | Run these commands to compile and load it: 6 | 7 | ``` 8 | checkmodule -M -m -o docker-openvpn.mod docker-openvpn.te 9 | semodule_package -o docker-openvpn.pp -m docker-openvpn.mod 10 | sudo semodule -i docker-openvpn.pp 11 | ``` 12 | 13 | Also, some configurations don't allow containers to load kernel modules, so on the host run this: 14 | 15 | ``` 16 | sudo modprobe tun 17 | ``` 18 | 19 | So the container doesn't have to load the `tun` module. 20 | 21 | 22 | # Still having issues? 23 | 24 | In January 2016, Fedora based systems got an update that fixed an issue for labeling namespaced net objects under /proc 25 | to fix, make sure that you have run `sudo dnf update` and you need to reboot to load the new policies 26 | -------------------------------------------------------------------------------- /docs/static-ips.md: -------------------------------------------------------------------------------- 1 | # Static IP Addresses 2 | 3 | The docker image is setup for static client configuration on the 192.168.254.0/24 subnet. To use it follow the Quick Start section below. Note that the IP addresses octets need to be picked special, see [OpenVPN Documentation](https://openvpn.net/index.php/open-source/documentation/howto.html#policy) for more details. 4 | 5 | ## Quick Start 6 | 7 | 1. Create a client specific configuration: 8 | 9 | $ echo "ifconfig-push 192.168.254.1 192.168.254.2" | docker run -v $OVPN_DATA:/etc/openvpn -i --rm kylemanna/openvpn tee /etc/openvpn/ccd/CERT_COMMON_NAME 10 | ifconfig-push 192.168.254.1 192.168.254.2 11 | 12 | 2. Wait for client to reconnect if necessary 13 | 14 | ## Advanced Admin 15 | 16 | Login to the data volume with a `bash` container, note only changes in /etc/openvpn will persist: 17 | 18 | docker run -v $OVPN_DATA:/etc/openvpn -it --rm kylemanna/openvpn bash -l 19 | 20 | ## Upgrading from Old OpenVPN Configurations 21 | 22 | If you're running an old configuration and need to upgrade it to pull in the ccd directory run the following: 23 | 24 | docker run -v $OVPN_DATA:/etc/openvpn --rm kylemanna/openvpn ovpn_genconfig 25 | -------------------------------------------------------------------------------- /docs/systemd.md: -------------------------------------------------------------------------------- 1 | # Docker + OpenVPN systemd Service 2 | 3 | The systemd service aims to make the update and invocation of the 4 | `docker-openvpn` container seamless. It automatically downloads the latest 5 | `docker-openvpn` image and instantiates a Docker container with that image. At 6 | shutdown it cleans-up the old container. 7 | 8 | In the event the service dies (crashes, or is killed) systemd will attempt to 9 | restart the service every 10 seconds until the service is stopped with 10 | `systemctl stop docker-openvpn@NAME.service`. 11 | 12 | A number of IPv6 hacks are incorporated to workaround Docker shortcomings and 13 | are harmless for those not using IPv6. 14 | 15 | To use and enable automatic start by systemd: 16 | 17 | 1. Create a Docker volume container named `ovpn-data-NAME` where `NAME` is the 18 | user's choice to describe the use of the container. In this example 19 | configuration, `NAME=example`. 20 | 21 | OVPN_DATA="ovpn-data-example" 22 | docker volume create --name $OVPN_DATA 23 | 24 | 2. Initialize the data container, but don't start the container : 25 | 26 | docker run -v $OVPN_DATA:/etc/openvpn --rm kylemanna/openvpn ovpn_genconfig -u udp://VPN.SERVERNAME.COM 27 | docker run -v $OVPN_DATA:/etc/openvpn --rm -it kylemanna/openvpn ovpn_initpki 28 | 29 | 3. Download the [docker-openvpn@.service](https://raw.githubusercontent.com/kylemanna/docker-openvpn/master/init/docker-openvpn%40.service) 30 | file to `/etc/systemd/system`: 31 | 32 | curl -L https://raw.githubusercontent.com/kylemanna/docker-openvpn/master/init/docker-openvpn%40.service | sudo tee /etc/systemd/system/docker-openvpn@.service 33 | 34 | 4. Enable and start the service with: 35 | 36 | systemctl enable --now docker-openvpn@example.service 37 | 38 | 5. Verify service start-up with: 39 | 40 | systemctl status docker-openvpn@example.service 41 | journalctl --unit docker-openvpn@example.service 42 | 43 | For more information, see the [systemd manual pages](https://www.freedesktop.org/software/systemd/man/index.html). 44 | -------------------------------------------------------------------------------- /docs/tcp.md: -------------------------------------------------------------------------------- 1 | # TCP Protocol 2 | 3 | ## TCP vs. UDP - Pros & Cons 4 | By default, OpenVPN is configured to use the UDP protocol. Because UDP incurs minimal protocol overhead (for example, no acknowledgment is required upon successful packet receipt), it can sometimes result in slightly faster throughput. However, in situations where VPN service is needed over an unreliable connection, the user experience can benefit from the extra diagnostic features of the TCP protocol. 5 | 6 | As an example, users connecting from an airplane wifi network may experience high packet drop rates, where the error detection and sliding window control of TCP can more readily adjust to the inconsistent connection. 7 | 8 | Another example would be trying to open a VPN connection from within a very restrictive network. In some cases port 1194, or even UDP traffic on any port, may be restricted by network policy. Because TCP traffic on port 443 is used for normal TLS (https) web browsing, it is very unlikely to be blocked. 9 | 10 | ## Using TCP 11 | Those requiring TCP connections should initialize the data container by specifying the TCP protocol and port number: 12 | 13 | docker run -v $OVPN_DATA:/etc/openvpn --rm kylemanna/openvpn ovpn_genconfig -u tcp://VPN.SERVERNAME.COM:443 14 | docker run -v $OVPN_DATA:/etc/openvpn --rm -it kylemanna/openvpn ovpn_initpki 15 | 16 | Because the server container always exposes port 1194, regardless of the 17 | specified protocol, adjust the mapping appropriately: 18 | 19 | docker run -v $OVPN_DATA:/etc/openvpn -d -p 443:1194/tcp --cap-add=NET_ADMIN kylemanna/openvpn 20 | 21 | ## Running a Second Fallback TCP Container 22 | Instead of choosing between UDP and TCP, you can use both. A single instance of OpenVPN can only listen for a single protocol on a single port, but this image makes it easy to run two instances simultaneously. After building, configuring, and starting a standard container listening for UDP traffic on 1194, you can start a second container listening for tcp traffic on port 443: 23 | 24 | docker run -v $OVPN_DATA:/etc/openvpn --rm -p 443:1194/tcp --cap-add=NET_ADMIN kylemanna/openvpn ovpn_run --proto tcp 25 | 26 | `ovpn_run` will load all the values from the default config file, and `--proto tcp` will override the protocol setting. 27 | 28 | This allows you to use UDP most of the time, but fall back to TCP on the rare occasion that you need it. 29 | 30 | Note that you will need to configure client connections manually. At this time it is not possible to generate a client config that will automatically fall back to the TCP connection. 31 | 32 | ## Forward HTTP/HTTPS connection to another TCP port 33 | You might run into cases where you want your OpenVPN server listening on TCP port 443 to allow connection behind a restricted network, but you already have a webserver on your host running on that port. OpenVPN has a built-in option named `port-share` that allow you to proxy incoming traffic that isn't OpenVPN protocol to another host and port. 34 | 35 | First, change the listening port of your existing webserver (for instance from 443 to 4433). 36 | 37 | Then initialize the data container by specifying the TCP protocol, port 443 and the port-share option: 38 | 39 | docker run -v $OVPN_DATA:/etc/openvpn --rm kylemanna/openvpn ovpn_genconfig \ 40 | -u tcp://VPN.SERVERNAME.COM:443 \ 41 | -e 'port-share VPN.SERVERNAME.COM 4433' 42 | 43 | Then proceed to initialize the pki, create your users and start the container as usual. 44 | 45 | This will proxy all non OpenVPN traffic incoming on TCP port 443 to TCP port 4433 on the same host. This is currently only designed to work with HTTP or HTTPS protocol. 46 | -------------------------------------------------------------------------------- /init/docker-openvpn@.service: -------------------------------------------------------------------------------- 1 | # 2 | # Docker + OpenVPN systemd service 3 | # 4 | # Author: Kyle Manna 5 | # Source: https://github.com/kylemanna/docker-openvpn 6 | # 7 | # This service aims to make the update and invocation of the docker-openvpn 8 | # container seamless. It automatically downloads the latest docker-openvpn 9 | # image and instantiates a Docker container with that image. At shutdown it 10 | # cleans-up the old container. 11 | # 12 | # In the event the service dies (crashes, or is killed) systemd will attempt 13 | # to restart the service every 10 seconds until the service is stopped with 14 | # `systemctl stop docker-openvpn@NAME`. 15 | # 16 | # A number of IPv6 hacks are incorporated to workaround Docker shortcomings and 17 | # are harmless for those not using IPv6. 18 | # 19 | # To use: 20 | # 1. Create a Docker volume container named `ovpn-data-NAME` where NAME is the 21 | # user's choice to describe the use of the container. 22 | # 2. Initialize the data container according to the docker-openvpn README, but 23 | # don't start the container. Stop the docker container if started. 24 | # 3. Download this service file to /etc/systemd/service/docker-openvpn@.service 25 | # 4. Enable and start the service template with: 26 | # `systemctl enable --now docker-openvpn@NAME.service` 27 | # 5. Verify service start-up with: 28 | # `systemctl status docker-openvpn@NAME.service` 29 | # `journalctl --unit docker-openvpn@NAME.service` 30 | # 31 | # For more information, see the systemd manual pages. 32 | # 33 | [Unit] 34 | Description=OpenVPN Docker Container 35 | Documentation=https://github.com/kylemanna/docker-openvpn 36 | After=network.target docker.service 37 | Requires=docker.service 38 | 39 | [Service] 40 | RestartSec=10 41 | Restart=always 42 | 43 | # Modify IP6_PREFIX to match network config 44 | #Environment="IP6_PREFIX=2001:db8::/64" 45 | #Environment="ARGS=--config openvpn.conf --server-ipv6 2001:db8::/64" 46 | Environment="NAME=ovpn-%i" 47 | Environment="DATA_VOL=ovpn-data-%i" 48 | Environment="IMG=kylemanna/openvpn:latest" 49 | Environment="PORT=1194:1194/udp" 50 | 51 | # To override environment variables, use local configuration directory: 52 | # /etc/systemd/system/docker-openvpn@foo.d/local.conf 53 | # http://www.freedesktop.org/software/systemd/man/systemd.unit.html 54 | 55 | # Clean-up bad state if still hanging around 56 | ExecStartPre=-/usr/bin/docker rm -f $NAME 57 | 58 | # Attempt to pull new image for security updates 59 | ExecStartPre=-/usr/bin/docker pull $IMG 60 | 61 | # IPv6: Ensure forwarding is enabled on host's networking stack (hacky) 62 | # Would be nice to use systemd-network on the host, but this doesn't work 63 | # http://lists.freedesktop.org/archives/systemd-devel/2015-June/032762.html 64 | ExecStartPre=/bin/sh -c 'test -z "$IP6_PREFIX" && exit 0; sysctl net.ipv6.conf.all.forwarding=1' 65 | 66 | # Main process 67 | ExecStart=/usr/bin/docker run --rm --cap-add=NET_ADMIN -v ${DATA_VOL}:/etc/openvpn --name ${NAME} -p ${PORT} ${IMG} ovpn_run $ARGS 68 | 69 | # IPv6: Add static route for IPv6 after it starts up 70 | ExecStartPost=/bin/sh -c 'test -z "${IP6_PREFIX}" && exit 0; sleep 1; ip route replace ${IP6_PREFIX} via $(docker inspect -f "{{ .NetworkSettings.GlobalIPv6Address }}" $NAME ) dev docker0' 71 | 72 | # IPv6: Clean-up 73 | ExecStopPost=/bin/sh -c 'test -z "$IP6_PREFIX" && exit 0; ip route del $IP6_PREFIX dev docker0' 74 | 75 | [Install] 76 | WantedBy=multi-user.target 77 | -------------------------------------------------------------------------------- /init/upstart.init: -------------------------------------------------------------------------------- 1 | # Copy to /etc/init/docker-openvpn.conf 2 | description "Docker container for OpenVPN server" 3 | start on filesystem and started docker 4 | stop on runlevel [!2345] 5 | respawn 6 | script 7 | exec docker run -v ovpn-data-example:/etc/openvpn --rm -p 1194:1194/udp --cap-add=NET_ADMIN kylemanna/openvpn 8 | end script 9 | -------------------------------------------------------------------------------- /otp/openvpn: -------------------------------------------------------------------------------- 1 | # Uses google authenticator library as PAM module using a single folder for all users tokens 2 | # User root is required to stick with an hardcoded user when trying to determine user id and allow unexisting system users 3 | # See https://github.com/google/google-authenticator-libpam#usersome-user 4 | auth required pam_google_authenticator.so secret=/etc/openvpn/otp/${USER}.google_authenticator user=root 5 | 6 | # Accept any user since we're dealing with virtual users there's no need to have a system account (pam_unix.so) 7 | account sufficient pam_permit.so 8 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # Tests 2 | 3 | Philosophy is to not re-invent the wheel while allowing users to quickly test repository specific tests. 4 | 5 | Example invocation from top-level of repository: 6 | 7 | docker build -t kylemanna/openvpn . 8 | test/run.sh kylemanna/openvpn 9 | # Be sure to pull kylemanna/openvpn:latest after you're done testing 10 | 11 | More details: https://github.com/docker-library/official-images/tree/master/test 12 | 13 | ## Continuous Integration 14 | 15 | The set of scripts defined by `config.sh` are run every time a pull request or push to the repository is made. 16 | 17 | ## Maintenance 18 | 19 | Periodically these scripts may need to be synchronized with their upsteam source. Would be nice to be able to just use them from upstream if it such a feature is added later to avoid having to copy them in place. 20 | -------------------------------------------------------------------------------- /test/client/wait-for-connect.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | [ -n "${DEBUG+x}" ] && set -x 5 | 6 | OPENVPN_CONFIG=${1:-/client/config.ovpn} 7 | 8 | # For some reason privileged mode creates the char device and cap-add=NET_ADMIN doesn't 9 | mkdir -p /dev/net 10 | if [ ! -c /dev/net/tun ]; then 11 | mknod /dev/net/tun c 10 200 12 | fi 13 | 14 | # Run in background using bash job management, setup trap to clean-up 15 | trap "{ jobs -p | xargs -r kill; wait; }" EXIT 16 | openvpn --config "$OPENVPN_CONFIG" --management 127.0.0.1 9999 & 17 | 18 | # Spin waiting for interface to exist signifying connection 19 | timeout=10 20 | for i in $(seq $timeout); do 21 | # Allow to start-up 22 | sleep 0.5 23 | 24 | # Use bash magic to open tcp socket on fd 3 and break when successful 25 | exec 3<>/dev/tcp/127.0.0.1/9999 && break 26 | done 27 | 28 | if [ $i -ge $timeout ]; then 29 | echo "Error connecting to OpenVPN mgmt interface, i=$i, exiting." 30 | exit 2 31 | fi 32 | 33 | # Consume all header input and echo, look for errors here 34 | while read -t 0.1 <&3; do echo $REPLY; done 35 | 36 | # Request state over mgmt interface 37 | timeout=10 38 | for i in $(seq $timeout); do 39 | echo "state" >&3 40 | state=$(head -n1 <&3) 41 | echo -n "$state" | grep -q 'CONNECTED,SUCCESS' && break 42 | sleep 1 43 | done 44 | 45 | if [ $i -ge $timeout ]; then 46 | echo "Error connecting to OpenVPN, i=$i, exiting." 47 | exit 3 48 | fi 49 | 50 | exec 3>&- 51 | -------------------------------------------------------------------------------- /test/config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | testAlias+=( 5 | [kylemanna/openvpn]='openvpn' 6 | ) 7 | 8 | imageTests+=( 9 | [openvpn]=' 10 | paranoid 11 | conf_options 12 | client 13 | basic 14 | dual-proto 15 | otp 16 | iptables 17 | revocation 18 | ' 19 | ) 20 | -------------------------------------------------------------------------------- /test/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | dir="$(dirname "$(readlink -f "$BASH_SOURCE")")" 5 | 6 | self="$(basename "$0")" 7 | 8 | usage() { 9 | cat <&2 && false; })" 23 | eval set -- "$opts" 24 | 25 | declare -A argTests=() 26 | declare -a configs=() 27 | dryRun= 28 | while true; do 29 | flag=$1 30 | shift 31 | case "$flag" in 32 | --dry-run) dryRun=1 ;; 33 | --help|-h|'-?') usage && exit 0 ;; 34 | --test|-t) argTests["$1"]=1 && shift ;; 35 | --config|-c) configs+=("$(readlink -f "$1")") && shift ;; 36 | --) break ;; 37 | *) 38 | { 39 | echo "error: unknown flag: $flag" 40 | usage 41 | } >&2 42 | exit 1 43 | ;; 44 | esac 45 | done 46 | 47 | if [ $# -eq 0 ]; then 48 | usage >&2 49 | exit 1 50 | fi 51 | 52 | # declare configuration variables 53 | declare -a globalTests=() 54 | declare -A testAlias=() 55 | declare -A imageTests=() 56 | declare -A globalExcludeTests=() 57 | declare -A explicitTests=() 58 | 59 | # if there are no user-specified configs, use the default config 60 | if [ ${#configs} -eq 0 ]; then 61 | configs+=("$dir/config.sh") 62 | fi 63 | 64 | # load the configs 65 | declare -A testPaths=() 66 | for conf in "${configs[@]}"; do 67 | . "$conf" 68 | 69 | # Determine the full path to any newly-declared tests 70 | confDir="$(dirname "$conf")" 71 | 72 | for testName in ${globalTests[@]} ${imageTests[@]}; do 73 | [ "${testPaths[$testName]}" ] && continue 74 | 75 | if [ -d "$confDir/tests/$testName" ]; then 76 | # Test directory found relative to the conf file 77 | testPaths[$testName]="$confDir/tests/$testName" 78 | elif [ -d "$dir/tests/$testName" ]; then 79 | # Test directory found in the main tests/ directory 80 | testPaths[$testName]="$dir/tests/$testName" 81 | fi 82 | done 83 | done 84 | 85 | didFail= 86 | for dockerImage in "$@"; do 87 | echo "testing $dockerImage" 88 | 89 | if ! docker inspect "$dockerImage" &> /dev/null; then 90 | echo $'\timage does not exist!' 91 | didFail=1 92 | continue 93 | fi 94 | 95 | repo="${dockerImage%:*}" 96 | tagVar="${dockerImage#*:}" 97 | #version="${tagVar%-*}" 98 | variant="${tagVar##*-}" 99 | 100 | testRepo=$repo 101 | [ -z "${testAlias[$repo]}" ] || testRepo="${testAlias[$repo]}" 102 | 103 | explicitVariant= 104 | if [ \ 105 | "${explicitTests[:$variant]}" \ 106 | -o "${explicitTests[$repo:$variant]}" \ 107 | -o "${explicitTests[$testRepo:$variant]}" \ 108 | ]; then 109 | explicitVariant=1 110 | fi 111 | 112 | testCandidates=() 113 | if [ -z "$explicitVariant" ]; then 114 | testCandidates+=( "${globalTests[@]}" ) 115 | fi 116 | testCandidates+=( 117 | ${imageTests[:$variant]} 118 | ) 119 | if [ -z "$explicitVariant" ]; then 120 | testCandidates+=( 121 | ${imageTests[$testRepo]} 122 | ) 123 | fi 124 | testCandidates+=( 125 | ${imageTests[$testRepo:$variant]} 126 | ) 127 | if [ "$testRepo" != "$repo" ]; then 128 | if [ -z "$explicitVariant" ]; then 129 | testCandidates+=( 130 | ${imageTests[$repo]} 131 | ) 132 | fi 133 | testCandidates+=( 134 | ${imageTests[$repo:$variant]} 135 | ) 136 | fi 137 | 138 | tests=() 139 | for t in "${testCandidates[@]}"; do 140 | if [ ${#argTests[@]} -gt 0 -a -z "${argTests[$t]}" ]; then 141 | # skipping due to -t 142 | continue 143 | fi 144 | 145 | if [ \ 146 | ! -z "${globalExcludeTests[${testRepo}_$t]}" \ 147 | -o ! -z "${globalExcludeTests[${testRepo}:${variant}_$t]}" \ 148 | -o ! -z "${globalExcludeTests[:${variant}_$t]}" \ 149 | -o ! -z "${globalExcludeTests[${repo}_$t]}" \ 150 | -o ! -z "${globalExcludeTests[${repo}:${variant}_$t]}" \ 151 | -o ! -z "${globalExcludeTests[:${variant}_$t]}" \ 152 | ]; then 153 | # skipping due to exclude 154 | continue 155 | fi 156 | 157 | tests+=( "$t" ) 158 | done 159 | 160 | currentTest=0 161 | totalTest="${#tests[@]}" 162 | for t in "${tests[@]}"; do 163 | (( currentTest+=1 )) 164 | echo -ne "\t'$t' [$currentTest/$totalTest]..." 165 | 166 | # run test against dockerImage here 167 | # find the script for the test 168 | scriptDir="${testPaths[$t]}" 169 | if [ -d "$scriptDir" ]; then 170 | script="$scriptDir/run.sh" 171 | if [ -x "$script" -a ! -d "$script" ]; then 172 | # TODO dryRun logic 173 | if output="$("$script" $dockerImage)"; then 174 | if [ -f "$scriptDir/expected-std-out.txt" ] && ! d="$(echo "$output" | diff -u "$scriptDir/expected-std-out.txt" - 2>/dev/null)"; then 175 | echo 'failed; unexpected output:' 176 | echo "$d" 177 | didFail=1 178 | else 179 | echo 'passed' 180 | fi 181 | else 182 | echo 'failed' 183 | didFail=1 184 | fi 185 | else 186 | echo "skipping" 187 | echo >&2 "error: $script missing, not executable or is a directory" 188 | didFail=1 189 | continue 190 | fi 191 | else 192 | echo "skipping" 193 | echo >&2 "error: unable to locate test '$t'" 194 | didFail=1 195 | continue 196 | fi 197 | done 198 | done 199 | 200 | if [ "$didFail" ]; then 201 | exit 1 202 | fi 203 | -------------------------------------------------------------------------------- /test/tests/basic/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | [ -n "${DEBUG+x}" ] && set -x 5 | 6 | OVPN_DATA=basic-data 7 | CLIENT=travis-client 8 | IMG=kylemanna/openvpn 9 | CLIENT_DIR="$(readlink -f "$(dirname "$BASH_SOURCE")/../../client")" 10 | 11 | ip addr ls 12 | SERV_IP=$(ip -4 -o addr show scope global | awk '{print $4}' | sed -e 's:/.*::' | head -n1) 13 | docker run -v $OVPN_DATA:/etc/openvpn --rm $IMG ovpn_genconfig -u udp://$SERV_IP 14 | 15 | # nopass is insecure 16 | docker run -v $OVPN_DATA:/etc/openvpn --rm -it -e "EASYRSA_BATCH=1" -e "EASYRSA_REQ_CN=Travis-CI Test CA" $IMG ovpn_initpki nopass 17 | 18 | docker run -v $OVPN_DATA:/etc/openvpn --rm -it $IMG easyrsa build-client-full $CLIENT nopass 19 | 20 | docker run -v $OVPN_DATA:/etc/openvpn --rm $IMG ovpn_getclient $CLIENT | tee $CLIENT_DIR/config.ovpn 21 | 22 | docker run -v $OVPN_DATA:/etc/openvpn --rm $IMG ovpn_listclients | grep $CLIENT 23 | 24 | # 25 | # Fire up the server and setup a trap to always clean it up 26 | # 27 | trap "{ jobs -p | xargs -r kill; wait; }" EXIT 28 | docker run --name "ovpn-test" -v $OVPN_DATA:/etc/openvpn --rm -e DEBUG --cap-add=NET_ADMIN $IMG & 29 | 30 | for i in $(seq 10); do 31 | SERV_IP_INTERNAL=$(docker inspect --format '{{ .NetworkSettings.IPAddress }}' "ovpn-test" 2>/dev/null || true) 32 | test -n "$SERV_IP_INTERNAL" && break 33 | sleep 0.1 34 | done 35 | sed -i -e s:$SERV_IP:$SERV_IP_INTERNAL:g ${CLIENT_DIR}/config.ovpn 36 | 37 | # 38 | # Fire up a client in a container since openvpn is disallowed by Travis-CI 39 | # 40 | docker run --rm --cap-add=NET_ADMIN -e DEBUG --volume $CLIENT_DIR:/client $IMG /client/wait-for-connect.sh 41 | 42 | # 43 | # Celebrate 44 | # 45 | cat < 48 | ----------- 49 | \ ^__^ 50 | \ (oo)\_______ 51 | (__)\ )\/\\ 52 | ||----w | 53 | || || 54 | EOF 55 | -------------------------------------------------------------------------------- /test/tests/client/container.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SERV_IP=$(ip -4 -o addr show scope global | awk '{print $4}' | sed -e 's:/.*::' | head -n1) 4 | SERVER_CONF="/etc/openvpn/openvpn.conf" 5 | TEST1_OVPN="/etc/openvpn/test1.ovpn" 6 | 7 | # Function to fail 8 | abort() { cat <<< "$@" 1>&2; exit 1; } 9 | 10 | # Check a config (haystack) for a given line (needle) exit with error if not 11 | # found. 12 | test_config() { 13 | 14 | local needle="${2}" 15 | local file="${1}" 16 | 17 | busybox grep -q "${needle}" "${file}" 18 | if [ $? -ne 0 ]; then 19 | abort "==> Config match not found: ${needle}" 20 | fi 21 | } 22 | 23 | # Check a config (haystack) for absence of given line (needle) exit with error 24 | # if found. 25 | test_not_config() { 26 | 27 | local needle="${2}" 28 | local file="${1}" 29 | 30 | busybox grep -vq "${needle}" "${file}" 31 | if [ $? -ne 0 ]; then 32 | abort "==> Config match found: ${needle}" 33 | fi 34 | } 35 | 36 | 37 | # 38 | # Generate openvpn.config file 39 | # 40 | 41 | ovpn_genconfig \ 42 | -u udp://$SERV_IP \ 43 | -m 1337 \ 44 | 45 | 46 | EASYRSA_BATCH=1 EASYRSA_REQ_CN="Travis-CI Test CA" ovpn_initpki nopass 47 | 48 | easyrsa build-client-full test1 nopass 2>/dev/null 49 | 50 | ovpn_getclient test1 > "${TEST1_OVPN}" 51 | 52 | 53 | # 54 | # Simple test cases 55 | # 56 | 57 | # 1. client MTU 58 | test_config "${TEST1_OVPN}" "^tun-mtu\s\+1337" 59 | 60 | 61 | # 62 | # Test udp client with tcp fallback 63 | # 64 | ovpn_genconfig -u udp://$SERV_IP -E "remote $SERV_IP 443 tcp" -E "remote vpn.example.com 443 tcp" 65 | # nopass is insecure 66 | EASYRSA_BATCH=1 EASYRSA_REQ_CN="Travis-CI Test CA" ovpn_initpki nopass 67 | easyrsa build-client-full client-fallback nopass 68 | ovpn_getclient client-fallback > "${TEST1_OVPN}" 69 | 70 | test_config "${TEST1_OVPN}" "^remote\s\+$SERV_IP\s\+443\s\+tcp" 71 | test_config "${TEST1_OVPN}" "^remote\s\+vpn.example.com\s\+443\s\+tcp" 72 | 73 | 74 | # 75 | # Test non-defroute config 76 | # 77 | ovpn_genconfig -d -u udp://$SERV_IP -r "172.33.33.0/24" -r "172.34.34.0/24" 78 | # nopass is insecure 79 | EASYRSA_BATCH=1 EASYRSA_REQ_CN="Travis-CI Test CA" ovpn_initpki nopass 80 | easyrsa build-client-full non-defroute nopass 81 | ovpn_getclient non-defroute > "${TEST1_OVPN}" 82 | 83 | # The '!' inverts the match to test that the string isn't present 84 | test_not_config "${TEST1_OVPN}" "^redirect-gateway\s\+def1" 85 | -------------------------------------------------------------------------------- /test/tests/client/run.sh: -------------------------------------------------------------------------------- 1 | ../run-bash-in-container.sh -------------------------------------------------------------------------------- /test/tests/conf_options/container.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SERV_IP=$(ip -4 -o addr show scope global | awk '{print $4}' | sed -e 's:/.*::' | head -n1) 4 | SERVER_CONF="/etc/openvpn/openvpn.conf" 5 | TEST1_OVPN="/etc/openvpn/test1.ovpn" 6 | 7 | # Function to fail 8 | abort() { cat <<< "$@" 1>&2; exit 1; } 9 | 10 | # Check a config (haystack) for a given line (needle) exit with error if not found. 11 | test_config() { 12 | 13 | local needle="${2}" 14 | local file="${1}" 15 | 16 | busybox grep -q "${needle}" "${file}" 17 | if [ $? -ne 0 ]; then 18 | abort "==> Config match not found: ${needle}" 19 | fi 20 | } 21 | 22 | # Check a config (haystack) for absence of given line (needle) exit with error 23 | # if found. 24 | test_not_config() { 25 | 26 | local needle="${2}" 27 | local file="${1}" 28 | 29 | busybox grep -vq "${needle}" "${file}" 30 | if [ $? -ne 0 ]; then 31 | abort "==> Config match found: ${needle}" 32 | fi 33 | } 34 | 35 | 36 | # 37 | # Generate openvpn.config file 38 | # 39 | read -d '' MULTILINE_EXTRA_SERVER_CONF << EOF 40 | management localhost 7505 41 | max-clients 10 42 | EOF 43 | 44 | ovpn_genconfig \ 45 | -u udp://$SERV_IP \ 46 | -f 1400 \ 47 | -k '60 300' \ 48 | -e "$MULTILINE_EXTRA_SERVER_CONF" \ 49 | -e 'duplicate-cn' \ 50 | -e 'topology subnet' \ 51 | -p 'route 172.22.22.0 255.255.255.0' \ 52 | 53 | # Run ovpn_genconfig a second time with no arguments to test its repeatability. 54 | ovpn_genconfig 55 | 56 | # 57 | # Simple test cases 58 | # 59 | 60 | # 1. verb config 61 | test_config "${SERVER_CONF}" "^verb\s\+3" 62 | 63 | # 2. fragment config 64 | test_config "${SERVER_CONF}" "^fragment\s\+1400" 65 | 66 | ## Tests for extra configs 67 | # 3. management config 68 | test_config "${SERVER_CONF}" "^management\s\+localhost\s\+7505" 69 | 70 | # 4. max-clients config 71 | test_config "${SERVER_CONF}" "^max-clients\s\+10" 72 | 73 | # 5. duplicate-cn config 74 | test_config "${SERVER_CONF}" "^duplicate-cn" 75 | 76 | # 6. topology config 77 | test_config "${SERVER_CONF}" "^topology\s\+subnet" 78 | 79 | ## Tests for push config 80 | # 7. push route 81 | test_config "${SERVER_CONF}" '^push\s\+"route\s\+172.22.22.0\s\+255.255.255.0"' 82 | 83 | ## Test for default 84 | # 8. Should see default route if none provided 85 | test_config "${SERVER_CONF}" "^route\s\+192.168.254.0\s\+255.255.255.0" 86 | 87 | # 9. Should see a push of 'block-outside-dns' by default 88 | test_config "${SERVER_CONF}" '^push\s\+"block-outside-dns"' 89 | 90 | # 10. Should see a push of 'dhcp-option DNS' by default 91 | test_config "${SERVER_CONF}" '^push\s\+"dhcp-option\s\+DNS\s\+8.8.8.8"' 92 | test_config "${SERVER_CONF}" '^push\s\+"dhcp-option\s\+DNS\s\+8.8.4.4"' 93 | 94 | ## Test for keepalive 95 | # 11. keepalive config 96 | test_config "${SERVER_CONF}" '^keepalive\s\+60\s\+300' 97 | 98 | 99 | # 100 | # More elaborate route tests 101 | # 102 | 103 | ovpn_genconfig -u udp://$SERV_IP -r "172.33.33.0/24" -r "172.34.34.0/24" 104 | 105 | test_config "${SERVER_CONF}" "^route\s\+172.33.33.0\s\+255.255.255.0" 106 | test_config "${SERVER_CONF}" "^route\s\+172.34.34.0\s\+255.255.255.0" 107 | 108 | 109 | # 110 | # Block outside DNS test 111 | # 112 | 113 | ovpn_genconfig -u udp://$SERV_IP -b 114 | 115 | test_not_config "${SERVER_CONF}" '^push "block-outside-dns"' 116 | cat ${SERVER_CONF} >&1 117 | -------------------------------------------------------------------------------- /test/tests/conf_options/run.sh: -------------------------------------------------------------------------------- 1 | ../run-bash-in-container.sh -------------------------------------------------------------------------------- /test/tests/docker-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # wrapper around "docker build" that creates a temporary directory and copies files into it first so that arbitrary host directories can be copied into containers without bind mounts, but accepts a Dockerfile on stdin 5 | 6 | # usage: ./docker-build.sh some-host-directory some-new-image:some-tag < "$tmp/Dockerfile" 25 | 26 | from="$(awk -F '[ \t]+' 'toupper($1) == "FROM" { print $2; exit }' "$tmp/Dockerfile")" 27 | onbuilds="$(docker inspect -f '{{len .Config.OnBuild}}' "$from")" 28 | if [ "$onbuilds" -gt 0 ]; then 29 | # crap, the image we want to build has some ONBUILD instructions 30 | # those are kind of going to ruin our day 31 | # let's do some hacks to strip those bad boys out in a new fake layer 32 | "$(dirname "$(readlink -f "$BASH_SOURCE")")/remove-onbuild.sh" "$from" "$imageTag" 33 | awk -F '[ \t]+' 'toupper($1) == "FROM" { $2 = "'"$imageTag"'" } { print }' "$tmp/Dockerfile" > "$tmp/Dockerfile.new" 34 | mv "$tmp/Dockerfile.new" "$tmp/Dockerfile" 35 | fi 36 | 37 | cp -RL "$dir" "$tmp/dir" 38 | 39 | docker build -t "$imageTag" "$tmp" > /dev/null 40 | -------------------------------------------------------------------------------- /test/tests/dual-proto/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | [ -n "${DEBUG+x}" ] && set -x 5 | 6 | OVPN_DATA=dual-data 7 | CLIENT_UDP=travis-client 8 | CLIENT_TCP=travis-client-tcp 9 | IMG=kylemanna/openvpn 10 | CLIENT_DIR="$(readlink -f "$(dirname "$BASH_SOURCE")/../../client")" 11 | 12 | ip addr ls 13 | SERV_IP=$(ip -4 -o addr show scope global | awk '{print $4}' | sed -e 's:/.*::' | head -n1) 14 | 15 | # get temporary TCP config 16 | docker run -v $OVPN_DATA:/etc/openvpn --rm $IMG ovpn_genconfig -u tcp://$SERV_IP:443 17 | 18 | # nopass is insecure 19 | docker run -v $OVPN_DATA:/etc/openvpn --rm -it -e "EASYRSA_BATCH=1" -e "EASYRSA_REQ_CN=Travis-CI Test CA" $IMG ovpn_initpki nopass 20 | 21 | # gen TCP client 22 | docker run -v $OVPN_DATA:/etc/openvpn --rm -it $IMG easyrsa build-client-full $CLIENT_TCP nopass 23 | docker run -v $OVPN_DATA:/etc/openvpn --rm $IMG ovpn_getclient $CLIENT_TCP | tee $CLIENT_DIR/config-tcp.ovpn 24 | 25 | # switch to UDP config and gen UDP client 26 | docker run -v $OVPN_DATA:/etc/openvpn --rm $IMG ovpn_genconfig -u udp://$SERV_IP 27 | docker run -v $OVPN_DATA:/etc/openvpn --rm -it $IMG easyrsa build-client-full $CLIENT_UDP nopass 28 | docker run -v $OVPN_DATA:/etc/openvpn --rm $IMG ovpn_getclient $CLIENT_UDP | tee $CLIENT_DIR/config.ovpn 29 | 30 | #Verify client configs 31 | docker run -v $OVPN_DATA:/etc/openvpn --rm $IMG ovpn_listclients | grep $CLIENT_TCP 32 | docker run -v $OVPN_DATA:/etc/openvpn --rm $IMG ovpn_listclients | grep $CLIENT_UDP 33 | 34 | # 35 | # Fire up the server 36 | # 37 | 38 | # Run in shell bg to get logs, setup trap to clean-up 39 | trap "{ jobs -p | xargs -r kill; wait; docker volume rm ${OVPN_DATA}; }" EXIT 40 | docker run --name "ovpn-test-udp" -v $OVPN_DATA:/etc/openvpn --rm --cap-add=NET_ADMIN -e DEBUG $IMG & 41 | docker run --name "ovpn-test-tcp" -v $OVPN_DATA:/etc/openvpn --rm --cap-add=NET_ADMIN -e DEBUG $IMG ovpn_run --proto tcp --port 443 & 42 | 43 | # Update configs 44 | for i in $(seq 10); do 45 | SERV_IP_INTERNAL=$(docker inspect --format '{{ .NetworkSettings.IPAddress }}' "ovpn-test-udp" 2>/dev/null || true) 46 | test -n "$SERV_IP_INTERNAL" && break 47 | sleep 0.1 48 | done 49 | sed -i -e s:$SERV_IP:$SERV_IP_INTERNAL:g $CLIENT_DIR/config.ovpn 50 | 51 | for i in $(seq 10); do 52 | SERV_IP_INTERNAL=$(docker inspect --format '{{ .NetworkSettings.IPAddress }}' "ovpn-test-tcp" 2>/dev/null || true) 53 | test -n "$SERV_IP_INTERNAL" && break 54 | sleep 0.1 55 | done 56 | sed -i -e s:$SERV_IP:$SERV_IP_INTERNAL:g $CLIENT_DIR/config-tcp.ovpn 57 | 58 | # 59 | # Fire up a clients in a containers since openvpn is disallowed by Travis-CI 60 | # 61 | docker run --rm --cap-add=NET_ADMIN -v $CLIENT_DIR:/client -e DEBUG $IMG /client/wait-for-connect.sh 62 | docker run --rm --cap-add=NET_ADMIN -v $CLIENT_DIR:/client -e DEBUG $IMG /client/wait-for-connect.sh "/client/config-tcp.ovpn" 63 | 64 | # 65 | # Celebrate 66 | # 67 | cat < < both ways! > 70 | ------------ ------------ 71 | \ ^__^ ^__^ / 72 | \ (oo)\______/(oo) / 73 | (__)\ /(__) 74 | ||w---w|| 75 | || || 76 | EOF 77 | 78 | -------------------------------------------------------------------------------- /test/tests/image-name.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # usage: ./image-name.sh librarytest/something some/image:some-tag 5 | # output: librarytest/something:some-image-some-tag 6 | 7 | base="$1"; shift 8 | tag="$1"; shift 9 | 10 | echo "$base:$(echo "$tag" | sed 's![:/]!-!g')" 11 | -------------------------------------------------------------------------------- /test/tests/iptables/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | [ -n "${DEBUG+x}" ] && set -x 5 | OVPN_DATA=basic-data 6 | IMG="kylemanna/openvpn" 7 | NAME="ovpn-test" 8 | SERV_IP=$(ip -4 -o addr show scope global | awk '{print $4}' | sed -e 's:/.*::' | head -n1) 9 | 10 | # generate server config including iptables nat-ing 11 | docker volume create --name $OVPN_DATA 12 | docker run --rm -v $OVPN_DATA:/etc/openvpn $IMG ovpn_genconfig -u udp://$SERV_IP -N 13 | docker run -v $OVPN_DATA:/etc/openvpn --rm -it -e "EASYRSA_BATCH=1" -e "EASYRSA_REQ_CN=Travis-CI Test CA" $IMG ovpn_initpki nopass 14 | 15 | # Fire up the server 16 | docker run -d --name $NAME -v $OVPN_DATA:/etc/openvpn --cap-add=NET_ADMIN $IMG 17 | 18 | # check default iptables rules 19 | for i in $(seq 10); do 20 | docker exec -ti $NAME bash -c 'source /etc/openvpn/ovpn_env.sh; exec iptables -t nat -C POSTROUTING -s $OVPN_SERVER -o eth0 -j MASQUERADE' && break 21 | echo waiting for server start-up 22 | sleep 1 23 | done 24 | 25 | # append new setupIptablesAndRouting function to config 26 | docker exec -ti $NAME bash -c 'echo function setupIptablesAndRouting { iptables -t nat -A POSTROUTING -m comment --comment "test"\;} >> /etc/openvpn/ovpn_env.sh' 27 | 28 | # kill server in preparation to modify config 29 | docker rm -f $NAME 30 | 31 | # check that overridden function exists and that test iptables rules is active 32 | docker run -d --name $NAME -v $OVPN_DATA:/etc/openvpn --cap-add=NET_ADMIN $IMG 33 | docker exec -ti $NAME bash -c 'source /etc/openvpn/ovpn_env.sh; type -t setupIptablesAndRouting && iptables -t nat -C POSTROUTING -m comment --comment "test"' 34 | 35 | # 36 | # kill server 37 | # 38 | 39 | docker rm -f $NAME 40 | docker volume rm $OVPN_DATA 41 | -------------------------------------------------------------------------------- /test/tests/otp/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | [ -n "${DEBUG+x}" ] && set -x 5 | 6 | OVPN_DATA=basic-data-otp 7 | CLIENT=travis-client 8 | IMG=kylemanna/openvpn 9 | OTP_USER=otp 10 | CLIENT_DIR="$(readlink -f "$(dirname "$BASH_SOURCE")/../../client")" 11 | 12 | # Function to fail 13 | abort() { cat <<< "$@" 1>&2; exit 1; } 14 | 15 | ip addr ls 16 | SERV_IP=$(ip -4 -o addr show scope global | awk '{print $4}' | sed -e 's:/.*::' | head -n1) 17 | # Configure server with two factor authentication 18 | docker run -v $OVPN_DATA:/etc/openvpn --rm $IMG ovpn_genconfig -u udp://$SERV_IP -2 19 | 20 | # Ensure reneg-sec 0 in server config when two factor is enabled 21 | docker run -v $OVPN_DATA:/etc/openvpn --rm $IMG cat /etc/openvpn/openvpn.conf | grep 'reneg-sec 0' || abort 'reneg-sec not set to 0 in server config' 22 | 23 | # nopass is insecure 24 | docker run -v $OVPN_DATA:/etc/openvpn --rm -it -e "EASYRSA_BATCH=1" -e "EASYRSA_REQ_CN=Travis-CI Test CA" $IMG ovpn_initpki nopass 25 | 26 | docker run -v $OVPN_DATA:/etc/openvpn --rm -it $IMG easyrsa build-client-full $CLIENT nopass 27 | 28 | # Generate OTP credentials for user named test, should return QR code for test user 29 | docker run -v $OVPN_DATA:/etc/openvpn --rm -it $IMG ovpn_otp_user $OTP_USER | tee $CLIENT_DIR/qrcode.txt 30 | # Ensure a chart link is printed in client OTP configuration 31 | grep 'https://www.google.com/chart' $CLIENT_DIR/qrcode.txt || abort 'Link to chart not generated' 32 | grep 'Your new secret key is:' $CLIENT_DIR/qrcode.txt || abort 'Secret key is missing' 33 | # Extract an emergency code from textual output, grepping for line and trimming spaces 34 | OTP_TOKEN=$(grep -A1 'Your emergency scratch codes are' $CLIENT_DIR/qrcode.txt | tail -1 | tr -d '[[:space:]]') 35 | # Token should be present 36 | if [ -z $OTP_TOKEN ]; then 37 | abort "QR Emergency Code not detected" 38 | fi 39 | 40 | # Store authentication credentials in config file and tell openvpn to use them 41 | echo -e "$OTP_USER\n$OTP_TOKEN" > $CLIENT_DIR/credentials.txt 42 | 43 | # Override the auth-user-pass directive to use a credentials file 44 | docker run -v $OVPN_DATA:/etc/openvpn --rm $IMG ovpn_getclient $CLIENT | sed 's/auth-user-pass/auth-user-pass \/client\/credentials.txt/' | tee $CLIENT_DIR/config.ovpn 45 | 46 | # Ensure reneg-sec 0 in client config when two factor is enabled 47 | grep 'reneg-sec 0' $CLIENT_DIR/config.ovpn || abort 'reneg-sec not set to 0 in client config' 48 | 49 | # 50 | # Fire up the server 51 | # 52 | trap "{ jobs -p | xargs -r kill; wait; }" EXIT 53 | docker run --name "ovpn-test" -v $OVPN_DATA:/etc/openvpn --rm --cap-add=NET_ADMIN $IMG & 54 | 55 | for i in $(seq 10); do 56 | SERV_IP_INTERNAL=$(docker inspect --format '{{ .NetworkSettings.IPAddress }}' "ovpn-test" 2>/dev/null || true) 57 | test -n "$SERV_IP_INTERNAL" && break 58 | sleep 0.1 59 | done 60 | sed -i -e s:$SERV_IP:$SERV_IP_INTERNAL:g $CLIENT_DIR/config.ovpn 61 | 62 | # 63 | # Fire up a client in a container since openvpn is disallowed by Travis-CI 64 | docker run --rm --cap-add=NET_ADMIN --volume $CLIENT_DIR:/client -e DEBUG $IMG /client/wait-for-connect.sh 65 | 66 | # 67 | # Celebrate 68 | # 69 | cat < 72 | ----------- 73 | \ ^__^ 74 | \ (oo)\_______ 75 | (__)\ )\/\\ 76 | ||----w | 77 | || || 78 | EOF 79 | -------------------------------------------------------------------------------- /test/tests/paranoid/container.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | SERV_IP=$(ip -4 -o addr show scope global | awk '{print $4}' | sed -e 's:/.*::' | head -n1) 5 | 6 | # 7 | # Generate a simple configuration, returns nonzero on error 8 | # 9 | ovpn_genconfig -u udp://$SERV_IP 2>/dev/null 10 | 11 | export EASYRSA_BATCH=1 12 | export EASYRSA_REQ_CN="Travis-CI Test CA" 13 | 14 | # 15 | # Initialize the certificate PKI state, returns nonzero on error 16 | # 17 | ovpn_initpki nopass 2>/dev/null 18 | 19 | # 20 | # Test back-up 21 | # 22 | ovpn_copy_server_files 23 | -------------------------------------------------------------------------------- /test/tests/paranoid/run.sh: -------------------------------------------------------------------------------- 1 | ../run-bash-in-container.sh -------------------------------------------------------------------------------- /test/tests/revocation/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | [ -n "${DEBUG+x}" ] && set -x 5 | 6 | OVPN_DATA="ovpn-revoke-test-data" 7 | CLIENT1="travis-client1" 8 | CLIENT2="travis-client2" 9 | IMG="kylemanna/openvpn" 10 | NAME="ovpn-revoke-test" 11 | CLIENT_DIR="$(readlink -f "$(dirname "$BASH_SOURCE")/../../client")" 12 | SERV_IP="$(ip -4 -o addr show scope global | awk '{print $4}' | sed -e 's:/.*::' | head -n1)" 13 | 14 | # 15 | # Initialize openvpn configuration and pki. 16 | # 17 | docker volume create --name $OVPN_DATA 18 | docker run --rm -v $OVPN_DATA:/etc/openvpn $IMG ovpn_genconfig -u udp://$SERV_IP 19 | docker run --rm -v $OVPN_DATA:/etc/openvpn -it -e "EASYRSA_BATCH=1" -e "EASYRSA_REQ_CN=Travis-CI Test CA" $IMG ovpn_initpki nopass 20 | 21 | # Register clean-up function 22 | function finish { 23 | # Stop the server and clean up 24 | docker rm -f $NAME 25 | docker volume rm $OVPN_DATA 26 | jobs -p | xargs -r kill 27 | wait 28 | } 29 | trap finish EXIT 30 | 31 | # Put the server in the background 32 | docker run -d -v $OVPN_DATA:/etc/openvpn --cap-add=NET_ADMIN --name $NAME $IMG 33 | 34 | # 35 | # Test that easy_rsa generate CRLs with 'next publish' set to 3650 days. 36 | # 37 | crl_next_update="$(docker exec $NAME bash -c "openssl crl -nextupdate -noout -in \$EASYRSA_PKI/crl.pem | cut -d'=' -f2 | tr -d 'GMT'")" 38 | crl_next_update="$(date -u -d "$crl_next_update" "+%s")" 39 | now="$(docker exec $NAME date "+%s")" 40 | crl_remain="$(( $crl_next_update - $now ))" 41 | crl_remain="$(( $crl_remain / 86400 ))" 42 | if (( $crl_remain < 3649 )); then 43 | echo "easy_rsa CRL next publish set to less than 3650 days." >&2 44 | exit 2 45 | fi 46 | 47 | # 48 | # Generate a first client certificate and configuration using $CLIENT1 as CN then revoke it. 49 | # 50 | docker exec -it $NAME easyrsa build-client-full $CLIENT1 nopass 51 | docker exec -it $NAME ovpn_getclient $CLIENT1 > $CLIENT_DIR/config.ovpn 52 | docker exec -it $NAME bash -c "echo 'yes' | ovpn_revokeclient $CLIENT1" 53 | 54 | # Determine IP address of container running daemon and update config 55 | for i in $(seq 10); do 56 | SERV_IP_INTERNAL=$(docker inspect --format '{{ .NetworkSettings.IPAddress }}' "$NAME" 2>/dev/null || true) 57 | test -n "$SERV_IP_INTERNAL" && break 58 | sleep 0.1 59 | done 60 | sed -i -e s:$SERV_IP:$SERV_IP_INTERNAL:g $CLIENT_DIR/config.ovpn 61 | 62 | # 63 | # Test that openvpn client can't connect using $CLIENT1 config. 64 | # 65 | if docker run --rm -v $CLIENT_DIR:/client --cap-add=NET_ADMIN -e DEBUG $IMG /client/wait-for-connect.sh; then 66 | echo "Client was able to connect after revocation test #1." >&2 67 | exit 2 68 | fi 69 | 70 | # 71 | # Generate and revoke a second client certificate using $CLIENT2 as CN, then test for failed client connection. 72 | # 73 | docker exec -it $NAME easyrsa build-client-full $CLIENT2 nopass 74 | docker exec -it $NAME ovpn_getclient $CLIENT2 > $CLIENT_DIR/config.ovpn 75 | docker exec -it $NAME bash -c "echo 'yes' | ovpn_revokeclient $CLIENT2" 76 | 77 | # Determine IP address of container running daemon and update config 78 | for i in $(seq 10); do 79 | SERV_IP_INTERNAL=$(docker inspect --format '{{ .NetworkSettings.IPAddress }}' "$NAME" 2>/dev/null || true) 80 | test -n "$SERV_IP_INTERNAL" && break 81 | sleep 0.1 82 | done 83 | 84 | if docker run --rm -v $CLIENT_DIR:/client --cap-add=NET_ADMIN -e DEBUG $IMG /client/wait-for-connect.sh; then 85 | echo "Client was able to connect after revocation test #2." >&2 86 | exit 2 87 | fi 88 | 89 | # 90 | # Restart the server 91 | # 92 | docker stop $NAME && docker start $NAME 93 | 94 | # 95 | # Test for failed connection using $CLIENT2 config again. 96 | # 97 | if docker run --rm -v $CLIENT_DIR:/client --cap-add=NET_ADMIN -e DEBUG $IMG /client/wait-for-connect.sh; then 98 | echo "Client was able to connect after revocation test #3." >&2 99 | exit 2 100 | fi 101 | 102 | # 103 | # Celebrate 104 | # 105 | cat < 108 | ----------- 109 | \ ^__^ 110 | \ (oo)\_______ 111 | (__)\ )\/\\ 112 | ||----w | 113 | || || 114 | EOF 115 | -------------------------------------------------------------------------------- /test/tests/run-bash-in-container.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | testDir="$(readlink -f "$(dirname "$BASH_SOURCE")")" 5 | runDir="$(dirname "$(readlink -f "$BASH_SOURCE")")" 6 | 7 | source "$runDir/run-in-container.sh" "$testDir" "$1" bash ./container.sh 8 | -------------------------------------------------------------------------------- /test/tests/run-in-container.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # NOT INTENDED TO BE USED AS A TEST "run.sh" DIRECTLY 5 | # SEE OTHER "run-*-in-container.sh" SCRIPTS FOR USAGE 6 | 7 | testDir="$1" 8 | shift 9 | 10 | image="$1" 11 | shift 12 | entrypoint="$1" 13 | shift 14 | 15 | # do some fancy footwork so that if testDir is /a/b/c, we mount /a/b and use c as the working directory (so relative symlinks work one level up) 16 | thisDir="$(dirname "$(readlink -f "$BASH_SOURCE")")" 17 | testDir="$(readlink -f "$testDir")" 18 | testBase="$(basename "$testDir")" 19 | hostMount="$(dirname "$testDir")" 20 | containerMount="/tmp/test-dir" 21 | workdir="$containerMount/$testBase" 22 | # TODO should we be doing something fancy with $BASH_SOURCE instead so we can be arbitrarily deep and mount the top level always? 23 | 24 | newImage="$("$thisDir/image-name.sh" librarytest/run-in-container "$image--$testBase")" 25 | "$thisDir/docker-build.sh" "$hostMount" "$newImage" <