From a42db0219c83ea9e48ac09fa636c3cd7d21e2321 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lauri=20V=C3=B5sandi?= Date: Wed, 2 Jun 2021 15:44:19 +0300 Subject: [PATCH] Move OpenVPN and StrongSwan entrypoint to separate Docker images --- Dockerfile | 2 +- MANIFEST.in | 1 + helpers/openvpn-client-connect.py | 15 -- helpers/openvpn-learn-address.py | 28 --- helpers/updown.py | 31 --- pinecrypt/server/api/utils/firewall.py | 6 +- pinecrypt/server/authority.py | 1 + pinecrypt/server/cli.py | 238 +++++++----------------- pinecrypt/server/const.py | 17 +- pinecrypt/server/templates/ipsec.conf | 21 +++ pinecrypt/server/templates/openvpn.conf | 69 +++++++ 11 files changed, 171 insertions(+), 258 deletions(-) delete mode 100755 helpers/openvpn-client-connect.py delete mode 100755 helpers/openvpn-learn-address.py delete mode 100755 helpers/updown.py create mode 100644 pinecrypt/server/templates/ipsec.conf create mode 100644 pinecrypt/server/templates/openvpn.conf diff --git a/Dockerfile b/Dockerfile index 3470c54..3bdbe46 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:20.04 as base +FROM ubuntu:20.04 ENV container docker ENV PYTHONUNBUFFERED=1 ENV LC_ALL C.UTF-8 diff --git a/MANIFEST.in b/MANIFEST.in index 9f8ed11..9a0dd44 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,5 @@ include README.md +include pinecrypt/server/templates/*.conf include pinecrypt/server/templates/mail/*.md include pinecrypt/server/builder/overlay/usr/bin/pinecrypt.server-* include pinecrypt/server/builder/overlay/etc/uci-defaults/* diff --git a/helpers/openvpn-client-connect.py b/helpers/openvpn-client-connect.py deleted file mode 100755 index 3683159..0000000 --- a/helpers/openvpn-client-connect.py +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/python3 -import os -import sys -from pinecrypt.server import db - -# This implements OCSP like functionality - -obj = db.certificates.find_one({ - # TODO: use digest instead - "serial_number": "%x" % int(os.environ["tls_serial_0"]), - "status":"signed", -}) - -if not obj: - sys.exit(1) diff --git a/helpers/openvpn-learn-address.py b/helpers/openvpn-learn-address.py deleted file mode 100755 index e0764c7..0000000 --- a/helpers/openvpn-learn-address.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/python3 -import os -import sys -from pinecrypt.server import db -from datetime import datetime - -operation, addr = sys.argv[1:3] -if operation == "delete": - pass -else: - common_name = sys.argv[3] - db.certificates.update_one({ - # TODO: use digest instead - "serial_number": "%x" % int(os.environ["tls_serial_0"]), - "status":"signed", - }, { - "$set": { - "last_seen": datetime.utcnow(), - "instance": os.environ["instance"], - "remote": { - "port": int(os.environ["untrusted_port"]), - "addr": os.environ["untrusted_ip"], - } - }, - "$addToSet": { - "ip": addr - } - }) diff --git a/helpers/updown.py b/helpers/updown.py deleted file mode 100755 index e5e277d..0000000 --- a/helpers/updown.py +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/python3 -import sys -import os -from pinecrypt.server import db -from datetime import datetime - -addrs = set() -for key, value in os.environ.items(): - if key.startswith("PLUTO_PEER_SOURCEIP"): - addrs.add(value) - -with open("/instance") as fh: - instance = fh.read().strip() - -db.certificates.update_one({ - "distinguished_name": os.environ["PLUTO_PEER_ID"], - "status":"signed", -}, { - "$set": { - "last_seen": datetime.utcnow(), - "instance": instance, - "remote": { - "addr": os.environ["PLUTO_PEER"] - } - }, - "$addToSet": { - "ip": { - "$each": list(addrs) - } - } -}) diff --git a/pinecrypt/server/api/utils/firewall.py b/pinecrypt/server/api/utils/firewall.py index 2824c94..be0fb91 100644 --- a/pinecrypt/server/api/utils/firewall.py +++ b/pinecrypt/server/api/utils/firewall.py @@ -169,7 +169,7 @@ def authenticate(optional=False): user, passwd = b64decode(token).decode("utf-8").split(":", 1) if "ldap" in const.AUTHENTICATION_BACKENDS: - upn = "%s@%s" % (user, const.KERBEROS_REALM) + upn = ("%s@%s" % (user, const.KERBEROS_REALM)).lower() click.echo("Connecting to %s as %s" % (const.LDAP_AUTHENTICATION_URI, upn)) conn = ldap.initialize(const.LDAP_AUTHENTICATION_URI, bytes_mode=False) conn.set_option(ldap.OPT_REFERRALS, 0) @@ -185,9 +185,9 @@ def authenticate(optional=False): raise except ldap.INVALID_CREDENTIALS: logger.critical("LDAP bind authentication failed for user %s from %s", - repr(user), req.context["remote"]["addr"]) + repr(upn), req.context["remote"]["addr"]) raise falcon.HTTPUnauthorized( - description="Please authenticate with %s domain account username" % const.DOMAIN, + description="Please authenticate with %s domain account username" % const.KERBEROS_REALM, challenges=["Basic"]) req.context["ldap_conn"] = conn diff --git a/pinecrypt/server/authority.py b/pinecrypt/server/authority.py index 91e1d55..81cd8cd 100644 --- a/pinecrypt/server/authority.py +++ b/pinecrypt/server/authority.py @@ -81,6 +81,7 @@ def self_enroll(skip_notify=False): cert, buf = sign(mongo_id=id, skip_notify=skip_notify, overwrite=True, profile="Gateway", namespace=const.AUTHORITY_NAMESPACE) + os.umask(0o133) with open(const.SELF_CERT_PATH + ".part", "wb") as fh: fh.write(buf) os.rename(const.SELF_CERT_PATH + ".part", const.SELF_CERT_PATH) diff --git a/pinecrypt/server/cli.py b/pinecrypt/server/cli.py index 4f613b2..c06678e 100644 --- a/pinecrypt/server/cli.py +++ b/pinecrypt/server/cli.py @@ -20,7 +20,9 @@ import pytz from asn1crypto import pem, x509 from certbuilder import CertificateBuilder, pem_armor_certificate from datetime import datetime, timedelta +from jinja2 import Environment, PackageLoader from oscrypto import asymmetric +from math import log, ceil from pinecrypt.server import const, mongolog, mailer, db from pinecrypt.server.middleware import NormalizeMiddleware, PrometheusEndpoint from pinecrypt.server.common import cn_to_dn, generate_serial @@ -340,8 +342,8 @@ def pinecone_provision(): with open(const.AUTHORITY_PRIVATE_KEY_PATH + ".part", "wb") as f: f.write(obj["key"]) - # Set permission bits to 640 - os.umask(0o137) + # Set permission bits to 644 + os.umask(0o133) with open(const.AUTHORITY_CERTIFICATE_PATH + ".part", "wb") as f: f.write(obj["cert"]) @@ -371,6 +373,71 @@ def pinecone_provision(): } }) + # Separate pushed subnets by address family + push4 = set() + push6 = set() + for subnet in const.PUSH_SUBNETS: + if subnet.version == 4: + push4.add(subnet) + elif subnet.version == 6: + if not const.CLIENT_SUBNET6: + raise ValueError("Can't push IPv6 routes if no IPv6 client subnet is configured") + push6.add(subnet) + else: + raise NotImplementedError() + + # Generate OpenVPN configurations + click.echo("Generating OpenVPN configuration files...") + from pinecrypt.server import config + ctx = { + "authority_namespace": const.AUTHORITY_NAMESPACE, + "push4": push4, + "push6": push6, + "openvpn_tls_version_min": config.get("Globals", "OPENVPN_TLS_VERSION_MIN")["value"], + "openvpn_tls_ciphersuites": config.get("Globals", "OPENVPN_TLS_CIPHERSUITES")["value"], + "openvpn_tls_cipher": config.get("Globals", "OPENVPN_TLS_CIPHER")["value"], + "openvpn_cipher": config.get("Globals", "OPENVPN_CIPHER")["value"], + "openvpn_auth": config.get("Globals", "OPENVPN_AUTH")["value"], + "strongswan_dhgroup": config.get("Globals", "STRONGSWAN_DHGROUP")["value"], + "strongswan_ike": config.get("Globals", "STRONGSWAN_IKE")["value"], + "strongswan_esp": config.get("Globals", "STRONGSWAN_ESP")["value"], + } + + env = Environment(loader=PackageLoader("pinecrypt.server", "templates")) + os.umask(0o133) + + d = ceil(log(const.CLIENT_SUBNET_SLOT_COUNT) / log(2)) + for slot, proto in enumerate(["udp", "tcp"]): + ctx["proto"] = proto + ctx["slot4"] = list(const.CLIENT_SUBNET4.subnets(d))[slot] + ctx["slot6"] = list(const.CLIENT_SUBNET6.subnets(d))[slot] if const.CLIENT_SUBNET6 else None + with open("/server-secrets/openvpn-%s.conf" % proto, "w") as fh: + fh.write(env.get_template("openvpn.conf").render(ctx)) + + # Merged variants for StrongSwan + ctx["push"] = ctx["push4"].union(ctx["push6"]) + + # Generate StrongSwan config + click.echo("Generating StrongSwan configuration files...") + slot += 1 + ctx["slot4"] = list(const.CLIENT_SUBNET4.subnets(d))[slot] + ctx["slot6"] = list(const.CLIENT_SUBNET6.subnets(d))[slot] if const.CLIENT_SUBNET6 else [] + with open("/server-secrets/ipsec.conf", "w") as fh: + fh.write(env.get_template("ipsec.conf").render(ctx)) + + # Why do you do this StrongSwan?! You will parse the cert anyway, + # why do I need to distinguish ECDSA vs RSA in config?! + with open(const.SELF_CERT_PATH, "rb") as fh: + certificate_buf = fh.read() + header, _, certificate_der_bytes = pem.unarmor(certificate_buf) + certificate = x509.Certificate.load(certificate_der_bytes) + public_key = asymmetric.load_public_key(certificate["tbs_certificate"]["subject_public_key_info"]) + with open("/server-secrets/ipsec.secrets", "w") as fh: + fh.write(": %s %s\n" % ( + "ECDSA" if public_key.algorithm == "ec" else "RSA", + const.SELF_KEY_PATH + )) + # TODO: use this task to send notification emails maybe? click.echo("Finished starting up") sleep(86400) @@ -491,173 +558,6 @@ def pinecone_session(): pass def entry_point(): pass -@click.command("openvpn", help="Start OpenVPN server process") -@click.option("--local", "-l", default="0.0.0.0", help="OpenVPN listening address, defaults to all interfaces") -@click.option("--proto", "-t", default="udp", type=click.Choice(["udp", "tcp"]), help="OpenVPN transport protocol, UDP by default") -@click.option("--client-subnet-slot", "-s", type=int, help="Client subnet slot index") -@waitfile(const.SELF_CERT_PATH) -def pinecone_serve_openvpn(local, proto, client_subnet_slot): - from pinecrypt.server import config - # TODO: Generate (per-client configs) from MongoDB - executable = "/usr/sbin/openvpn" - - args = executable, - slot4 = const.CLIENT_SUBNET4_SLOTS[client_subnet_slot] - args += "--server", str(slot4.network_address), str(slot4.netmask), - if const.CLIENT_SUBNET6: - args += "--server-ipv6", str(const.CLIENT_SUBNET6_SLOTS[client_subnet_slot]), - args += "--local", local - - # Support only two modes TCP 443 and UDP 1194 - if proto == "tcp": - args += "--dev", "tuntcp0", - args += "--port-share", "127.0.0.1", "1443", - args += "--proto", "tcp-server", - args += "--port", "443", - args += "--socket-flags", "TCP_NODELAY", - args += "--management", "127.0.0.1", "7506", - instance = "%s-openvpn-tcp-443" % const.FQDN - else: - args += "--dev", "tunudp0", - args += "--proto", "udp", - args += "--port", "1194", - args += "--management", "127.0.0.1", "7505", - instance = "%s-openvpn-udp-1194" % const.FQDN - args += "--setenv", "instance", instance - db.certificates.update_many({ - "instance": instance - }, { - "$unset": { - "ip": "", - "instance": "", - } - }) - - # Send keep alive packets, mainly for UDP - args += "--keepalive", "60", "120", - - args += "--opt-verify", - - args += "--key", const.SELF_KEY_PATH - args += "--cert", const.SELF_CERT_PATH - args += "--ca", const.AUTHORITY_CERTIFICATE_PATH - - if const.PUSH_SUBNETS: - args += "--push", "route-metric 1000" - for subnet in const.PUSH_SUBNETS: - if subnet.version == 4: - args += "--push", "route %s %s" % (subnet.network_address, subnet.netmask), - elif subnet.version == 6: - if not const.CLIENT_SUBNET6: - raise ValueError("Can't push IPv6 routes if no IPv6 client subnet is configured") - args += "--push", "route-ipv6 %s" % subnet - else: - raise NotImplementedError() - - # TODO: Figure out how to do dhparam without blocking initially - if os.path.exists(const.DHPARAM_PATH): - args += "--dh", const.DHPARAM_PATH - else: - args += "--dh", "none" - - # For more info see: openvpn --show-tls - args += "--tls-version-min", config.get("Globals", "OPENVPN_TLS_VERSION_MIN")["value"] - args += "--tls-ciphersuites", config.get("Globals", "OPENVPN_TLS_CIPHERSUITES")["value"], # Used by TLS 1.3 - args += "--tls-cipher", config.get("Globals", "OPENVPN_TLS_CIPHER")["value"], # Used by TLS 1.2 - - # Data channel encryption parameters - # TODO: Rename to --data-cipher when OpenVPN 2.5 becomes available - args += "--cipher", config.get("Globals", "OPENVPN_CIPHER")["value"] - args += "--auth", config.get("Globals", "OPENVPN_AUTH")["value"] - - # Just to sanity check ourselves - args += "--tls-cert-profile", "preferred", - - # Disable cipher negotiation since we know what we want - args += "--ncp-disable", - - args += "--script-security", "2", - args += "--learn-address", "/helpers/openvpn-learn-address.py" - args += "--client-connect", "/helpers/openvpn-client-connect.py" - args += "--verb", "0", - - logger.info("Executing: %s" % (" ".join(args))) - os.execv(executable, args) - - -@click.command("strongswan", help="Start StrongSwan") -@click.option("--client-subnet-slot", "-s", type=int, help="Client subnet slot index") -@waitfile(const.SELF_CERT_PATH) -def pinecone_serve_strongswan(client_subnet_slot): - from pinecrypt.server import config - slots = [] - slots.append(const.CLIENT_SUBNET4_SLOTS[client_subnet_slot]) - if const.CLIENT_SUBNET6: - slots.append(const.CLIENT_SUBNET6_SLOTS[client_subnet_slot]) - - with open("/etc/ipsec.conf", "w") as fh: - fh.write("config setup\n") - fh.write(" strictcrlpolicy=yes\n") - fh.write(" charondebug=\"cfg 2\"\n") - - fh.write("\n") - fh.write("ca authority\n") - fh.write(" auto=add\n") - fh.write(" cacert=%s\n" % const.AUTHORITY_CERTIFICATE_PATH) - fh.write("\n") - fh.write("conn s2c\n") - fh.write(" auto=add\n") - fh.write(" keyexchange=ikev2\n") - - fh.write(" left=%s\n" % const.AUTHORITY_NAMESPACE) - fh.write(" leftsendcert=always\n") - fh.write(" leftallowany=yes\n") # For load-balancing - fh.write(" leftcert=%s\n" % const.SELF_CERT_PATH) - if const.PUSH_SUBNETS: - fh.write(" leftsubnet=%s\n" % ",".join([str(j) for j in const.PUSH_SUBNETS])) - fh.write(" leftupdown=/helpers/updown.py\n") - - fh.write(" right=%any\n") - fh.write(" rightsourceip=%s\n" % ",".join([str(j) for j in slots])) - fh.write(" ike=%s!\n" % config.get("Globals", "STRONGSWAN_IKE")["value"]) - fh.write(" esp=%s!\n" % config.get("Globals", "STRONGSWAN_ESP")["value"]) - with open("/etc/ipsec.conf") as fh: - print(fh.read()) - - # Why do you do this StrongSwan?! You will parse the cert anyway, - # why do I need to distinguish ECDSA vs RSA in config?! - with open(const.SELF_CERT_PATH, "rb") as fh: - certificate_buf = fh.read() - header, _, certificate_der_bytes = pem.unarmor(certificate_buf) - certificate = x509.Certificate.load(certificate_der_bytes) - public_key = asymmetric.load_public_key(certificate["tbs_certificate"]["subject_public_key_info"]) - with open("/etc/ipsec.secrets", "w") as fh: - fh.write(": %s %s\n" % ( - "ECDSA" if public_key.algorithm == "ec" else "RSA", - const.SELF_KEY_PATH - )) - executable = "/usr/sbin/ipsec" - args = executable, "start", "--nofork" - logger.info("Executing: %s" % (" ".join(args))) - instance = "%s-ipsec" % const.FQDN - - db.certificates.update_many({ - "instance": instance - }, { - "$unset": { - "ip": "", - "instance": "", - } - }) - - # TODO: Find better way to push env vars to updown script - with open("/instance", "w") as fh: - fh.write(instance) - os.execv(executable, args) - - -pinecone_serve.add_command(pinecone_serve_openvpn) -pinecone_serve.add_command(pinecone_serve_strongswan) pinecone_serve.add_command(pinecone_serve_backend) pinecone_serve.add_command(pinecone_serve_ocsp_responder) pinecone_serve.add_command(pinecone_serve_events) diff --git a/pinecrypt/server/const.py b/pinecrypt/server/const.py index beb0b00..d334675 100644 --- a/pinecrypt/server/const.py +++ b/pinecrypt/server/const.py @@ -6,7 +6,6 @@ import socket import sys from datetime import timedelta from ipaddress import ip_network -from math import log, ceil RE_USERNAME = r"^[a-z][a-z0-9]+$" RE_FQDN = r"^(([a-z0-9]|[a-z0-9][a-z0-9\-_]*[a-z0-9])\.)+([a-z0-9]|[a-z0-9][a-z0-9\-_]*[a-z0-9])?$" @@ -46,11 +45,11 @@ CURVE_NAME = "secp384r1" # Kerberos-like clock skew tolerance CLOCK_SKEW_TOLERANCE = timedelta(minutes=5) -AUTHORITY_PRIVATE_KEY_PATH = "/var/lib/certidude/authority-secrets/ca_key.pem" -AUTHORITY_CERTIFICATE_PATH = "/var/lib/certidude/server-secrets/ca_cert.pem" -SELF_CERT_PATH = "/var/lib/certidude/server-secrets/self_cert.pem" -SELF_KEY_PATH = "/var/lib/certidude/server-secrets/self_key.pem" -DHPARAM_PATH = "/var/lib/certidude/server-secrets/dhparam.pem" +AUTHORITY_PRIVATE_KEY_PATH = "/authority-secrets/ca_key.pem" +AUTHORITY_CERTIFICATE_PATH = "/server-secrets/ca_cert.pem" +SELF_CERT_PATH = "/server-secrets/self_cert.pem" +SELF_KEY_PATH = "/server-secrets/self_key.pem" +DHPARAM_PATH = "/server-secrets/dhparam.pem" BUILDER_TARBALLS = "" FQDN = socket.getfqdn() @@ -114,7 +113,7 @@ TOKEN_OVERWRITE_PERMITTED = os.getenv("TOKEN_OVERWRITE_PERMITTED") AUTHENTICATION_BACKENDS = set(["ldap"]) MAIL_SUFFIX = os.getenv("MAIL_SUFFIX") -KERBEROS_KEYTAB = os.getenv("KERBEROS_KEYTAB", "/var/lib/certidude/server-secrets/krb5.keytab") +KERBEROS_KEYTAB = os.getenv("KERBEROS_KEYTAB", "/server-secrets/krb5.keytab") KERBEROS_REALM = os.getenv("KERBEROS_REALM") LDAP_AUTHENTICATION_URI = os.getenv("LDAP_AUTHENTICATION_URI") LDAP_GSSAPI_CRED_CACHE = os.getenv("LDAP_GSSAPI_CRED_CACHE", "/run/certidude/krb5cc") @@ -158,13 +157,9 @@ REQUEST_SUBMISSION_ALLOWED = os.getenv("REQUEST_SUBMISSION_ALLOWED") REVOCATION_LIST_LIFETIME = os.getenv("REVOCATION_LIST_LIFETIME") PUSH_SUBNETS = [ip_network(j) for j in os.getenv("PUSH_SUBNETS", "").replace(" ", ",").split(",") if j] - CLIENT_SUBNET4 = ip_network(os.getenv("CLIENT_SUBNET4", "192.168.33.0/24")) CLIENT_SUBNET6 = ip_network(os.getenv("CLIENT_SUBNET6")) if os.getenv("CLIENT_SUBNET6") else None CLIENT_SUBNET_SLOT_COUNT = int(os.getenv("CLIENT_SUBNET_COUNT", 4)) -divisions = ceil(log(CLIENT_SUBNET_SLOT_COUNT) / log(2)) -CLIENT_SUBNET4_SLOTS = list(CLIENT_SUBNET4.subnets(divisions)) -CLIENT_SUBNET6_SLOTS = list(CLIENT_SUBNET6.subnets(divisions)) if CLIENT_SUBNET6 else [] if CLIENT_SUBNET4.netmask == str("255.255.255.255"): raise ValueError("Invalid client subnet specification: %s" % CLIENT_SUBNET4) diff --git a/pinecrypt/server/templates/ipsec.conf b/pinecrypt/server/templates/ipsec.conf new file mode 100644 index 0000000..3f66b3b --- /dev/null +++ b/pinecrypt/server/templates/ipsec.conf @@ -0,0 +1,21 @@ +config setup + strictcrlpolicy=yes + charondebug="cfg 2" + +ca authority + auto=add + cacert=/server-secrets/ca_cert.pem + +conn s2c + auto=add + keyexchange=ikev2 + left={{ authority_namespace }} + leftsendcert=always + leftallowany=yes + leftcert=/server-secrets/self_cert.pem + leftsubnet={% for subnet in push %}{{ subnet }},{% endfor %} + leftupdown=/helpers/updown.py + right=%any + rightsourceip={{ slot4 }}{% if slot6 %},{{ slot6 }}{% endif %} + ike={{ strongswan_ike }}! + esp={{ strongswan_esp }}! diff --git a/pinecrypt/server/templates/openvpn.conf b/pinecrypt/server/templates/openvpn.conf new file mode 100644 index 0000000..52b70ed --- /dev/null +++ b/pinecrypt/server/templates/openvpn.conf @@ -0,0 +1,69 @@ +{% if proto == "udp" %} +dev tun0 +proto udp +port 1194 +management 127.0.0.1 7505 +setenv service openvpn-udp +{% else %} +dev tun1 +port-share 127.0.0.1 1443 +proto tcp-server +port 443 +socket-flags TCP_NODELAY +management 127.0.0.1 7506 +setenv service openvpn-tcp +{% endif %} + +# Client subnets +server {{ slot4.network_address }} {{ slot4.netmask }} +{% if slot6 %} +server-ipv6 {{ slot6 }} +{% endif %} +topology subnet + +# Bind to all interfaces +local 0.0.0.0 + +# Send keep alive packets, mainly for UDP +keepalive 60 120 + +opt-verify + +# Keypairs +key /server-secrets/self_key.pem +cert /server-secrets/self_cert.pem +ca /server-secrets/ca_cert.pem + +# Push subnets +{% if push %} +push "route-metric 10002 +{% endif %} +{% for subnet in push4 %} +push "route {{ subnet.network_address }} {{ subnet.netmask }}" +{% endfor %} +{% for subnet in push6 %} +push "route-ipv6 {{ subnet }}" +{% endfor %} + +# DH parameters file +dh none +#dhparam.pem + +# Control channel encryption parameterss +# For more info see: openvpn --show-tls +tls-version-min {{ openvpn_tls_version_min }} +tls-ciphersuites {{ openvpn_tls_ciphersuites }} # Used by TLS 1.3 +tls-cipher {{ openvpn_tls_cipher }} # Used by TLS 1.2 + +# Data channel encryption parameters +cipher {{ openvpn_cipher }} +auth {{ openvpn_auth }} + +# Just to sanity check ourselves +tls-cert-profile preferred + +script-security 2 +learn-address /helpers/learn-address.py +client-connect /helpers/client-connect.py +#verb 0 +