├── Dockerfile ├── LICENSE ├── README.md ├── bin ├── func.sh ├── ovpn_getclient ├── ovpn_initpki ├── ovpn_listclients └── ovpn_otp_user ├── docs ├── FAQ.md └── variables.md ├── entrypoint.sh ├── kube ├── configmaps-example.yaml ├── deploy.sh ├── deployment-minimal.yaml ├── deployment.yaml ├── routeme.yaml ├── routing1.dia ├── routing1.png ├── routing2.dia ├── routing2.png └── update-crl.sh ├── openvpn.tmpl ├── otp └── openvpn └── watch-portmapping.sh /Dockerfile: -------------------------------------------------------------------------------- 1 | # Smallest base image 2 | FROM alpine:3.10 3 | 4 | MAINTAINER Pieter Lange 5 | 6 | RUN echo "http://dl-cdn.alpinelinux.org/alpine/edge/community/" >> /etc/apk/repositories && \ 7 | echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing/" >> /etc/apk/repositories && \ 8 | apk add --update openvpn=2.4.7-r1 \ 9 | bash easy-rsa libintl inotify-tools openvpn-auth-pam google-authenticator pamtester && \ 10 | apk add --virtual temppkg gettext && \ 11 | cp /usr/bin/envsubst /usr/local/bin/envsubst && \ 12 | ln -s /usr/share/easy-rsa/easyrsa /usr/local/bin && \ 13 | apk del temppkg && \ 14 | rm -rf /tmp/* /var/tmp/* /var/cache/apk/* /var/cache/distfiles/* 15 | 16 | # Needed by scripts 17 | ENV OPENVPN /etc/openvpn 18 | ENV OVPN_TEMPLATE $OPENVPN/templates/openvpn.tmpl 19 | ENV OVPN_CONFIG $OPENVPN/openvpn.conf 20 | 21 | ENV OVPN_PORTMAPPING $OPENVPN/portmapping 22 | ENV OVPN_CRL $OPENVPN/crl/crl.pem 23 | ENV OVPN_CCD $OPENVPN/ccd 24 | ENV OVPN_DEFROUTE 0 25 | 26 | ENV OVPN_CIPHER "AES-256-CBC" 27 | ENV OVPN_TLS_CIPHER "TLS-ECDHE-RSA-WITH-AES-128-GCM-SHA256:TLS-ECDHE-ECDSA-WITH-AES-128-GCM-SHA256:TLS-ECDHE-RSA-WITH-AES-256-GCM-SHA384" 28 | 29 | ENV EASYRSA /usr/share/easy-rsa 30 | ENV EASYRSA_PKI $OPENVPN/pki 31 | 32 | # Some PKI scripts. 33 | ADD ./bin /usr/local/bin 34 | RUN chmod a+x /usr/local/bin/* 35 | 36 | # Initialisation scripts and default template 37 | COPY *.sh /sbin/ 38 | COPY openvpn.tmpl $OVPN_TEMPLATE 39 | 40 | # Add support for OTP authentication using a PAM module 41 | ADD ./otp/openvpn /etc/pam.d/ 42 | 43 | CMD ["/sbin/entrypoint.sh"] 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Pieter Lange 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | kube-openvpn 2 | ================== 3 | 4 | [![Docker Repository on Quay](https://quay.io/repository/plange/openvpn/status "Docker Repository on Quay")](https://quay.io/repository/plange/openvpn) 5 | [![Docker Repository on Docker Hub](https://img.shields.io/docker/automated/ptlange/openvpn.svg "Docker Repository on Docker Hub")](https://hub.docker.com/r/ptlange/openvpn/) 6 | 7 | ## Synopsis 8 | Simple OpenVPN deployment using native kubernetes semantics. There is no persistent storage, CA management (key storage, cert signing) needs to be done outside of the cluster for now. I think this is better - unless you leave your keys on your dev laptop. 9 | 10 | ## Motivation 11 | The main motivator for this project was having the ability to route service requests back to local apps (running on the VPN client), making life much easier for development environments where developers cannot run the entire app stack locally but need to iterate on 1 app quickly. 12 | 13 | ## Usage 14 | First, you need to initialize your PKI infrastructure. Easyrsa is bundled in this container, so this is fairly easy. Replace `OVPN_SERVER_URL` with your endpoint to-be. 15 | ``` 16 | $ docker run --user=$(id -u) -e OVPN_SERVER_URL=tcp://vpn.my.fqdn:1194 -v $PWD:/etc/openvpn:z -ti ptlange/openvpn ovpn_initpki 17 | ``` 18 | Follow the instructions on screen. Remember (or better: securely store) your secure password for the CA. You are now left with a `pki` folder in your current working directory. 19 | 20 | Generate the initial Certificate Revocation List. This file needs to be updated every `$EASYRSA_CRL_DAYS`. All clients will be blocked when this file expires. 21 | ``` 22 | $ docker run --user=$(id -u) -e EASYRSA_CRL_DAYS=180 -v $PWD:/etc/openvpn:z -ti ptlange/openvpn easyrsa gen-crl 23 | ``` 24 | 25 | 26 | Getting service_cidr and pod_cidr within google cloud: 27 | 28 | service_cidr: 29 | 30 | ``` 31 | gcloud container clusters describe | grep servicesIpv4Cidr 32 | ``` 33 | 34 | pod_cidr: 35 | 36 | ``` 37 | gcloud container clusters describe | grep clusterIpv4Cidr 38 | ``` 39 | 40 | Deploy the VPN server (namespace needs to exist already): 41 | 42 | ``` 43 | $ ./kube/deploy.sh 44 | Usage: ./kube/deploy.sh 45 | 46 | $ ./kube/deploy.sh default tcp://vpn.my.fqdn:1194 10.3.0.0/24 10.2.0.0/16 47 | secret "openvpn-pki" created 48 | configmap "openvpn-settings" created 49 | configmap "openvpn-ccd" created 50 | deployment "openvpn" created 51 | You have exposed your service on an external port on all nodes in your 52 | cluster. If you want to expose this service to the external internet, you may 53 | need to set up firewall rules for the service port(s) (tcp:30xxx) to serve traffic. 54 | 55 | See http://releases.k8s.io/release-1.3/docs/user-guide/services-firewalls.md for more details. 56 | service "openvpn-ingress" created 57 | ``` 58 | 59 | Your VPN endpoint is now reachable on every node in the cluster on port 30XXX. This port can be easily exposed by setting the `Type` field of the openvpn Service to `LoadBalancer` if you're running your cluster within a public cloud. Assign the correct `CNAME`/`A` address to your loadbalancer or replace the original servername with the DNS name of your newly created loadbalancer in your client configuration. 60 | 61 | ## Accessing the cluster 62 | With the pki still in `$PWD/pki` we can create a new VPN user and grab the `.ovpn` configuration file: 63 | 64 | ``` 65 | # Generate VPN client credentials for CLIENTNAME without password protection; leave 'nopass' out to enter password 66 | $ docker run --user=$(id -u) -v $PWD:/etc/openvpn:z -ti ptlange/openvpn easyrsa build-client-full CLIENTNAME nopass 67 | $ docker run --user=$(id -u) -e OVPN_SERVER_URL=tcp://vpn.my.fqdn:1194 -v $PWD:/etc/openvpn:z --rm ptlange/openvpn ovpn_getclient CLIENTNAME > CLIENTNAME.ovpn 68 | ``` 69 | 70 | `CLIENTNAME.ovpn` can now be used to connect to the cluster and interact with k8s services and pods directly. Whoohoo! 71 | 72 | ![One-way traffic](kube/routing1.png "Direct access to kubernetes services") 73 | 74 | 75 | ## Routing back to the client 76 | 77 | In order to route cluster traffic back to a service running on the client, we need to assign `CLIENTNAME` a static IP. If you have not configured an `$OVPN_NETWORK` you need to pick something in the `10.140.0.0/24` range. 78 | 79 | Edit the CCD (client configuration directory) configmap: 80 | ``` 81 | $ kubectl edit configmap openvpn-ccd 82 | ``` 83 | Look at the example and add another entry for the `CLIENTNAME` you added before. You do not have to restart openvpn but if you are already connected you will need to reconnect to get the static IP. 84 | 85 | Next you have to define what port on the openvpn pod to route back to the client. The port forwarding will automatically load after the configmap has been updated. 86 | ``` 87 | $ kubectl edit configmap openvpn-portmapping 88 | ``` 89 | 90 | ![Two-way traffic](kube/routing2.png "Direct access to the client from other kubernetes services!") 91 | 92 | You can now reach the openvpn client! If you want to substitute a kubernetes service for a service running on the client, simply modify the label selector to match your openvpn endpoint address and the `targetPort` just configured in the openvpn-portmapping configmap. 93 | 94 | Exampe service definition routing service `myapp` on port 80 to the `example` client's service running on port 80. 95 | ```yaml 96 | apiVersion: v1 97 | kind: Service 98 | metadata: 99 | name: myapp 100 | spec: 101 | ports: 102 | - port: 80 103 | targetPort: 20080 104 | selector: 105 | openvpn: vpn.my.fqdn 106 | ``` 107 | 108 | ## Custom openvpn configuration 109 | User-specific settings need to be set in the client config directory by editing the `openvpn-ccd` ConfigMap 110 | 111 | You can also use your own openvpn configuration by mounting in a `openvpn.tmpl` file in `/etc/openvpn/templates/`. Create your own `openvpn.tmpl` from the example in this repository. Load it into a configmap with `kubectl create configmap openvpn-template --from-file=openvpn.tmpl`. Now edit the openvpn deployment configuration and add in an extra mountpoint at `/etc/openvpn/templates` for the `openvpn-template` configmap. 112 | 113 | Note that you can use environment variables in the template! 114 | 115 | ## Updating the CRL 116 | Use the `crl-update.sh` script. This extends the CRL for another 182 days by default. If you automate this i recommend setting this far shorter. 117 | 118 | ``` 119 | $ ./kube/update-crl default 120 | ``` 121 | 122 | ## Tests 123 | NONE. See next section. 124 | 125 | ## Contributing 126 | Please file issues on github. PR's are welcomed. 127 | 128 | ## Thanks 129 | I used kylemanna/openvpn extensively for a long time and lend quite a few of his scripts for easing the PKI handling. offlinehacker/openvpn-k8s provided some inspiration as well and showed i can run openvpn without any persistent storage, prompting me to write this collection of scripts. 130 | -------------------------------------------------------------------------------- /bin/func.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Build runtime arguments array based on environment 4 | USER_ARGS=("${@}") 5 | ARGS=() 6 | 7 | # Checks if ARGS already contains the given value 8 | function hasArg { 9 | local element 10 | for element in "${@:2}"; do 11 | [ "${element}" == "${1}" ] && return 0 12 | done 13 | return 1 14 | } 15 | 16 | # Adds the given argument if it's not already specified. 17 | function addArg { 18 | local arg="${1}" 19 | shift 20 | if ! hasArg "${arg}" "${USER_ARGS[@]}"; then 21 | ARGS+=("${arg}") 22 | if [ $# -ge 1 ]; then 23 | for val in "$@"; do 24 | ARGS+=("${val}") 25 | done 26 | fi 27 | fi 28 | } 29 | 30 | # Convert 1.2.3.4/24 -> 255.255.255.0 31 | cidr2mask() 32 | { 33 | local i 34 | local subnetmask="" 35 | local cidr=${1#*/} 36 | local full_octets=$(($cidr/8)) 37 | local partial_octet=$(($cidr%8)) 38 | 39 | for ((i=0;i<4;i+=1)); do 40 | if [ $i -lt $full_octets ]; then 41 | subnetmask+=255 42 | elif [ $i -eq $full_octets ]; then 43 | subnetmask+=$((256 - 2**(8-$partial_octet))) 44 | else 45 | subnetmask+=0 46 | fi 47 | [ $i -lt 3 ] && subnetmask+=. 48 | done 49 | echo $subnetmask 50 | } 51 | 52 | # Used often enough to justify a function 53 | getroute() { 54 | echo ${1%/*} $(cidr2mask $1) 55 | } 56 | 57 | # Server name is in the form "udp://vpn.example.com:1194" 58 | if [[ "$OVPN_SERVER_URL" =~ ^((udp|tcp)://)?([0-9a-zA-Z\.\-]+)(:([0-9]+))?$ ]]; then 59 | OVPN_PROTO=${BASH_REMATCH[2]}; 60 | OVPN_CN=${BASH_REMATCH[3]}; 61 | OVPN_PORT=${BASH_REMATCH[5]}; 62 | else 63 | echo "Need to pass in OVPN_SERVER_URL in 'proto://fqdn:port' format" 64 | exit 1 65 | fi 66 | -------------------------------------------------------------------------------- /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 | 17 | if ! source "/usr/local/bin/func.sh"; then 18 | echo "Could not source /usr/local/bin/func.sh" 19 | exit 1 20 | fi 21 | 22 | if [ -z "$EASYRSA_PKI" ]; then 23 | export EASYRSA_PKI="$OPENVPN/pki" 24 | fi 25 | 26 | cn="$1" 27 | parm="$2" 28 | 29 | if [ ! -f "$EASYRSA_PKI/private/${cn}.key" ]; then 30 | echo "Unable to find \"${cn}\", please try again or generate the key first" >&2 31 | exit 1 32 | fi 33 | 34 | get_client_config() { 35 | mode="$1" 36 | echo " 37 | client 38 | nobind 39 | dev tun 40 | key-direction 1 41 | remote-cert-tls server 42 | 43 | remote $OVPN_CN $OVPN_PORT $OVPN_PROTO 44 | " 45 | if [ "$mode" == "combined" ]; then 46 | echo " 47 | 48 | $(cat $EASYRSA_PKI/private/${cn}.key) 49 | 50 | 51 | $(openssl x509 -in $EASYRSA_PKI/issued/${cn}.crt) 52 | 53 | 54 | $(cat $EASYRSA_PKI/ca.crt) 55 | 56 | 57 | $(cat $EASYRSA_PKI/ta.key) 58 | 59 | key-direction 1 60 | " 61 | elif [ "$mode" == "separated" ]; then 62 | echo " 63 | key ${cn}.key 64 | ca ca.crt 65 | cert ${cn}.crt 66 | tls-auth ta.key 1 67 | $OVPN_ADDITIONAL_CLIENT_CONFIG 68 | " 69 | fi 70 | 71 | if [ "$OVPN_DEFROUTE" != "0" ];then 72 | echo "redirect-gateway def1" 73 | fi 74 | 75 | if [ -n "$OVPN_MTU" ]; then 76 | echo "tun-mtu $OVPN_MTU" 77 | fi 78 | 79 | if [ -n "$OVPN_TLS_CIPHER" ]; then 80 | echo "tls-cipher $OVPN_TLS_CIPHER" 81 | fi 82 | 83 | if [ -n "$OVPN_CIPHER" ]; then 84 | echo "cipher $OVPN_CIPHER" 85 | fi 86 | 87 | if [ -n "$OVPN_AUTH" ]; then 88 | echo "auth $OVPN_AUTH" 89 | fi 90 | 91 | if [ -n "$OVPN_OTP_AUTH" ]; then 92 | echo "auth-user-pass" 93 | echo "auth-nocache" 94 | fi 95 | 96 | if [ -n "$OVPN_COMP_LZO" ]; then 97 | echo "comp-lzo" 98 | fi 99 | } 100 | 101 | dir="$OPENVPN/clients/$cn" 102 | case "$parm" in 103 | "separated") 104 | mkdir -p "$dir" 105 | get_client_config "$parm" > "$dir/${cn}.ovpn" 106 | cp "$EASYRSA_PKI/private/${cn}.key" "$dir/${cn}.key" 107 | cp "$EASYRSA_PKI/ca.crt" "$dir/ca.crt" 108 | cp "$EASYRSA_PKI/issued/${cn}.crt" "$dir/${cn}.crt" 109 | cp "$EASYRSA_PKI/ta.key" "$dir/ta.key" 110 | ;; 111 | "" | "combined") 112 | get_client_config "combined" 113 | ;; 114 | "combined-save") 115 | get_client_config "combined" > "$dir/${cn}-combined.ovpn" 116 | ;; 117 | *) 118 | echo "This script can produce the client configuration in to formats:" >&2 119 | 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 120 | echo " 2. separated: Separated files." >&2 121 | echo "Please specific one of those options as second parameter." >&2 122 | ;; 123 | esac 124 | -------------------------------------------------------------------------------- /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 /usr/local/bin/func.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 | # For a server key with a password, manually init; this is autopilot 30 | easyrsa build-server-full "$OVPN_CN" nopass 31 | -------------------------------------------------------------------------------- /bin/ovpn_listclients: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Server name is in the form "udp://vpn.example.com:1194" 4 | if [[ "$OVPN_SERVER_URL" =~ ^((udp|tcp)://)?([0-9a-zA-Z\.\-]+)(:([0-9]+))?$ ]]; then 5 | OVPN_PROTO=${BASH_REMATCH[2]}; 6 | OVPN_CN=${BASH_REMATCH[3]}; 7 | OVPN_PORT=${BASH_REMATCH[5]}; 8 | else 9 | echo "Need to pass in OVPN_SERVER_URL in 'proto://fqdn:port' format" 10 | exit 1 11 | fi 12 | 13 | cd "$EASYRSA_PKI" 14 | 15 | if [ -e crl.pem ]; then 16 | cat ca.crt crl.pem > cacheck.pem 17 | fi 18 | 19 | echo "name,begin,end,status" 20 | for name in issued/*.crt; do 21 | path=$name 22 | begin=$(openssl x509 -noout -startdate -in $path | awk -F= '{ print $2 }') 23 | end=$(openssl x509 -noout -enddate -in $path | awk -F= '{ print $2 }') 24 | 25 | name=${name%.crt} 26 | name=${name#issued/} 27 | if [ "$name" != "$OVPN_" ]; then 28 | if [ -e crl.pem ]; then 29 | if openssl verify -crl_check -CAfile cacheck.pem $path &> /dev/null; then 30 | status="VALID" 31 | else 32 | status="REVOKED" 33 | fi 34 | else 35 | status="VALID" 36 | fi 37 | 38 | echo "$name,$begin,$end,$status" 39 | fi 40 | done 41 | 42 | if [ -e crl.pem ]; then 43 | rm cacheck.pem 44 | fi 45 | -------------------------------------------------------------------------------- /bin/ovpn_otp_user: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # Generate OpenVPN users via google authenticator 5 | # 6 | 7 | if [ -z $1 ]; then 8 | echo "Usage: ovpn_otp_user USERNAME" 9 | exit 1 10 | fi 11 | 12 | # Server name is in the form "udp://vpn.example.com:1194" 13 | if [[ "$OVPN_SERVER_URL" =~ ^((udp|tcp)://)?([0-9a-zA-Z\.\-]+)(:([0-9]+))?$ ]]; then 14 | OVPN_PROTO=${BASH_REMATCH[2]}; 15 | OVPN_CN=${BASH_REMATCH[3]}; 16 | OVPN_PORT=${BASH_REMATCH[5]}; 17 | else 18 | echo "Need to pass in OVPN_SERVER_URL in 'proto://fqdn:port' format" 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 | 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 | fi 34 | -------------------------------------------------------------------------------- /docs/FAQ.md: -------------------------------------------------------------------------------- 1 | # Frequently asked questions 2 | 3 | ### "I don't like easyrsa" 4 | Many people don't! This is fine, you can use your own PKI management system like [xca](http://xca.sourceforge.net). All you need to do is map the correct files into the `openvpn-pki` secret object and mount it into `/etc/openvpn/pki`. Refer to `deploy.sh` for details. 5 | 6 | ### I can't ping my kubernetes service 7 | Kubernetes services aren't pingable, try connecting to the service port. 8 | 9 | ### I can't lookup my kubernetes service 10 | - Check if `kube-dns` is configured correctly 11 | - Check if you received the correct resolver (see `/etc/resolv.conf` on your client) 12 | - Make sure you can resolve the service by it's full cluster FQDN (eg `kubernetes.default.svc.cluster.local`) 13 | 14 | ### I can't lookup my kubernetes service (OSX) 15 | See https://github.com/michthom/AlwaysAppendSearchDomains 16 | 17 | Execute the following steps: 18 | * `sudo launchctl unload /System/Library/LaunchDaemons/com.apple.mDNSResponder.plist` 19 | * `sudo defaults write /Library/Preferences/com.apple.mDNSResponder.plist AlwaysAppendSearchDomains -bool YES` 20 | * `sudo launchctl load /System/Library/LaunchDaemons/com.apple.mDNSResponder.plist` 21 | 22 | ### The pod keeps crashing! 23 | This shouldn't happen! Please set the `$DEBUG` environment variable (any value) and file an issue with a full logfile. 24 | 25 | ### Weird reachability issues 26 | Make sure you're not already using `10.140.0.0/24` in your architecture. If you are, set the `$OVPN_NETWORK` environment variable to something non-conflicting 27 | 28 | ### I want to route all traffic through the VPN 29 | Set `$OVPN_DEFROUTE` to a value of `1` on the kubernetes pod to enable VPN clients to route to other networks than the kubernetes pod/service networks. Set `$OVPN_DEFROUTE` to `2` to also push this configuration to the openvpn clients. 30 | 31 | ### Revoking clients 32 | This works just like any other PKI system. If you followed the setup instructions, you're using easyrsa and client revocation is done as follows. 33 | 34 | Revoke the client (where is the client name): 35 | ``` 36 | docker run --user=$(id -u) -e OVPN_SERVER_URL=tcp://vpn.my.fqdn:1194 -v $PWD:/etc/openvpn -ti ptlange/openvpn easyrsa revoke 37 | ``` 38 | 39 | Now update the CRL on the cluster: 40 | ``` 41 | ./kube/update-crl.sh [#days the CRL is valid] 42 | ``` 43 | -------------------------------------------------------------------------------- /docs/variables.md: -------------------------------------------------------------------------------- 1 | # Standard environment variables 2 | 3 | * **OVPN_K8S_SERVICE_NETWORK** - The IP address space of the kubernetes service network in CIDR notation. **required** Default: none) 4 | * **OVPN_K8S_POD_NETWORK** - The IP address space of the kubernetes pod overlay network in CIDR notation. **required** (Default: none) 5 | * **OVPN_SERVER_URL** - The openvpn endpoint this pod is exposed as in `$proto://$fqdn:$port` notation. **required** (Default: none) 6 | * OVPN_NETWORK - The openvpn client network. (Default: `10.140.0.0/24`) 7 | * OVPN_PROTO - The openvpn protocol used. (Default: set from `OVPN_SERVER_URL`) 8 | * OVPN_NATDEVICE - The outgoing device that routes to the kubernetes overlay network. (Default: `eth0`) 9 | * OVPN_K8S_DOMAIN - The DNS search domain pushed to clients. (Default: `svc.cluster.local`) 10 | * OVPN_K8S_DNS - The DNS resolver pushed to clients. (Default: resolver used in openvpn pod itself) 11 | * OVPN_VERB - The verbosity of openvpn logs. (Default: `3`) 12 | * OVPN_DEFROUTE - Whether to allow clients to route traffic other than pod/service networks. Set to `1` to allow, set to `2` to push a default route to clients. (Default: `0`) 13 | * OVPN_ROUTES - Comma separated list of CIDR routes to push to clients and configure firewall rules for (Default: `$OVPN_K8S_SERVICE_NETWORK,$OVPN_K8S_POD_NETWORK`) 14 | * DEBUG - Set this variable to any value to print each command executed and set `OVPN_DEBUG` to `5`. 15 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | [[ $DEBUG ]] && set -x && OVPN_VERB=${OVPN_VERB:-5} 3 | 4 | set -ae 5 | 6 | source /usr/local/bin/func.sh 7 | 8 | addArg "--config" "$OVPN_CONFIG" 9 | 10 | # Server name is in the form "udp://vpn.example.com:1194" 11 | if [[ "$OVPN_SERVER_URL" =~ ^((udp|tcp)(4|6)?://)?([0-9a-zA-Z\.\-]+)(:([0-9]+))?$ ]]; then 12 | OVPN_PROTO=${BASH_REMATCH[2]}; 13 | OVPN_CN=${BASH_REMATCH[4]}; 14 | OVPN_PORT=${BASH_REMATCH[6]}; 15 | else 16 | echo "Need to pass in OVPN_SERVER_URL in 'proto://fqdn:port' format" 17 | exit 1 18 | fi 19 | 20 | OVPN_NETWORK="${OVPN_NETWORK:-10.140.0.0/24}" 21 | OVPN_PROTO="${OVPN_PROTO:-tcp}" 22 | OVPN_NATDEVICE="${OVPN_NATDEVICE:-eth0}" 23 | OVPN_K8S_DOMAIN="${OVPN_K8S_DOMAIN:-svc.cluster.local}" 24 | OVPN_VERB=${OVPN_VERB:-3} 25 | OVPN_STATUS_VERSION=${OVPN_STATUS_VERSION:-2} 26 | 27 | if [ ! -d "${EASYRSA_PKI}" ]; then 28 | echo "PKI directory missing. Did you mount in your Secret?" 29 | exit 1 30 | fi 31 | 32 | if [ -z "${OVPN_K8S_SERVICE_NETWORK}" ]; then 33 | echo "Service network not specified" 34 | exit 1 35 | fi 36 | 37 | if [ -z "${OVPN_K8S_POD_NETWORK}" ]; then 38 | echo "Pod network not specified" 39 | exit 1 40 | fi 41 | 42 | # You don't need to set this variable unless you touched your dnsPolicy for this pod. 43 | if [ -z "${OVPN_K8S_DNS}" ]; then 44 | OVPN_K8S_DNS=$(cat /etc/resolv.conf | grep -i nameserver | head -n1 | cut -d ' ' -f2) 45 | fi 46 | 47 | # Do some CIDR conversion 48 | OVPN_NETWORK_ROUTE=$(getroute ${OVPN_NETWORK}) 49 | OVPN_K8S_SERVICE_NETWORK_ROUTE=$(getroute $OVPN_K8S_SERVICE_NETWORK) 50 | OVPN_K8S_POD_NETWORK_ROUTE=$(getroute $OVPN_K8S_POD_NETWORK) 51 | 52 | envsubst < $OVPN_TEMPLATE > $OVPN_CONFIG 53 | 54 | IFS=',' read -r -a routes <<< "$OVPN_ROUTES" 55 | routes+=("$OVPN_K8S_SERVICE_NETWORK" "$OVPN_K8S_POD_NETWORK") 56 | 57 | for route in "${routes[@]}"; do 58 | if [[ "$route" =~ ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$ ]]; then 59 | addArg "--push" "route $(getroute $route)" 60 | else 61 | echo "$(date "+%a %b %d %H:%M:%S %Y") Dropping invalid route '${route}'." 62 | routes=("${routes[@]/$route}" ) 63 | fi 64 | done 65 | 66 | if [ $OVPN_DEFROUTE -gt 0 ]; then 67 | iptables -t nat -A POSTROUTING -s ${OVPN_NETWORK} -o ${OVPN_NATDEVICE} -j SNAT --to-source $PODIPADDR 68 | [ $OVPN_DEFROUTE -gt 1 ] && addArg "--push" "redirect-gateway def1" 69 | else 70 | for route in "${routes[@]}"; do 71 | iptables -t nat -A POSTROUTING -s ${OVPN_NETWORK} -d $route -o ${OVPN_NATDEVICE} -j SNAT --to-source $PODIPADDR 72 | done 73 | fi 74 | 75 | # Use client configuration directory if it exists. 76 | if [ -d "$OVPN_CCD" ]; then 77 | addArg "--client-config-dir" "$OVPN_CCD" 78 | 79 | # Watch for changes to port translation configmap in the background 80 | /sbin/watch-portmapping.sh & 81 | fi 82 | 83 | mkdir -p /dev/net 84 | if [ ! -c /dev/net/tun ]; then 85 | mknod /dev/net/tun c 10 200 86 | fi 87 | 88 | # Load CRL if it is readable (remember to set defaultMode: 555 (ugo+rx) on the volume) 89 | if [ -r $OVPN_CRL ]; then 90 | addArg "--crl-verify" "$OVPN_CRL" 91 | fi 92 | 93 | # Optional OTP authentication support 94 | if [ -d "${OVPN_OTP_AUTH:-}" ]; then 95 | addArg "--plugin" "/usr/lib/openvpn/plugins/openvpn-plugin-auth-pam.so" "openvpn" 96 | addArg "--reneg-sec" "0" 97 | fi 98 | 99 | if [ -n "${OVPN_MANAGEMENT_PORT}" ]; then 100 | addArg "--management" "127.0.0.1" "${OVPN_MANAGEMENT_PORT}" 101 | fi 102 | 103 | if [ -n "${OVPN_STATUS}" ]; then 104 | addArg "--status" "${OVPN_STATUS}" 105 | addArg "--status-version" "${OVPN_STATUS_VERSION}" 106 | fi 107 | 108 | if [ $DEBUG ]; then 109 | echo "openvpn.conf:" 110 | cat $OVPN_CONFIG 111 | fi 112 | 113 | echo "$(date "+%a %b %d %H:%M:%S %Y") Running 'openvpn ${ARGS[@]} ${USER_ARGS[@]}'" 114 | exec openvpn "${ARGS[@]}" "${USER_ARGS[@]}" 1> /dev/stderr 2> /dev/stderr 115 | -------------------------------------------------------------------------------- /kube/configmaps-example.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: openvpn-ccd 5 | data: 6 | example: "ifconfig-push 10.140.0.5 255.255.255.0" 7 | --- 8 | apiVersion: v1 9 | kind: ConfigMap 10 | metadata: 11 | name: openvpn-portmapping 12 | data: 13 | 20080: "example:80" 14 | --- 15 | -------------------------------------------------------------------------------- /kube/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | [ -z $4 ] && echo "Usage: $0 " && exit 1 4 | 5 | namespace=$1 6 | serverurl=$2 7 | servicecidr=$3 8 | podcidr=$4 9 | domain=${5:-svc.cluster.local} 10 | 11 | # Server name is in the form "udp://vpn.example.com:1194" 12 | if [[ "$serverurl" =~ ^((udp|tcp)(4|6)?://)?([0-9a-zA-Z\.\-]+)(:([0-9]+))?$ ]]; then 13 | OVPN_PROTO=$(echo ${BASH_REMATCH[2]} | tr '[:lower:]' '[:upper:]') 14 | OVPN_CN=$(echo ${BASH_REMATCH[4]} | tr '[:upper:]' '[:lower:]') 15 | OVPN_PORT=${BASH_REMATCH[6]}; 16 | else 17 | echo "Need to pass in OpenVPN URL in 'proto://fqdn:port' format" 18 | echo "eg: tcp://my.fully.qualified.domain.com:1194" 19 | exit 1 20 | fi 21 | OVPN_PORT="${OVPN_PORT:-1194}" 22 | 23 | if [ ! -d pki ]; then 24 | echo "This script requires a directory named 'pki' in the current working directory, populated with a CA generated by easyrsa" 25 | echo "You can easily generate this. Execute the following command and follow the instructions on screen:" 26 | echo "docker run -e OVPN_SERVER_URL=$serverurl -v $PWD:/etc/openvpn:z -ti ptlange/openvpn ovpn_initpki" 27 | exit 1 28 | fi 29 | 30 | # test if -w0 is a valid option 31 | base64 -w0 /dev/null > /dev/null 2>&1 32 | if [ $? -eq 0 ]; then 33 | base64="base64 -w0" 34 | else 35 | base64="base64" 36 | fi 37 | 38 | kuberes='./kube/kube-resources' 39 | mkdir -p $kuberes 40 | 41 | echo "Generating Kubernetes resources" 42 | 43 | cat << EOSECRETS > $kuberes/openvpn-pki.yaml 44 | apiVersion: v1 45 | kind: Secret 46 | metadata: 47 | name: openvpn-pki 48 | type: Opaque 49 | data: 50 | private.key: "$($base64 pki/private/${OVPN_CN}.key)" 51 | ca.crt: "$($base64 pki/ca.crt)" 52 | certificate.crt: "$($base64 pki/issued/${OVPN_CN}.crt)" 53 | dh.pem: "$($base64 pki/dh.pem)" 54 | ta.key: "$($base64 pki/ta.key)" 55 | --- 56 | EOSECRETS 57 | 58 | cat << EOCONFIGMAP > $kuberes/openvpn-settings.yaml 59 | apiVersion: v1 60 | kind: ConfigMap 61 | metadata: 62 | name: openvpn-settings 63 | data: 64 | servicecidr: "${servicecidr}" 65 | podcidr: "${podcidr}" 66 | serverurl: "${serverurl}" 67 | domain: "${domain}" 68 | statusfile: "/etc/openvpn/status/server.status" 69 | --- 70 | EOCONFIGMAP 71 | 72 | cat << EOSERVICE > $kuberes/openvpn-svc.yaml 73 | --- 74 | apiVersion: v1 75 | kind: Service 76 | metadata: 77 | labels: 78 | openvpn: ${OVPN_CN} 79 | name: openvpn 80 | spec: 81 | type: NodePort 82 | ports: 83 | - port: ${OVPN_PORT} 84 | protocol: ${OVPN_PROTO} 85 | targetPort: 1194 86 | selector: 87 | openvpn: ${OVPN_CN} 88 | --- 89 | EOSERVICE 90 | 91 | echo "Creating and applying Kubernetes resources" 92 | kubectl create configmap --namespace=$namespace openvpn-crl --from-file=crl.pem=$PWD/pki/crl.pem 93 | kubectl apply --namespace=$namespace -f ./kube/configmaps-example.yaml 94 | kubectl apply --namespace=$namespace -f $kuberes/openvpn-pki.yaml 95 | kubectl apply --namespace=$namespace -f $kuberes/openvpn-settings.yaml 96 | kubectl apply --namespace=$namespace -f $kuberes/openvpn-svc.yaml 97 | sed "s/\${OVPN_CN}/${OVPN_CN}/g;" kube/deployment.yaml | kubectl create --namespace=$namespace -f - 98 | -------------------------------------------------------------------------------- /kube/deployment-minimal.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: openvpn 5 | spec: 6 | revisionHistoryLimit: 1 7 | replicas: 1 8 | template: 9 | metadata: 10 | labels: 11 | openvpn: ${OVPN_CN} 12 | spec: 13 | restartPolicy: Always 14 | terminationGracePeriodSeconds: 60 15 | containers: 16 | - name: openvpn 17 | image: ptlange/openvpn:latest 18 | securityContext: 19 | capabilities: 20 | add: 21 | - NET_ADMIN 22 | resources: 23 | limits: 24 | cpu: 200m 25 | memory: 100Mi 26 | requests: 27 | cpu: 100m 28 | memory: 50Mi 29 | volumeMounts: 30 | - mountPath: /etc/openvpn/pki 31 | name: openvpn-pki 32 | env: 33 | - name: PODIPADDR 34 | valueFrom: 35 | fieldRef: 36 | fieldPath: status.podIP 37 | - name: OVPN_SERVER_URL 38 | valueFrom: 39 | configMapKeyRef: 40 | name: openvpn-settings 41 | key: serverurl 42 | - name: OVPN_K8S_SERVICE_NETWORK 43 | valueFrom: 44 | configMapKeyRef: 45 | name: openvpn-settings 46 | key: servicecidr 47 | - name: OVPN_K8S_POD_NETWORK 48 | valueFrom: 49 | configMapKeyRef: 50 | name: openvpn-settings 51 | key: podcidr 52 | volumes: 53 | - name: openvpn-pki 54 | secret: 55 | secretName: openvpn-pki 56 | -------------------------------------------------------------------------------- /kube/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: openvpn 5 | spec: 6 | revisionHistoryLimit: 1 7 | replicas: 1 8 | template: 9 | metadata: 10 | labels: 11 | openvpn: ${OVPN_CN} 12 | annotations: 13 | prometheus.io/scrape: "true" 14 | prometheus.io/port: "9176" 15 | spec: 16 | restartPolicy: Always 17 | terminationGracePeriodSeconds: 60 18 | containers: 19 | - name: openvpn 20 | image: quay.io/plange/openvpn:latest 21 | securityContext: 22 | capabilities: 23 | add: 24 | - NET_ADMIN 25 | resources: 26 | limits: 27 | cpu: 200m 28 | memory: 100Mi 29 | requests: 30 | cpu: 100m 31 | memory: 50Mi 32 | volumeMounts: 33 | - mountPath: /etc/openvpn/pki 34 | name: openvpn-pki 35 | - mountPath: /etc/openvpn/crl 36 | name: openvpn-crl 37 | - mountPath: /etc/openvpn/ccd 38 | name: openvpn-ccd 39 | - mountPath: /etc/openvpn/portmapping 40 | name: openvpn-portmapping 41 | - mountPath: /etc/openvpn/status 42 | name: openvpn-status 43 | ports: 44 | - name: openvpn 45 | containerPort: 1194 46 | env: 47 | - name: PODIPADDR 48 | valueFrom: 49 | fieldRef: 50 | fieldPath: status.podIP 51 | - name: OVPN_SERVER_URL 52 | valueFrom: 53 | configMapKeyRef: 54 | name: openvpn-settings 55 | key: serverurl 56 | - name: OVPN_K8S_SERVICE_NETWORK 57 | valueFrom: 58 | configMapKeyRef: 59 | name: openvpn-settings 60 | key: servicecidr 61 | - name: OVPN_K8S_POD_NETWORK 62 | valueFrom: 63 | configMapKeyRef: 64 | name: openvpn-settings 65 | key: podcidr 66 | - name: OVPN_K8S_DOMAIN 67 | valueFrom: 68 | configMapKeyRef: 69 | name: openvpn-settings 70 | key: domain 71 | - name: OVPN_STATUS 72 | valueFrom: 73 | configMapKeyRef: 74 | name: openvpn-settings 75 | key: statusfile 76 | - name: metrics 77 | image: quay.io/plange/openvpn_exporter:latest 78 | ports: 79 | - name: openvpn 80 | containerPort: 9176 81 | volumeMounts: 82 | - mountPath: /etc/openvpn_exporter/ 83 | name: openvpn-status 84 | volumes: 85 | - name: openvpn-pki 86 | secret: 87 | secretName: openvpn-pki 88 | defaultMode: 0400 89 | - name: openvpn-ccd 90 | configMap: 91 | name: openvpn-ccd 92 | - name: openvpn-crl 93 | configMap: 94 | defaultMode: 0555 95 | name: openvpn-crl 96 | - name: openvpn-portmapping 97 | configMap: 98 | name: openvpn-portmapping 99 | - name: openvpn-status 100 | emptyDir: {} 101 | -------------------------------------------------------------------------------- /kube/routeme.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: openvpn-portmapping 5 | data: 6 | ${NAT_PORT}: "${USER_CNAME}:${USER_PORT}" 7 | --- 8 | apiVersion: v1 9 | kind: Service 10 | metadata: 11 | name: ${SERVICE_NAME} 12 | spec: 13 | ports: 14 | - port: ${SERVICE_PORT} 15 | targetPort: ${NAT_PORT} 16 | selector: 17 | openvpn: ${OVPN_CN} 18 | --- 19 | -------------------------------------------------------------------------------- /kube/routing1.dia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pieterlange/kube-openvpn/9fe1fea1cf5adbf49c9db20c4875b2a3c82e8b04/kube/routing1.dia -------------------------------------------------------------------------------- /kube/routing1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pieterlange/kube-openvpn/9fe1fea1cf5adbf49c9db20c4875b2a3c82e8b04/kube/routing1.png -------------------------------------------------------------------------------- /kube/routing2.dia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pieterlange/kube-openvpn/9fe1fea1cf5adbf49c9db20c4875b2a3c82e8b04/kube/routing2.dia -------------------------------------------------------------------------------- /kube/routing2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pieterlange/kube-openvpn/9fe1fea1cf5adbf49c9db20c4875b2a3c82e8b04/kube/routing2.png -------------------------------------------------------------------------------- /kube/update-crl.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | [ -z $1 ] && echo "Usage: $0 [days]" && exit 1 4 | 5 | namespace=$1 6 | days=${2:-182} 7 | 8 | if [ ! -d pki ]; then 9 | echo "This script requires a directory named 'pki' in the current working directory, populated with a CA generated by easyrsa" 10 | exit 1 11 | fi 12 | 13 | docker run --user=$(id -u) -e EASYRSA_CRL_DAYS=$days -v $PWD:/etc/openvpn -ti ptlange/openvpn easyrsa gen-crl 14 | 15 | kubectl delete configmap --namespace=$namespace openvpn-crl 16 | kubectl create configmap --namespace=$namespace openvpn-crl --from-file=crl.pem=$PWD/pki/crl.pem 17 | -------------------------------------------------------------------------------- /openvpn.tmpl: -------------------------------------------------------------------------------- 1 | server ${OVPN_NETWORK_ROUTE} 2 | topology subnet 3 | verb ${OVPN_VERB} 4 | 5 | # Filled by Secrets object. Use generic names 6 | key ${EASYRSA_PKI}/private.key 7 | ca ${EASYRSA_PKI}/ca.crt 8 | cert ${EASYRSA_PKI}/certificate.crt 9 | 10 | dh none 11 | ecdh-curve secp256k1 12 | 13 | key-direction 0 14 | keepalive 10 60 15 | persist-key 16 | persist-tun 17 | push "block-outside-dns" 18 | 19 | proto ${OVPN_PROTO} 20 | cipher ${OVPN_CIPHER} 21 | tls-cipher ${OVPN_TLS_CIPHER} 22 | 23 | # Rely on scheduler to do port mapping, internally always 1194 24 | port 1194 25 | dev tun0 26 | 27 | user nobody 28 | group nogroup 29 | 30 | push "dhcp-option DOMAIN ${OVPN_K8S_DOMAIN}" 31 | push "dhcp-option DNS ${OVPN_K8S_DNS}" 32 | -------------------------------------------------------------------------------- /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/tree/master/libpam#secretpathtosecretfile--usersome-user 4 | auth required pam_google_authenticator.so secret=${OVPN_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 | -------------------------------------------------------------------------------- /watch-portmapping.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | [ $DEBUG ] && set -x 3 | 4 | iptables -t nat -N KUBEOPENVPNPORTFORWARD 5 | iptables -t nat -A PREROUTING -j KUBEOPENVPNPORTFORWARD 6 | 7 | while true; do 8 | if [ -d $OVPN_CCD ]; then 9 | 10 | if [ -d $OVPN_PORTMAPPING ]; then 11 | # Flush any old NAT rules. 12 | iptables -t nat -F KUBEOPENVPNPORTFORWARD 13 | 14 | for port in $(ls -1 ${OVPN_PORTMAPPING}); do 15 | dest_cname=$(cut -d':' -f1 ${OVPN_PORTMAPPING}/${port}) 16 | dest_port=$(cut -d':' -f2 ${OVPN_PORTMAPPING}/${port}) 17 | if [ -f ${OVPN_CCD}/${dest_cname} ]; then 18 | dest_ip=$(grep 'ifconfig-push' $OVPN_CCD/${dest_cname} | cut -d' ' -f2) 19 | echo "$(date "+%a %b %d %H:%M:%S %Y") Routing ${PODIPADDR}:${port} to ${dest_ip}:${dest_port} (${dest_cname})" 20 | iptables -t nat -A KUBEOPENVPNPORTFORWARD -p tcp -d $PODIPADDR --dport ${port} -j DNAT --to ${dest_ip}:${dest_port} 21 | else 22 | echo "$(date "+%a %b %d %H:%M:%S %Y") ERROR: client configuration for ${dest_cname} not found" 23 | fi 24 | done 25 | 26 | # Done. Block for updates to configmap. 27 | inotifywait -qq -e modify -e create -e delete $OVPN_PORTMAPPING 28 | else 29 | # Watch $OPENVPN directory for portmapping directory creation 30 | inotifywait -qq -e create $OPENVPN 31 | 32 | # Give it time to settle 33 | sleep 60 34 | fi 35 | else 36 | # Watch $OPENVPN directory for client configuration directory creation 37 | inotifywait -qq -e create $OPENVPN 38 | 39 | # Give it time to settle 40 | sleep 60 41 | fi 42 | done 43 | --------------------------------------------------------------------------------