From d82b6e5d629d0235c4a93b7592ce37af9ef3e53f Mon Sep 17 00:00:00 2001 From: Richard Shaw Date: Wed, 30 Aug 2017 12:11:59 +0100 Subject: [PATCH] Added 0.0.0-0.2 changes --- Dockerfile | 15 + LICENSE | 201 +++++++ Makefile | 19 + README.md | 124 ++++ bin/clean.sh | 9 + bin/easyrsa | 1203 ++++++++++++++++++++++++++++++++++++++ bin/env.sh | 20 + bin/envs.sh | 8 + bin/packages.sh | 7 + bin/run.sh | 148 +++++ bin/zkshrun.sh | 3 + config.json | 64 ++ dcos_openvpn/__init__.py | 2 + dcos_openvpn/cert.py | 41 ++ dcos_openvpn/main.py | 56 ++ dcos_openvpn/web.py | 65 ++ local_universe_setup.sh | 37 ++ notes.md | 38 ++ requirements.txt | 4 + setup.cfg | 5 + setup.py | 99 ++++ tests.md | 26 + version | 1 + 23 files changed, 2195 insertions(+) create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100755 bin/clean.sh create mode 100755 bin/easyrsa create mode 100755 bin/env.sh create mode 100755 bin/envs.sh create mode 100755 bin/packages.sh create mode 100755 bin/run.sh create mode 100755 bin/zkshrun.sh create mode 100644 config.json create mode 100644 dcos_openvpn/__init__.py create mode 100644 dcos_openvpn/cert.py create mode 100644 dcos_openvpn/main.py create mode 100644 dcos_openvpn/web.py create mode 100755 local_universe_setup.sh create mode 100644 notes.md create mode 100644 requirements.txt create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 tests.md create mode 100644 version diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b98ce66 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM kylemanna/openvpn + +MAINTAINER Richard Shaw + +RUN apk -U add ca-certificates python python-dev py-setuptools alpine-sdk libffi libffi-dev openssl-dev + +COPY . /dcos + +WORKDIR /dcos +RUN ["/usr/bin/python", "setup.py", "install"] +RUN apk del alpine-sdk && \ + apk fix openssl && \ + rm -rf /tmp/* /var/tmp/* /var/cache/apk/* /var/cache/distfiles/* +EXPOSE 5000 1194/tcp 1194/udp +CMD ["bin/run.sh", "run_server"] \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8dada3e --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..fa919b9 --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ +all: env test packages + +clean: + bin/clean.sh + +env: + bin/env.sh + +test: + bin/test.sh + +packages: + bin/packages.sh + +push: + bin/push.sh + +setup-dns: + echo 'DOCKER_OPTS="-b=bridge0"' >> /etc/default/docker diff --git a/README.md b/README.md new file mode 100644 index 0000000..3f0f990 --- /dev/null +++ b/README.md @@ -0,0 +1,124 @@ +DC/OS OpenVPN +=============== + +OpenVPN server and REST management interface package for DC/OS + +Please note: This is a DC/OS Community package, which is not supported by Mesosphere Customer support. + +All issues and PRs should be raised on this repository. + +Features +-------------- + +1. Inherits OpenVPN in Docker from https://hub.docker.com/r/kylemanna/openvpn +1. Automatically configures PKI, certificates and runs OpenVPN without user interaction +1. Provides a REST interface for adding, revoking users and accessing their credentials for use with a suitable client +1. Exposes endpoints for OpenVPN - 1194/TCP & UDP, 5000/TCP REST +1. The REST interface uses Flask-BasicAuth and DC/OS secrets for basic (username & password) authentication +1. TLS is enabled by default on the REST interface - currently using the self signed openvpn certificate +1. The Zookeeper znode dcos-vpn has ACLs enabled using the secrets and protects server and client credentials +1. Synchronisation of assets between the container and Zookeeper in case the container is restarted +1. Clients revoked through the REST interface are correctly revoked from OpenVPN +1. Merged the previously separate openvpn server & openvpn-admin packages into one. The openvpn-admin package is no longer required. + +Task Installation +-------------- + +The simplest method of installation is simply to add it as a new Marathon task: + +1. You must add the secrets to DC/OS; ovpn_username & ovpn_password. Without these, the task will not launch +1. Clone this repo to your machine +1. Using the DC/OS cli add the task `dcos marathon app add config.json` +1. From the DC/OS UI > Services > openvpn +1. Check it's running, if failed, goto the most recent failed task > Logs > Stderr +1. From Services > openvpn > latest running task > Details +1. The first endpoint address is the REST interface, the second is the OpenVPN endpoint +1. Launch the first endpoint address and append /test to the end of the URL +1. Authenticate using the username and password added to secrets, now move onto managing users + +Local Universe Installation +-------------- + +The task can be also be added as a package to a local Universe repository + +https://github.com/mesosphere/universe +https://docs.mesosphere.com/1.9/administering-clusters/deploying-a-local-dcos-universe/ + +A simple helper script called local_universe_setup.sh is available for testing + +This requires a Docker registry to be available to publish the image to and for DC/OS to be able to access it. + +Managing Users +-------------- + +### Add User +1. Authenticate and post to the REST endpoint, the new user's credentials will be output to the POST BODY. Add these to a suitable OpenVPN client. +1. The new assets will be copied to Zookeeper for persistence in case the task is killed, and will be copied back to the container on startup. +``` +curl -k -u admin:password -X POST -d "name=richard" https://192.168.33.12:31074/client +``` + +### Revoke User +1. Calls easyrsa revokeclient to correctly revoke the client, removes all assets locally and from Zookeeper +``` +curl -k -u admin:password -X DELETE https://192.168.33.12:19129/client/richard +``` + +How it works +-------------- + +Inherits the OpenVPN image from https://hub.docker.com/r/kylemanna/openvpn with a shell script to auto-configure OpenVPN without prompts, execute +the OpenVPN daemon and launch the REST interface. + +bin/run.sh, dcos_openvpn/web.py & dcos_openvpn/cert.py provide the main functionality. + +Python Flask provides the web microframework. + +zk-shell https://github.com/rgs1/zk_shell is used to interact with Zookeeper. In order to enable ACLs and use it programmatically, required creative +use of their stdin option. This is wrapped in the run_command function in run.sh. + +zkshrun.sh is a little standalone helper script that provides run_command to the cert.py. + +A modified version of easyrsa is shipped which removes user prompts. + +### Startup order +1. run.sh checks for existing assets in Zookeeper and copies them to the container if they exist, otherwise initpki and genconfig are run +1. Launchs the OpenVPN daemon in daemon mode, passing --daemon +1. Starts the Python REST interface + + +Troubleshooting +-------------- + +1. Review stdout and stderr from the task's logs under the DC/OS UI > Service > openvpn > running task > logs +2. If the task is running on DC/OS, get a shell on the running container to investigate further: +``` +docker ps +docker exec -it /bin/bash +``` +/dcos/bin/runs.sh & /dcos/dcos_openvpn/web.py are the two main files to investigate. + +The container can also be launched local onto a Docker daemon. + +`run.sh reset` & `run.sh reset_container` are useful for testing, resetting both the Zookeeper znode & container and just the container respectively. + +Modifying run.sh run_server as follows it useful for testing changes to the REST interface + +``` +function run_server { + source /dcos/bin/envs.sh + check_status + setup + #ovpn_run --daemon + ovpn_run + #/usr/bin/python -m dcos_openvpn.main +} +``` + +Todo +-------------- +1. The patch for zk-shell https://github.com/rgs1/zk_shell/pull/82 as managed in run.bash around line 100 needs removing when zk-shell is fixed +1. Update the /status endpoint for ovpn_status output and tie into a healthcheck +1. run.sh usage and tidying +1. Update for DC/OS 1.10 and file based secrets +1. Either extend zk-shell to add auth to its params or replace with Kazoo code diff --git a/bin/clean.sh b/bin/clean.sh new file mode 100755 index 0000000..ad49665 --- /dev/null +++ b/bin/clean.sh @@ -0,0 +1,9 @@ +#!/bin/bash -e + +set -o errexit -o nounset -o pipefail + +BASEDIR=`dirname $0`/.. + +rm -rf $BASEDIR/.tox $BASEDIR/env $BASEDIR/dist $BASEDIR/build +echo "Deleted virtualenv and test artifacts." + diff --git a/bin/easyrsa b/bin/easyrsa new file mode 100755 index 0000000..27de14a --- /dev/null +++ b/bin/easyrsa @@ -0,0 +1,1203 @@ +#!/bin/sh + +# Easy-RSA 3 -- A Shell-based CA Utility +# +# Copyright (C) 2013 by the Open-Source OpenVPN development community. +# A full list of contributors can be found in the ChangeLog. +# +# This code released under version 2 of the GNU GPL; see COPYING and the +# Licensing/ directory of this project for full licensing details. + +# Help/usage output to stdout +usage() { + # command help: + print " +Easy-RSA 3 usage and overview + +USAGE: easyrsa [options] COMMAND [command-options] + +A list of commands is shown below. To get detailed usage and help for a +command, run: + ./easyrsa help COMMAND + +For a listing of options that can be supplied before the command, use: + ./easyrsa help options + +Here is the list of commands available with a short syntax reminder. Use the +'help' command above to get full usage details. + + init-pki + build-ca [ cmd-opts ] + gen-dh + gen-req [ cmd-opts ] + sign-req + build-client-full [ cmd-opts ] + build-server-full [ cmd-opts ] + revoke + gen-crl + update-db + show-req [ cmd-opts ] + show-cert [ cmd-opts ] + import-req + export-p7 [ cmd-opts ] + export-p12 [ cmd-opts ] + set-rsa-pass [ cmd-opts ] + set-ec-pass [ cmd-opts ] +" + + # collect/show dir status: + local err_source="Not defined: vars autodetect failed and no value provided" + local work_dir="${EASYRSA:-$err_source}" + local pki_dir="${EASYRSA_PKI:-$err_source}" + print "\ +DIRECTORY STATUS (commands would take effect on these locations) + EASYRSA: $work_dir + PKI: $pki_dir +" +} # => usage() + +# Detailed command help +# When called with no args, calls usage(), otherwise shows help for a command +cmd_help() { + local text opts + case "$1" in + init-pki|clean-all) text=" + init-pki [ cmd-opts ] + Removes & re-initializes the PKI dir for a clean PKI" ;; + build-ca) text=" + build-ca [ cmd-opts ] + Creates a new CA" + opts=" + nopass - do not encrypt the CA key (default is encrypted) + subca - create a sub-CA keypair and request (default is a root CA)" ;; + gen-dh) text=" + gen-dh + Generates DH (Diffie-Helllman) parameters" ;; + gen-req) text=" + gen-req [ cmd-opts ] + Generate a standalone keypair and request (CSR) + + This request is suitable for sending to a remote CA for signing." + opts=" + nopass - do not encrypt the private key (default is encrypted)" ;; + sign|sign-req) text=" + sign-req + Sign a certificate request of the defined type. must be a known + type such as 'client', 'server', or 'ca' (or a user-added type.) + + This request file must exist in the reqs/ dir and have a .req file + extension. See import-req below for importing reqs from other sources." ;; + build|build-client-full|build-server-full) text=" + build-client-full [ cmd-opts ] + build-server-full [ cmd-opts ] + Generate a keypair and sign locally for a client or server + + This mode uses the as the X509 CN." + opts=" + nopass - do not encrypt the private key (default is encrypted)" ;; + revoke) text=" + revoke + Revoke a certificate specified by the filename_base" ;; + gen-crl) text=" + gen-crl + Generate a CRL" ;; + update-db) text=" + update-db + Update the index.txt database + + This command will use the system time to update the status of issued + certificates." ;; + show-req|show-cert) text=" + show-req [ cmd-opts ] + show-cert [ cmd-opts ] + Shows details of the req or cert referenced by filename_base + + Human-readable output is shown, including any requested cert options when + showing a request." + opts=" + full - show full req/cert info, including pubkey/sig data" ;; + import-req) text=" + import-req + Import a certificate request from a file + + This will copy the specified file into the reqs/ dir in + preparation for signing. + The is the filename base to create. + + Example usage: + import-req /some/where/bob_request.req bob" ;; + export-p12) text=" + export-p12 [ cmd-opts ] + Export a PKCS#12 file with the keypair specified by " + opts=" + noca - do not include the ca.crt file in the PKCS12 output + nokey - do not include the private key in the PKCS12 output" ;; + export-p7) text=" + export-p7 [ cmd-opts ] + Export a PKCS#7 file with the pubkey specified by " + opts=" + noca - do not include the ca.crt file in the PKCS7 output" ;; + set-rsa-pass|set-ec-pass) text=" + set-rsa-pass [ cmd-opts ] + set-ec-pass [ cmd-opts ] + Set a new passphrase on an RSA or EC key for the listed ." + opts=" + nopass - use no password and leave the key unencrypted + file - (advanced) treat the file as a raw path, not a short-name" ;; + altname|subjectaltname|san) text=" + --subject-alt-name=SAN_FORMAT_STRING + This global option adds a subjectAltName to the request or issued + certificate. It MUST be in a valid format accepted by openssl or + req/cert generation will fail. Note that including multiple such names + requires them to be comma-separated; further invocations of this + option will REPLACE the value. + + Examples of the SAN_FORMAT_STRING shown below: + DNS:alternate.example.net + DNS:primary.example.net,DNS:alternate.example.net + IP:203.0.113.29 + email:alternate@example.net" ;; + options) + opt_usage ;; + "") + usage ;; + *) text=" + Unknown command: '$1' (try without commands for a list of commands)" ;; + esac + + # display the help text + print "$text" + [ -n "$opts" ] && print " + cmd-opts is an optional set of command options from this list: +$opts" +} # => cmd_help() + +# Options usage +opt_usage() { + print " +Easy-RSA Global Option Flags + +The following options may be provided before the command. Options specified +at runtime override env-vars and any 'vars' file in use. Unless noted, +non-empty values to options are mandatory. + +General options: + +--batch : set automatic (no-prompts when possible) mode +--pki-dir=DIR : declares the PKI directory +--vars=FILE : define a specific 'vars' file to use for Easy-RSA config + +Certificate & Request options: (these impact cert/req field values) + +--days=# : sets the signing validity to the specified number of days +--digest=ALG : digest to use in the requests & certificates +--dn-mode=MODE : DN mode to use (cn_only or org) +--keysize=# : size in bits of keypair to generate +--req-cn=NAME : default CN to use +--subca-len=# : path length of signed sub-CA certs; must be >= 0 if used +--subject-alt-name : Add a subjectAltName. For more info and syntax, see: + ./easyrsa help altname +--use-algo=ALG : crypto alg to use: choose rsa (default) or ec +--curve=NAME : for elliptic curve, sets the named curve to use + +Organizational DN options: (only used with the 'org' DN mode) + (values may be blank for org DN options) + +--req-c=CC : country code (2-letters) +--req-st=NAME : State/Province +--req-city=NAME : City/Locality +--req-org=NAME : Organization +--req-email=NAME : Email addresses +--req-ou=NAME : Organizational Unit + +Deprecated features: + +--ns-cert=YESNO : yes or no to including deprecated NS extensions +--ns-comment=COMMENT : NS comment to include (value may be blank) +" +} # => opt_usage() + +# Wrapper around printf - clobber print since it's not POSIX anyway +print() { printf "%s\n" "$*"; } + +# Exit fatally with a message to stderr +# present even with EASYRSA_BATCH as these are fatal problems +die() { + print " +Easy-RSA error: + +$1" 1>&2 + clean_temp + exit ${2:-1} +} # => die() + +# non-fatal warning output +warn() { + [ ! $EASYRSA_BATCH ] && \ + print " +$1" 1>&2 +} # => warn() + +# informational notices to stdout +notice() { + [ ! $EASYRSA_BATCH ] && \ + print " +$1" +} # => notice() + +# yes/no case-insensitive match (operates on stdin pipe) +# Returns 0 when input contains yes, 1 for no, 2 for no match +# If both strings are present, returns 1; first matching line returns. +awk_yesno() { + local awkscript=' +BEGIN {IGNORECASE=1; r=2} +{ if(match($0,"no")) {r=1; exit} + if(match($0,"yes")) {r=0; exit} +} END {exit r}' + awk "$awkscript" +} # => awk_yesno() + +# intent confirmation helper func +# returns without prompting in EASYRSA_BATCH +confirm() { + [ $EASYRSA_BATCH ] && return + local prompt="$1" value="$2" msg="$3" input + print " +$msg + +Type the word '$value' to continue, or any other input to abort." + printf %s " $prompt" + read input + [ "$input" = "$value" ] && return + notice "Aborting without confirmation." + exit 9 +} # => confirm() + +# remove temp files +clean_temp() { + for f in "$EASYRSA_TEMP_FILE" + do [ -f "$f" ] && rm "$f" 2>/dev/null + done +} # => clean_temp() + +vars_source_check() { + # Check for defined EASYRSA_PKI + [ -n "$EASYRSA_PKI" ] || die "\ +EASYRSA_PKI env-var undefined" + + # Verify EASYRSA_OPENSSL command gives expected output + if [ -z "$EASYRSA_SSL_OK" ]; then + local val="$("$EASYRSA_OPENSSL" version)" + [ "${val%% *}" = "OpenSSL" ] || die "\ +Missing or invalid OpenSSL +Expected to find openssl command at: $EASYRSA_OPENSSL" + fi + EASYRSA_SSL_OK=1 + + # Verify EASYRSA_SSL_CONF file exists + [ -f "$EASYRSA_SSL_CONF" ] || die "\ +The OpenSSL config file cannot be found. +Expected location: $EASYRSA_SSL_CONF" +} # => vars_source_check() + +# Verify supplied curve exists and generate curve file if needed +verify_curve() { + if ! "$EASYRSA_OPENSSL" ecparam -name "$EASYRSA_CURVE" > /dev/null; then + die "\ +Curve $EASYRSA_CURVE not found. Run openssl ecparam -list_curves to show a +list of supported curves." + fi + + # Check that the ecparams dir exists + [ -d "$EASYRSA_EC_DIR" ] || mkdir "$EASYRSA_EC_DIR" || die "\ +Failed creating ecparams dir (permissions?) at: +$EASYRSA_EC_DIR" + + # Check that the required ecparams file exists + local out="$EASYRSA_EC_DIR/${EASYRSA_CURVE}.pem" + [ -f "$out" ] && return 0 + "$EASYRSA_OPENSSL" ecparam -name "$EASYRSA_CURVE" -out "$out" || die "\ +Failed to generate ecparam file (permissions?) when writing to: +$out" + + # Explicitly return success for caller + return 0 +} + +# Basic sanity-check of PKI init and complain if missing +verify_pki_init() { + local help_note="Run easyrsa without commands for usage and command help." + + # check that the pki dir exists + vars_source_check + [ -d "$EASYRSA_PKI" ] || die "\ +EASYRSA_PKI does not exist (perhaps you need to run init-pki)? +Expected to find the EASYRSA_PKI at: $EASYRSA_PKI +$help_note" + + # verify expected dirs present: + for i in private reqs; do + [ -d "$EASYRSA_PKI/$i" ] || die "\ +Missing expected directory: $i (perhaps you need to run init-pki?) +$help_note" + done +} # => verify_pki_init() + +# Verify core CA files present +verify_ca_init() { + local help_note="Run without commands for usage and command help." + + # First check the PKI has been initialized + verify_pki_init + + # verify expected files present: + for i in serial index.txt ca.crt private/ca.key; do + if [ ! -f "$EASYRSA_PKI/$i" ]; then + [ "$1" = "test" ] && return 1 + die "\ +Missing expected CA file: $i (perhaps you need to run build-ca?) +$help_note" + fi + done + + # When operating in 'test' mode, return success. + # test callers don't care about CA-specific dir structure + [ "$1" = "test" ] && return 0 + + # verify expected CA-specific dirs: + for i in issued certs_by_serial; do + [ -d "$EASYRSA_PKI/$i" ] || die "\ +Missing expected CA dir: $i (perhaps you need to run build-ca?) +$help_note" + done + + # explicitly return success for callers + return 0 + +} # => verify_ca_init() + +# init-pki backend: +init_pki() { + vars_source_check + + # If EASYRSA_PKI exists, confirm before we rm -rf (skiped with EASYRSA_BATCH) + if [ -e "$EASYRSA_PKI" ]; then + confirm "Confirm removal: " "yes" " +WARNING!!! + +You are about to remove the EASYRSA_PKI at: $EASYRSA_PKI +and initialize a fresh PKI here." + # now remove it: + rm -rf "$EASYRSA_PKI" || die "Removal of PKI dir failed. Check/correct errors above" + fi + + # new dirs: + for i in private reqs; do + mkdir -p "$EASYRSA_PKI/$i" || die "Failed to create PKI file structure (permissions?)" + done + + notice "\ +init-pki complete; you may now create a CA or requests. +Your newly created PKI dir is: $EASYRSA_PKI +" + return 0 +} # => init_pki() + +# build-ca backend: +build_ca() { + local opts= sub_ca= + opts="-passout env:CA_PASS" + while [ -n "$1" ]; do + case "$1" in + nopass) opts="$opts -nodes" ;; + subca) sub_ca=1 ;; + *) warn "Ignoring unknown command option: '$1'" ;; + esac + shift + done + + verify_pki_init + [ "$EASYRSA_ALGO" = "ec" ] && verify_curve + + # setup for the simpler sub-CA situation and overwrite with root-CA if needed: + local out_file="$EASYRSA_PKI/reqs/ca.req" + local out_key="$EASYRSA_PKI/private/ca.key" + if [ ! $sub_ca ]; then + out_file="$EASYRSA_PKI/ca.crt" + opts="$opts -x509 -days $EASYRSA_CA_EXPIRE" + fi + + # Test for existing CA, and complain if already present + if verify_ca_init test; then + die "\ +Unable to create a CA as you already seem to have one set up. +If you intended to start a new CA, run init-pki first." + fi + # If a private key exists here, a sub-ca was created but not signed. + # Notify the user and require a signed ca.crt or a init-pki: + [ -f "$out_key" ] && \ + die "\ +A CA private key exists but no ca.crt is found in your PKI dir of: +$EASYRSA_PKI +Refusing to create a new CA keypair as this operation would overwrite your +current CA keypair. If you intended to start a new CA, run init-pki first." + + # create necessary files and dirs: + local err_file="Unable to create necessary PKI files (permissions?)" + for i in issued certs_by_serial; do + mkdir -p "$EASYRSA_PKI/$i" || die "$err_file" + done + printf "" > "$EASYRSA_PKI/index.txt" || die "$err_file" + print "01" > "$EASYRSA_PKI/serial" || die "$err_file" + + # Default CN only when not in global EASYRSA_BATCH mode: + [ $EASYRSA_BATCH ] && opts="$opts -batch" || export EASYRSA_REQ_CN="Easy-RSA CA" + # create the CA keypair: + "$EASYRSA_OPENSSL" req -new -newkey $EASYRSA_ALGO:"$EASYRSA_ALGO_PARAMS" \ + -config "$EASYRSA_SSL_CONF" -keyout "$out_key" -out "$out_file" $opts || \ + die "Failed to build the CA" + + # Success messages + if [ $sub_ca ]; then + notice "\ +NOTE: Your sub-CA request is at $out_file +and now must be sent to you parent CA for signing. Place your resulting cert +at $EASYRSA_PKI/ca.crt prior to signing operations. +" + else notice "\ +CA creation complete and you may now import and sign cert requests. +Your new CA certificate file for publishing is at: +$out_file +" + fi + return 0 +} # => build_ca() + +# gen-dh backend: +gen_dh() { + verify_pki_init + + local out_file="$EASYRSA_PKI/dh.pem" + "$EASYRSA_OPENSSL" dhparam -out "$out_file" $EASYRSA_KEY_SIZE || \ + die "Failed to build DH params" + notice "\ +DH parameters of size $EASYRSA_KEY_SIZE created at $out_file +" + return 0 +} # => gen_dh() + +# gen-req backend: +gen_req() { + # pull filename base and use as default interactive CommonName: + [ -n "$1" ] || die "\ +Error: gen-req must have a file base as the first argument. +Run easyrsa without commands for usage and commands." + local key_out="$EASYRSA_PKI/private/$1.key" + local req_out="$EASYRSA_PKI/reqs/$1.req" + [ ! $EASYRSA_BATCH ] && EASYRSA_REQ_CN="$1" + shift + + # function opts support + local opts= + opts="-passin env:CA_PASS" + while [ -n "$1" ]; do + case "$1" in + nopass) opts="$opts -nodes" ;; + # batch flag supports internal callers needing silent operation + batch) local EASYRSA_BATCH=1 ;; + *) warn "Ignoring unknown command option: '$1'" ;; + esac + shift + done + + verify_pki_init + [ "$EASYRSA_ALGO" = "ec" ] && verify_curve + + # don't wipe out an existing private key without confirmation + [ -f "$key_out" ] && confirm "Confirm key overwrite: " "yes" "\ + +WARNING!!! + +An existing private key was found at $key_out +Continuing with key generation will replace this key." + + # When EASYRSA_EXTRA_EXTS is defined, append it to openssl's [req] section: + if [ -n "$EASYRSA_EXTRA_EXTS" ]; then + # Setup & insert the extra ext data keyed by a magic line + EASYRSA_EXTRA_EXTS=" +req_extensions = req_extra +[ req_extra ] +$EASYRSA_EXTRA_EXTS" + local awkscript=' +{if ( match($0, "^#%EXTRA_EXTS%") ) + { while ( getline<"/dev/stdin" ) {print} next } + {print} +}' + print "$EASYRSA_EXTRA_EXTS" | \ + awk "$awkscript" "$EASYRSA_SSL_CONF" \ + > "$EASYRSA_TEMP_FILE" \ + || die "Copying SSL config to temp file failed" + # Use this new SSL config for the rest of this function + local EASYRSA_SSL_CONF="$EASYRSA_TEMP_FILE" + fi + + # generate request + [ $EASYRSA_BATCH ] && opts="$opts -batch" + "$EASYRSA_OPENSSL" req -new -newkey $EASYRSA_ALGO:"$EASYRSA_ALGO_PARAMS" \ + -config "$EASYRSA_SSL_CONF" -keyout "$key_out" -out "$req_out" $opts \ + || die "Failed to generate request" + notice "\ +Keypair and certificate request completed. Your files are: +req: $req_out +key: $key_out +" + clean_temp + return 0 +} # => gen_req() + +# common signing backend +sign_req() { + local crt_type="$1" opts= + local req_in="$EASYRSA_PKI/reqs/$2.req" + local crt_out="$EASYRSA_PKI/issued/$2.crt" + + opts="-passin env:CA_PASS" + + # Support batch by internal caller: + [ "$3" = "batch" ] && local EASYRSA_BATCH=1 + + verify_ca_init + + # Check argument sanity: + [ -n "$2" ] || die "\ +Incorrect number of arguments provided to sign-req: +expected 2, got $# (see command help for usage)" + + # Cert type must exist under the EASYRSA_EXT_DIR + [ -r "$EASYRSA_EXT_DIR/$crt_type" ] || die "\ +Unknown cert type '$crt_type'" + + # Request file must exist + [ -f "$req_in" ] || die "\ +No request found for the input: '$2' +Expected to find the request at: $req_in" + + # Confirm input is a cert req + verify_file req "$req_in" || die "\ +The certificate request file is not in a valid X509 request format. +Offending file: $req_in" + + # Display the request subject in an easy-to-read format + # Confirm the user wishes to sign this request + confirm "Confirm request details: " "yes" " +You are about to sign the following certificate. +Please check over the details shown below for accuracy. Note that this request +has not been cryptographically verified. Please be sure it came from a trusted +source or that you have verified the request checksum with the sender. + +Request subject, to be signed as a $crt_type certificate for $EASYRSA_CERT_EXPIRE days: + +$(display_dn req "$req_in") +" # => confirm end + + # Generate the extensions file for this cert: + { + # Append first any COMMON file (if present) then the cert-type extensions + cat "$EASYRSA_EXT_DIR/COMMON" + cat "$EASYRSA_EXT_DIR/$crt_type" + + # Support a dynamic CA path length when present: + [ "$crt_type" = "ca" ] && [ -n "$EASYRSA_SUBCA_LEN" ] && \ + print "basicConstraints = CA:TRUE, pathlen:$EASYRSA_SUBCA_LEN" + + # Deprecated Netscape extension support, if enabled + if print "$EASYRSA_NS_SUPPORT" | awk_yesno; then + [ -n "$EASYRSA_NS_COMMENT" ] && \ + print "nsComment = \"$EASYRSA_NS_COMMENT\"" + case "$crt_type" in + server) print "nsCertType = server" ;; + client) print "nsCertType = client" ;; + ca) print "nsCertType = sslCA" ;; + esac + fi + + # Add any advanced extensions supplied by env-var: + [ -n "$EASYRSA_EXTRA_EXTS" ] && print "$EASYRSA_EXTRA_EXTS" + + : # needed to keep die from inherting the above test + } > "$EASYRSA_TEMP_FILE" || die "\ +Failed to create temp extension file (bad permissions?) at: +$EASYRSA_TEMP_FILE" + + # sign request + "$EASYRSA_OPENSSL" ca -in "$req_in" -out "$crt_out" -config "$EASYRSA_SSL_CONF" \ + -extfile "$EASYRSA_TEMP_FILE" -days $EASYRSA_CERT_EXPIRE -batch $opts \ + || die "signing failed (openssl output above may have more detail)" + notice "\ +Certificate created at: $crt_out +" + clean_temp + return 0 +} # => sign_req() + +# common build backend +# used to generate+sign in 1 step +build_full() { + verify_ca_init + + # pull filename base: + [ -n "$2" ] || die "\ +Error: didn't find a file base name as the first argument. +Run easyrsa without commands for usage and commands." + local crt_type="$1" name="$2" + local req_out="$EASYRSA_PKI/reqs/$2.req" + local key_out="$EASYRSA_PKI/private/$2.key" + local crt_out="$EASYRSA_PKI/issued/$2.crt" + shift 2 + + # function opts support + local req_opts= + while [ -n "$1" ]; do + case "$1" in + nopass) req_opts="$req_opts nopass" ;; + *) warn "Ignoring unknown command option: '$1'" ;; + esac + shift + done + + # abort on existing req/key/crt files + local err_exists="\ +file already exists. Aborting build to avoid overwriting this file. +If you wish to continue, please use a different name or remove the file. +Matching file found at: " + [ -f "$req_out" ] && die "Request $err_exists $req_out" + [ -f "$key_out" ] && die "Key $err_exists $key_out" + [ -f "$crt_out" ] && die "Certificate $err_exists $crt_out" + + # create request + EASYRSA_REQ_CN="$name" + gen_req "$name" batch $req_opts + + # Sign it + sign_req "$crt_type" "$name" batch + +} # => build_full() + +# revoke backend +revoke() { + verify_ca_init + + # pull filename base: + [ -n "$1" ] || die "\ +Error: didn't find a file base name as the first argument. +Run easyrsa without commands for usage and command help." + local crt_in="$EASYRSA_PKI/issued/$1.crt" + + verify_file x509 "$crt_in" || die "\ +Unable to revoke as the input file is not a valid certificate. Unexpected +input in file: $crt_in" + + # confirm operation by displaying DN: + #confirm "Continue with revocation: " "yes" " +#Please confirm you wish to revoke the certificate with the following subject: + +$(display_dn x509 "$crt_in") +#" # => confirm end + + # referenced cert must exist: + [ -f "$crt_in" ] || die "\ +Unable to revoke as no certificate was found. Certificate was expected +at: $crt_in" + + "$EASYRSA_OPENSSL" ca -revoke "$crt_in" -config "$EASYRSA_SSL_CONF" || die "\ +Failed to revoke certificate: revocation command failed." + + notice "\ +IMPORTANT!!! + +Revocation was successful. You must run gen-crl and upload a CRL to your +infrastructure in order to prevent the revoked cert from being accepted. +" # => notice end + return 0 +} #= revoke() + +# gen-crl backend +gen_crl() { + verify_ca_init + + local out_file="$EASYRSA_PKI/crl.pem" + "$EASYRSA_OPENSSL" ca -gencrl -out "$out_file" -config "$EASYRSA_SSL_CONF" || die "\ +CRL Generation failed. +" + + notice "\ +An updated CRL has been created. +CRL file: $out_file +" + return 0 +} # => gen_crl() + +# import-req backend +import_req() { + verify_pki_init + + # pull passed paths + local in_req="$1" short_name="$2" + local out_req="$EASYRSA_PKI/reqs/$2.req" + + [ -n "$short_name" ] || die "\ +Unable to import: incorrect command syntax. +Run easyrsa without commands for usage and command help." + + verify_file req "$in_req" || die "\ +The input file does not appear to be a certificate request. Aborting import. +Offending file: $in_req" + + # destination must not exist + [ -f "$out_req" ] && die "\ +Unable to import the request as the destination file already exists. +Please choose a different name for your imported request file. +Existing file at: $out_req" + + # now import it + cp "$in_req" "$out_req" + + notice "\ +The request has been successfully imported with a short name of: $short_name +You may now use this name to perform signing operations on this request. +" + return 0 +} # => import_req() + +# export pkcs#12 or pkcs#7 +export_pkcs() { + local pkcs_type="$1" + shift + + [ -n "$1" ] || die "\ +Unable to export p12: incorrect command syntax. +Run easyrsa without commands for usage and command help." + + local short_name="$1" + local crt_in="$EASYRSA_PKI/issued/$1.crt" + local key_in="$EASYRSA_PKI/private/$1.key" + local crt_ca="$EASYRSA_PKI/ca.crt" + shift + + verify_pki_init + + # opts support + local want_ca=1 + local want_key=1 + while [ -n "$1" ]; do + case "$1" in + noca) want_ca= ;; + nokey) want_key= ;; + *) warn "Ignoring unknown command option: '$1'" ;; + esac + shift + done + + local pkcs_opts= + if [ $want_ca ]; then + verify_file x509 "$crt_ca" || die "\ +Unable to include CA cert in the $pkcs_type output (missing file, or use noca option.) +Missing file expected at: $crt_ca" + pkcs_opts="$pkcs_opts -certfile $crt_ca" + fi + + # input files must exist + verify_file x509 "$crt_in" || die "\ +Unable to export $pkcs_type for short name '$short_name' without the certificate. +Missing cert expected at: $crt_in" + + case "$pkcs_type" in + p12) + local pkcs_out="$EASYRSA_PKI/private/$short_name.p12" + + if [ $want_key ]; then + [ -f "$key_in" ] || die "\ +Unable to export p12 for short name '$short_name' without the key +(if you want a p12 without the private key, use nokey option.) +Missing key expected at: $key_in" + else + pkcs_opts="$pkcs_opts -nokeys" + fi + + # export the p12: + "$EASYRSA_OPENSSL" pkcs12 -in "$crt_in" -inkey "$key_in" -export \ + -out "$pkcs_out" $pkcs_opts || die "\ +Export of p12 failed: see above for related openssl errors." + ;; + p7) + local pkcs_out="$EASYRSA_PKI/issued/$short_name.p7b" + + # export the p7: + "$EASYRSA_OPENSSL" crl2pkcs7 -nocrl -certfile "$crt_in" \ + -out "$pkcs_out" $pkcs_opts || die "\ +Export of p7 failed: see above for related openssl errors." + ;; +esac + + notice "\ +Successful export of $pkcs_type file. Your exported file is at the following +location: $pkcs_out +" + return 0 +} # => export_pkcs() + +# set-pass backend +set_pass() { + verify_pki_init + + # key type, supplied internally from frontend command call (rsa/ec) + local key_type="$1" + + # values supplied by the user: + local raw_file="$2" + local file="$EASYRSA_PKI/private/$raw_file.key" + [ -n "$raw_file" ] || die "\ +Missing argument to 'set-$key_type-pass' command: no name/file supplied. +See help output for usage details." + + # parse command options + shift 2 + local crypto="-des3" + while [ -n "$1" ]; do + case "$1" in + nopass) crypto= ;; + file) file="$raw_file" ;; + *) warn "Ignoring unknown command option: '$1'" ;; + esac + shift + done + + [ -f "$file" ] || die "\ +Missing private key: expected to find the private key component at: +$file" + + notice "\ +If the key is currently encrypted you must supply the decryption passphrase. +${crypto:+You will then enter a new PEM passphrase for this key.$NL}" + + "$EASYRSA_OPENSSL" $key_type -in "$file" -out "$file" $crypto || die "\ +Failed to change the private key passphrase. See above for possible openssl +error messages." + + notice "Key passphrase successfully changed" + +} # => set_pass() + +# update-db backend +update_db() { + verify_ca_init + + "$EASYRSA_OPENSSL" ca -updatedb -config "$EASYRSA_SSL_CONF" || die "\ +Failed to perform update-db: see above for related openssl errors." + return 0 +} # => update_db() + +# display cert DN info on a req/X509, passed by full pathname +display_dn() { + local format="$1" path="$2" + print "$("$EASYRSA_OPENSSL" $format -in "$path" -noout -subject -nameopt multiline)" +} # => display_dn() + +# verify a file seems to be a valid req/X509 +verify_file() { + local format="$1" path="$2" + "$EASYRSA_OPENSSL" $format -in "$path" -noout 2>/dev/null || return 1 + return 0 +} # => verify_x509() + +# show-* command backend +# Prints req/cert details in a readable format +show() { + local type="$1" name="$2" in_file format + [ -n "$name" ] || die "\ +Missing expected filename_base argument. +Run easyrsa without commands for usage help." + shift 2 + + # opts support + local opts="-${type}opt no_pubkey,no_sigdump" + while [ -n "$1" ]; do + case "$1" in + full) opts= ;; + *) warn "Ignoring unknown command option: '$1'" ;; + esac + shift + done + + # Determine cert/req type + if [ "$type" = "cert" ]; then + verify_ca_init + in_file="$EASYRSA_PKI/issued/${name}.crt" + format="x509" + else + verify_pki_init + in_file="$EASYRSA_PKI/reqs/${name}.req" + format="req" + fi + + # Verify file exists and is of the correct type + [ -f "$in_file" ] || die "\ +No such $type file with a basename of '$name' is present. +Expected to find this file at: +$in_file" + verify_file $format "$in_file" || die "\ +This file is not a valid $type file: +$in_file" + + notice "\ +Showing $type details for '$name'. +This file is stored at: +$in_file +" + "$EASYRSA_OPENSSL" $format -in "$in_file" -noout -text\ + -nameopt multiline $opts || die "\ +OpenSSL failure to process the input" +} # => show() + +# vars setup +# Here sourcing of 'vars' if present occurs. If not present, defaults are used +# to support running without a sourced config format +vars_setup() { + # Try to locate a 'vars' file in order of location preference. + # If one is found, source it + local vars= + + # set up program path + local prog_vars="${0%/*}/vars" + + # command-line path: + if [ -f "$EASYRSA_VARS_FILE" ]; then + vars="$EASYRSA_VARS_FILE" + # EASYRSA_PKI, if defined: + elif [ -n "$EASYRSA_PKI" ] && [ -f "$EASYRSA_PKI/vars" ]; then + vars="$EASYRSA_PKI/vars" + # EASYRSA, if defined: + elif [ -n "$EASYRSA" ] && [ -f "$EASYRSA/vars" ]; then + vars="$EASYRSA/vars" + # program location: + elif [ -f "$prog_vars" ]; then + vars="$prog_vars" + fi + + # If a vars file was located, source it + # If $EASYRSA_NO_VARS is defined (not blank) this is skipped + if [ -z "$EASYRSA_NO_VARS" ] && [ -n "$vars" ]; then + EASYRSA_CALLER=1 . "$vars" + notice "\ +Note: using Easy-RSA configuration from: $vars" + fi + + # Set defaults, preferring existing env-vars if present + set_var EASYRSA "$PWD" + set_var EASYRSA_OPENSSL openssl + set_var EASYRSA_PKI "$EASYRSA/pki" + set_var EASYRSA_DN cn_only + set_var EASYRSA_REQ_COUNTRY "US" + set_var EASYRSA_REQ_PROVINCE "California" + set_var EASYRSA_REQ_CITY "San Francisco" + set_var EASYRSA_REQ_ORG "Copyleft Certificate Co" + set_var EASYRSA_REQ_EMAIL me@example.net + set_var EASYRSA_REQ_OU "My Organizational Unit" + set_var EASYRSA_ALGO rsa + set_var EASYRSA_KEY_SIZE 2048 + set_var EASYRSA_CURVE secp384r1 + set_var EASYRSA_EC_DIR "$EASYRSA_PKI/ecparams" + set_var EASYRSA_CA_EXPIRE 3650 + set_var EASYRSA_CERT_EXPIRE 3650 + set_var EASYRSA_CRL_DAYS 180 + set_var EASYRSA_NS_SUPPORT no + set_var EASYRSA_NS_COMMENT "Easy-RSA Generated Certificate" + set_var EASYRSA_TEMP_FILE "$EASYRSA_PKI/extensions.temp" + set_var EASYRSA_REQ_CN ChangeMe + set_var EASYRSA_DIGEST sha256 + + # Detect openssl config, preferring EASYRSA_PKI over EASYRSA + if [ -f "$EASYRSA_PKI/openssl-1.0.cnf" ]; then + set_var EASYRSA_SSL_CONF "$EASYRSA_PKI/openssl-1.0.cnf" + else set_var EASYRSA_SSL_CONF "$EASYRSA/openssl-1.0.cnf" + fi + + # Same as above for the x509-types extensions dir + if [ -d "$EASYRSA_PKI/x509-types" ]; then + set_var EASYRSA_EXT_DIR "$EASYRSA_PKI/x509-types" + else set_var EASYRSA_EXT_DIR "$EASYRSA/x509-types" + fi + + # EASYRSA_ALGO_PARAMS must be set depending on selected algo + if [ "ec" = "$EASYRSA_ALGO" ]; then + EASYRSA_ALGO_PARAMS="$EASYRSA_EC_DIR/${EASYRSA_CURVE}.pem" + elif [ "rsa" = "$EASYRSA_ALGO" ]; then + EASYRSA_ALGO_PARAMS="${EASYRSA_KEY_SIZE}" + else + die "Alg '$EASYRSA_ALGO' is invalid: must be 'rsa' or 'ec'" + fi + + # Setting OPENSSL_CONF prevents bogus warnings (especially useful on win32) + export OPENSSL_CONF="$EASYRSA_SSL_CONF" +} # vars_setup() + +# variable assignment by indirection when undefined; merely exports +# the variable when it is already defined (even if currently null) +# Sets $1 as the value contained in $2 and exports (may be blank) +set_var() { + local var=$1 + shift + local value="$*" + eval "export $var=\"\${$var-$value}\"" +} #=> set_var() + +######################################## +# Invocation entry point: + +NL=' +' + +# Be secure with a restrictive umask +[ -z "$EASYRSA_NO_UMASK" ] && umask 077 + +# Parse options +while :; do + # Separate option from value: + opt="${1%%=*}" + val="${1#*=}" + empty_ok= # Empty values are not allowed unless excepted + + case "$opt" in + --days) + export EASYRSA_CERT_EXPIRE="$val" + export EASYRSA_CA_EXPIRE="$val" + export EASYRSA_CRL_DAYS="$val" + ;; + --pki-dir) + export EASYRSA_PKI="$val" ;; + --use-algo) + export EASYRSA_ALGO="$val" ;; + --keysize) + export EASYRSA_KEY_SIZE="$val" ;; + --curve) + export EASYRSA_CURVE="$val" ;; + --dn-mode) + export EASYRSA_DN="$val" ;; + --req-cn) + export EASYRSA_REQ_CN="$val" ;; + --digest) + export EASYRSA_DIGEST="$val" ;; + --req-c) + empty_ok=1 + export EASYRSA_REQ_COUNTRY="$val" ;; + --req-st) + empty_ok=1 + export EASYRSA_REQ_PROVINCE="$val" ;; + --req-city) + empty_ok=1 + export EASYRSA_REQ_CITY="$val" ;; + --req-org) + empty_ok=1 + export EASYRSA_REQ_ORG="$val" ;; + --req-email) + empty_ok=1 + export EASYRSA_REQ_EMAIL="$val" ;; + --req-ou) + empty_ok=1 + export EASYRSA_REQ_OU="$val" ;; + --ns-cert) + export EASYRSA_NS_SUPPORT="$val" ;; + --ns-comment) + empty_ok=1 + export EASYRSA_NS_COMMENT="$val" ;; + --batch) + empty_ok=1 + export EASYRSA_BATCH=1 ;; + --subca-len) + export EASYRSA_SUBCA_LEN="$val" ;; + --vars) + export EASYRSA_VARS_FILE="$val" ;; + --subject-alt-name) + export EASYRSA_EXTRA_EXTS="\ +$EASYRSA_EXTRA_EXTS +subjectAltName = $val" ;; + *) + break ;; + esac + + # fatal error when no value was provided + if [ ! $empty_ok ] && { [ "$val" = "$1" ] || [ -z "$val" ]; }; then + die "Missing value to option: $opt" + fi + + shift +done + +# Intelligent env-var detection and auto-loading: +vars_setup + +# determine how we were called, then hand off to the function responsible +cmd="$1" +[ -n "$1" ] && shift # scrape off command +case "$cmd" in + init-pki|clean-all) + init_pki "$@" + ;; + build-ca) + build_ca "$@" + ;; + gen-dh) + gen_dh + ;; + gen-req) + gen_req "$@" + ;; + sign|sign-req) + sign_req "$@" + ;; + build-client-full) + build_full client "$@" + ;; + build-server-full) + build_full server "$@" + ;; + gen-crl) + gen_crl + ;; + revoke) + revoke "$@" + ;; + import-req) + import_req "$@" + ;; + export-p12) + export_pkcs p12 "$@" + ;; + export-p7) + export_pkcs p7 "$@" + ;; + set-rsa-pass) + set_pass rsa "$@" + ;; + set-ec-pass) + set_pass ec "$@" + ;; + update-db) + update_db + ;; + show-req) + show req "$@" + ;; + show-cert) + show cert "$@" + ;; + ""|help|-h|--help|--usage) + cmd_help "$1" + exit 0 + ;; + *) + die "Unknown command '$cmd'. Run without commands for usage help." + ;; +esac + +# vim: ft=sh nu ai sw=8 ts=8 diff --git a/bin/env.sh b/bin/env.sh new file mode 100755 index 0000000..c6e2ce8 --- /dev/null +++ b/bin/env.sh @@ -0,0 +1,20 @@ +#!/bin/bash -e + +BASEDIR=`dirname $0`/.. + +if [ ! -d "$BASEDIR/env" ]; then + virtualenv -q $BASEDIR/env --prompt='(dcos_openvpn) ' + echo "Virtualenv created." +fi + +cd $BASEDIR +source $BASEDIR/env/bin/activate +echo "Virtualenv activated." + +if [ ! -f "$BASEDIR/env/updated" -o $BASEDIR/setup.py -nt $BASEDIR/env/updated ]; then + pip install -e $BASEDIR + touch $BASEDIR/env/updated + echo "Requirements installed." +fi + +pip install -r $BASEDIR/requirements.txt diff --git a/bin/envs.sh b/bin/envs.sh new file mode 100755 index 0000000..ade7b06 --- /dev/null +++ b/bin/envs.sh @@ -0,0 +1,8 @@ +export CA_PASS=${CA_PASS:="nopass"} +export CA_CN=${CA_CN:="openvpn.dcos"} +export ZKPATH=${ZKPATH:="/dcos-vpn"} +export ZKCLI=${ZKCLI:="zk-shell"} +export ZKURL=${ZKURL:="master.mesos:2181"} +export CONFIG_LOCATION=${CONFIG_LOCATION:="/etc/openvpn"} +export HOST=${HOST:=127.0.0.1} +export PORT0=${PORT0:=6000} \ No newline at end of file diff --git a/bin/packages.sh b/bin/packages.sh new file mode 100755 index 0000000..2db5e56 --- /dev/null +++ b/bin/packages.sh @@ -0,0 +1,7 @@ +#!/bin/bash -e + +echo "Building wheel..." +python setup.py bdist_wheel + +echo "Building egg..." +python setup.py sdist diff --git a/bin/run.sh b/bin/run.sh new file mode 100755 index 0000000..ec6695e --- /dev/null +++ b/bin/run.sh @@ -0,0 +1,148 @@ +#!/bin/bash + +############################## +# Vars, checks and admin +############################## + +container_files=0 +zookeeper_path=0 +source /dcos/bin/envs.sh + +# Check to see if the container already has the openvpn files locally +# And the second checks for an existing znode in Zookeeper which suggests +# This is being re-run after a previous setup + +function check_status { + if [ -f $CONFIG_LOCATION/openvpn.conf ]; then + container_files=1 + fi + if [[ -z $(run_command "ls $ZKPATH/openvpn.conf") ]]; then + zookeeper_path=1 + fi + echo "container_files = " $container_files + echo "zookeeper_path = " $zookeeper_path +} + +# Workaround to pass add_auth on the one liner as zk-shell doens't provide this as a param + +function run_command { + echo -e "zk-shell --run-from-stdin master.mesos:2181 << EOF\nadd_auth digest $OVPN_USERNAME:$OVPN_PASSWORD\n$1\nEOF" | sh + return $? +} + +############################## +# File download and upload +############################## + +function download_files { + ZKPATH_STRIPPED=$(echo $ZKPATH | sed -e 's/^\///') + for fname in $(run_command "find / $ZKPATH_STRIPPED"); do + local sub_path=$(echo $fname | cut -d/ -f3-) + + # If the sub_path is empty, there's no reason to copy + [[ -z $sub_path ]] && continue + + if [ "$sub_path" == "Failed" ]; then + err "Unable to get data from $ZKURL$ZKPATH. Check your zookeeper." + fi + + local fs_path=$CONFIG_LOCATION/$sub_path + run_command "cp $fname file://$fs_path" > /dev/null 2>&1 + # Directories are copied as empty files, remove them so that the + # subsequent copies actually work. + [ -s $fs_path ] || rm $fs_path + done +} + +function upload_files { + if [ $zookeeper_path = 0 ]; then + run_command "create $ZKPATH '' false false true" + run_command "set_acls /$ZKPATH username_password:$OVPN_USERNAME:$OVPN_PASSWORD:cdrwa" + fi + for fname in $(find $CONFIG_LOCATION -not -type d); do + local zk_location=$(echo $fname | sed 's|'$CONFIG_LOCATION'/|/|') + run_command "cp file://$fname $ZKPATH$zk_location" + done +} + +############################## +# Location set and get +############################## + +function get_location { + echo $(run_command "get $ZKPATH/location.conf") +} + +function set_public_location { + local loc=$ZKPATH/location.conf + source $OPENVPN/ovpn_env.sh + if run_command "ls $loc" ; then + run_command "set $loc \"remote $(wget -O - -U curl ifconfig.me) $PORT0 $OVPN_PROTO\"" + else + run_command "create $loc ''" + set_public_location + fi +} + +############################## +# Main setup +############################## + +function build_configuration { + ovpn_genconfig -u udp://$CA_CN + rm -rf $CONFIG_LOCATION/pki + (echo $CA_CN) | PATH=/dcos/bin:$PATH ovpn_initpki nopass +} + +function setup { + + # Fix a bug in zk-shell copy that's pending a pull request + sed -i 's/return PathValue("".os.path.join(fph.readlines()))/return PathValue("".join(os.path.join(fph.readlines())))/g' /usr/lib/python2.7/site-packages/zk_shell-1.1.3-py2.7.egg/zk_shell/copy.py + + # Replace the shipped easyrsa with our easyrsa to remove the revoke confirmation + sed -i 's/easyrsa/\/dcos\/bin\/easyrsa/g' /usr/local/bin/ovpn_revokeclient + + if [ $zookeeper_path = 1 ] && [ $container_files = 0 ]; then + echo "Files found in Zookeeper - copying to container" + reset_container + download_files + else + echo "Files not found in Zookeeper - generating and uploading" + reset + build_configuration + upload_files + set_public_location + fi +} + +function run_server { + source /dcos/bin/envs.sh + check_status + setup + ovpn_run --daemon + /usr/bin/python -m dcos_openvpn.main +} + +function reset { + run_command "rmr /dcos-vpn/" > /dev/null 2>&1 + reset_container +} + +function reset_container { + rm -rf $CONFIG_LOCATION/pki + rm -f $CONFIG_LOCATION/openvpn.conf $CONFIG_LOCATION/crl.pem $CONFIG_LOCATION/ovpn_env.sh $CONFIG_LOCATION/location.conf +} + +case "$@" in + run_server) run_server ;; + setup) setup ;; + download_files) download_files ;; + upload_files) upload_files ;; + check_status) check_status ;; + build_configuration) build_configuration ;; + set_public_location) set_public_location ;; + get_location) get_location ;; + reset) reset ;; + reset_container) reset_container ;; + *) exit 1 ;; +esac \ No newline at end of file diff --git a/bin/zkshrun.sh b/bin/zkshrun.sh new file mode 100755 index 0000000..eb57457 --- /dev/null +++ b/bin/zkshrun.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +echo -e "zk-shell --run-from-stdin master.mesos:2181 << EOF\nadd_auth digest $OVPN_USERNAME:$OVPN_PASSWORD\n$1\nEOF" | sh \ No newline at end of file diff --git a/config.json b/config.json new file mode 100644 index 0000000..0eb4033 --- /dev/null +++ b/config.json @@ -0,0 +1,64 @@ +{ + "id": "openvpn", + "instances": 1, + "portDefinitions": [], + "container": { + "type": "DOCKER", + "docker": { + "portMappings": [ + { + "containerPort": 5000, + "protocol": "tcp", + "name": "openvpn-admin" + }, + { + "containerPort": 1194, + "hostport": 1194, + "protocol": "udp", + "name": "openvpnudp" + }, + { + "containerPort": 1194, + "hostport": 1194, + "protocol": "tcp", + "name": "openvpntcp" + } + ], + "network": "BRIDGE", + "image": "aggress/richard-openvpn:refactor", + "forcePullImage": true, + "privileged": true + } + }, + "healthChecks": [ + { + "gracePeriodSeconds": 120, + "intervalSeconds": 30, + "timeoutSeconds": 5, + "maxConsecutiveFailures": 0, + "path": "/status", + "portIndex": 0, + "protocol": "MESOS_HTTPS", + "ignoreHttp1xx": false + } + ], + "cpus": 0.1, + "mem": 128, + "secrets": { + "secret0": { + "source": "ovpn_username" + }, + "secret1": { + "source": "ovpn_password" + } + }, + "requirePorts": false, + "env": { + "OVPN_USERNAME": { + "secret": "secret0" + }, + "OVPN_PASSWORD": { + "secret": "secret1" + } + } +} diff --git a/dcos_openvpn/__init__.py b/dcos_openvpn/__init__.py new file mode 100644 index 0000000..16e69f8 --- /dev/null +++ b/dcos_openvpn/__init__.py @@ -0,0 +1,2 @@ + +__version__ = '0.1.0' diff --git a/dcos_openvpn/cert.py b/dcos_openvpn/cert.py new file mode 100644 index 0000000..73a57de --- /dev/null +++ b/dcos_openvpn/cert.py @@ -0,0 +1,41 @@ + +from __future__ import absolute_import, print_function + +import os +import re +import subprocess + +OVPN_USERNAME = os.environ.get('OVPN_USERNAME') +OVPN_PASSWORD = os.environ.get('OVPN_PASSWORD') +CA_PASS = "nopass" + + +def path(name): + return os.path.join(os.environ.get("EASYRSA_PKI", ""), + "private/{0}.key".format(name)) + + +def generate(name): + subprocess.check_call( + "source /dcos/bin/envs.sh; /dcos/bin/easyrsa build-client-full {0} nopass".format( + name), shell=True) + + +def upload(name): + subprocess.check_call('/dcos/bin/zkshrun.sh "cp file:///etc/openvpn/pki /dcos-vpn/pki true true"'.format(name), shell=True) + + +def output(name): + loc = subprocess.check_output("/dcos/bin/run.sh get_location", shell=True) + return re.sub("remote .*", loc, subprocess.check_output( + "ovpn_getclient {0}".format(name), shell=True)) + + +def remove(name): + subprocess.check_call("ovpn_revokeclient {0} remove ".format(name), shell=True) + subprocess.check_call('/dcos/bin/zkshrun.sh "rmr /dcos-vpn/pki"'.format(name), shell=True) + subprocess.check_call('/dcos/bin/zkshrun.sh "cp file:///etc/openvpn/pki /dcos-vpn/pki true true"'.format(name), shell=True) + + +def test(): + subprocess.check_call('/dcos/bin/zkshrun.sh "find dcos-vpn"', shell=True) \ No newline at end of file diff --git a/dcos_openvpn/main.py b/dcos_openvpn/main.py new file mode 100644 index 0000000..434e499 --- /dev/null +++ b/dcos_openvpn/main.py @@ -0,0 +1,56 @@ + +from __future__ import absolute_import, print_function + +import logging +import os +import sys + +from . import web + +OPTIONAL_ENV = [ + "MESOS_CONFIG", + "IMAGE" +] + +REQUIRED_ENV = [ + "HOST", + "EASYRSA_PKI" +] + + +def setup_logging(): + root = logging.getLogger() + root.setLevel(logging.DEBUG) + + ch = logging.StreamHandler(sys.stdout) + ch.setLevel(logging.DEBUG) + formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s') + ch.setFormatter(formatter) + root.addHandler(ch) + + web.app.logger.addHandler(root) + + +def check_env(): + stop = False + + for k in REQUIRED_ENV: + if not k in os.environ: + logging.error("missing required env: {0}".format(k)) + stop = True + + if stop: + sys.exit(1) + + +def main(): + setup_logging() + + check_env() + context = ('/etc/openvpn/pki/issued/openvpn.dcos.crt', '/etc/openvpn/pki/private/openvpn.dcos.key') + web.app.run(host='0.0.0.0', ssl_context=context, threaded=True) + + +if __name__ == "__main__": + main() diff --git a/dcos_openvpn/web.py b/dcos_openvpn/web.py new file mode 100644 index 0000000..17c4455 --- /dev/null +++ b/dcos_openvpn/web.py @@ -0,0 +1,65 @@ + +from __future__ import absolute_import, print_function + +import os +import json +import re +import sys + +from flask import Flask +from flask_basicauth import BasicAuth +from webargs import Arg +from webargs.flaskparser import use_args + +from . import cert + +app = Flask(__name__) + +app.config['BASIC_AUTH_USERNAME'] = os.environ.get('OVPN_USERNAME') +app.config['BASIC_AUTH_PASSWORD'] = os.environ.get('OVPN_PASSWORD') +basic_auth = BasicAuth(app) +ovpn_user = os.environ.get('OVPN_USERNAME') +ovpn_pass = os.environ.get('OVPN_PASSWORD') + + +@app.route("/") +def root(): + return "ok" + + +@app.route("/status") +def status(): + return "ok" + + +@app.route("/test") +@basic_auth.required +def test(): + print(ovpn_user, file=sys.stderr) + print(ovpn_pass, file=sys.stderr) + cert.test() + return "test" + + +@app.route("/client", methods=["POST"]) +@basic_auth.required +@use_args({ + 'name': Arg(str, required=True, + validate=lambda x: bool(re.match("^[a-zA-Z\-0-9]+$", x))) +}) +def create_client(args): + if os.path.exists(cert.path(args.get("name"))): + return json.dumps({ "type": "error", "msg": "client exists" }), 400 + + cert.generate(args.get("name")) + cert.upload(args.get("name")) + + return cert.output(args.get("name")) + + +@app.route("/client/", methods=["DELETE"]) +@basic_auth.required +def remove_client(name): + cert.remove(name) + + return json.dumps({ "type": "status", "msg": "success" }) diff --git a/local_universe_setup.sh b/local_universe_setup.sh new file mode 100755 index 0000000..97c0daa --- /dev/null +++ b/local_universe_setup.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +# Prerequisites +# Docker registry running and available by DC/OS +# docker run -d -p 5000:5000 --restart=always --name registry registry:2 +# Docker daemon on each DC/OS node configured to work with insecure registry +# https://docs.docker.com/registry/insecure/ or secure your registry + + +function build { + cd /Users/richard/code/ + dcos config set core.dcos_url https://192.168.33.11 + dcos auth login --username=admin --password=password + git clone https://github.com/mesosphere/universe --branch=version-3.x + cd universe + cp -R repo/packages/O/openvpn-admin/1 repo/packages/O/openvpn-admin/2 + sed -i -e 's/mesosphere\/dcos-openvpn/aggress\/dcos-openvpn/g' repo/packages/O/openvpn-admin/2/resource.json + sed -i -e 's/0.0.0-0.1/0.0.0-0.2/g' repo/packages/O/openvpn-admin/2/package.json + scripts/build.sh + DOCKER_IMAGE="192.168.33.10:5000/universe-server" DOCKER_TAG="universe-server" docker/server/build.bash + DOCKER_IMAGE="192.168.33.10:5000/universe-server" DOCKER_TAG="universe-server" docker/server/build.bash publish + dcos marathon app add /Users/richard/code/universe/docker/server/target/marathon.json + dcos package repo add --index=0 universe-server http://universe.marathon.mesos:8085/repo +} + +function remove { + dcos package uninstall openvpn-admin + dcos marathon app remove /universe + dcos package repo remove universe-server + rm -rf universe +} + +case "$@" in + remove) remove ;; + build) build ;; + *) echo "build or remove"; exit 1 ;; +esac diff --git a/notes.md b/notes.md new file mode 100644 index 0000000..513b679 --- /dev/null +++ b/notes.md @@ -0,0 +1,38 @@ +notes.md + +subprocess.check_call('$ZKCLI --run-once "cp file://{0} $ZKPATH/{1}" $ZKURL'.format(path(name), os.path.relpath(path(name),os.environ.get("CONFIG_LOCATION"))), shell=True) + +echo -e "zk-shell --run-from-stdin master.mesos:2181 << EOF\nadd_auth digest admin:password\nls /richard\nEOF" | sh + +curl -H "Authorization: token=$(dcos config show core.dcos_acs_token)" -X POST -d "name=richard" https://192.168.33.12:1271/client + +curl -k -u admin:password -X POST -d "name=richard" https://192.168.33.12:31074/client + + +def testkazoo(): + acl = make_digest_acl('admin', 'password', read=True, write=True, create=True, delete=True, admin=True, all=True) + zk = KazooClient(hosts='master.mesos:2181', default_acl=[acl]) + zk.start() + zk.add_auth("digest", "admin:password") + zk.ensure_path("/richard/test1") + zk.stop() + +"cmd": "/usr/bin/python -m dcos_openvpn.main", + + +#!/bin/bash + +echo -e "zk-shell --run-from-stdin master.mesos:2181 << EOF\nadd_auth digest $OVPN_USERNAME:$OVPN_PASSWORD\n$1\nEOF" | sh + + +removed '/etc/openvpn/pki/issued/richard.crt' +removed '/etc/openvpn/pki/private/richard.key' +removed '/etc/openvpn/pki/reqs/richard.req' + +/dcos-vpn/pki/reqs/richard.req +/dcos-vpn/pki/private/richard.key +/dcos-vpn/pki/issued/richard.crt + + + + "cmd": "/usr/bin/python -m dcos_openvpn.main", \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..baa3948 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +sphinx +tox +wheel +webargs=0.15.0 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..79bc678 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,5 @@ +[bdist_wheel] +# This flag says that the code is written to work on both Python 2 and Python +# 3. If at all possible, it is good practice to do this. If you cannot, you +# will need to generate wheels for each Python version that you support. +universal=1 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..fda295b --- /dev/null +++ b/setup.py @@ -0,0 +1,99 @@ + +from __future__ import absolute_import, print_function + +from setuptools import setup, find_packages +from codecs import open +from os import path + + +here = path.abspath(path.dirname(__file__)) + +# Get the long description from the relevant file +with open(path.join(here, 'README.md'), encoding='utf-8') as f: + long_description = f.read() + +setup( + # TODO: Set the project name + name='dcos-openvpn', + + # Versions should comply with PEP440. For a discussion on single-sourcing + # the version across setup.py and the project code, see + # https://packaging.python.org/en/latest/single_source_version.html + # TODO: Set the version + version='0.1.0', + + # TODO: Set the description + description='DCOS OpenVPN', + long_description=long_description, + + # The project's main homepage. + # TODO: Set the project URL + url='https://github.com/mesosphere/dcos-openvpn', + + # Author details + author='Mesosphere, Inc.', + author_email='team@mesosphere.io', + + classifiers=[ + 'Development Status :: 3 - Alpha', + + 'Intended Audience :: Developers', + 'Intended Audience :: Information Technology', + + 'License :: OSI Approved :: Apache Software License', + + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.2', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + ], + + # What does your project relate to? + keywords='dcos openvpn', + + # You can just specify the packages manually here if your project is + # simple. Or you can use find_packages(). + packages=find_packages(exclude=['contrib', 'docs', 'tests*']), + + # List run-time dependencies here. These will be installed by pip when your + # project is installed. For an analysis of "install_requires" vs pip's + # requirements files see: + # https://packaging.python.org/en/latest/requirements.html + install_requires=[ + 'flask >= 0.10.1', + 'webargs == 0.15.0', + # 'mesos.native >= 0.22.0', + 'zk-shell', + 'Flask-BasicAuth', + 'pyopenssl', + 'kazoo == 2.2.1' + ], + + # List additional groups of dependencies here (e.g. development + # dependencies). You can install these using the following syntax, for + # example: + # $ pip install -e .[dev,test] + extras_require={ + 'dev': ['check-manifest'], + 'test': ['coverage'], + }, + + # If there are data files included in your packages that need to be + # installed, specify them here. If using Python 2.6 or less, then these + # have to be included in MANIFEST.in as well. + package_data={}, + + # Although 'package_data' is the preferred approach, in some case you may + # need to place data files outside of your packages. + # In this case, 'data_file' will be installed into '/my_data' + # data_files=[('my_data', ['data/data_file'])], + data_files=[], + + # To provide executable scripts, use entry points in preference to the + # "scripts" keyword. Entry points provide cross-platform support and allow + # pip to create the appropriate form of executable for the target platform. + entry_points={}, +) diff --git a/tests.md b/tests.md new file mode 100644 index 0000000..6ebd391 --- /dev/null +++ b/tests.md @@ -0,0 +1,26 @@ +Testing + +Pre + reset ZK + Add secrets to DC/OS + +Install + ovpn running + python running + +Auth + Check random u:p works + Auth on client page + +Add user + Get ovpn creds + Check ZK upload + +Revoke user + Check container files + Check ZK files removed + +Re-install + dcos marathon app remove /openvpn + dcos marathon app add config.json + Check ZK download to container diff --git a/version b/version new file mode 100644 index 0000000..7273c0f --- /dev/null +++ b/version @@ -0,0 +1 @@ +25