From 32284c984f74f770701d242eb9b5de9245f1e7fc Mon Sep 17 00:00:00 2001 From: Justin Bailey Date: Mon, 6 Mar 2023 09:13:09 -0800 Subject: [PATCH] Add nix flake for running this program; security& logging update. * Added flake definition so this program can be run easily via nix. * Added `user` and `group` setting to YAML configuration, allowing openvpn to run unprivileged (after initialization). * Nix flake app runs as `nobody:nobody`. * Added a "logLevel" flag to program and set default to "Info" so SAML response (credentials) aren't logged by default. * Updated README with instructions. Tested on MacOS x86; needs testing on M1 & Linux. --- .gitignore | 6 +- README.md | 52 ++++++++- awsvpnclient.go | 6 + awsvpnclient.yml.example | 2 + config.go | 2 + flake.lock | 234 +++++++++++++++++++++++++++++++++++++++ flake.nix | 94 ++++++++++++++++ serve.go | 27 ++++- 8 files changed, 418 insertions(+), 5 deletions(-) create mode 100644 flake.lock create mode 100644 flake.nix diff --git a/.gitignore b/.gitignore index bd2a259..2659ef9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,8 @@ aws-vpn-client openvpn *.yml -*.openvpn \ No newline at end of file +*.openvpn +.devenv +.direnv +.envrc + diff --git a/README.md b/README.md index 985bbd3..ce435da 100644 --- a/README.md +++ b/README.md @@ -17,11 +17,61 @@ AWS compatible OpenVPN v2.4.9, based on the 1. Build patched openvpn version and put it to the folder with a script 2. Build aws-vpn-client wrapper `go build .` -3. `cp ./awsvpnclient.yml.example ./awsvpnclient.yml` and update the necsery paths. +3. `cp ./awsvpnclient.yml.example ./awsvpnclient.yml` and update the necessary paths. 4. Finally run `./aws-vpn-client serve --config myconfig.openvpn` to connect to the AWS. +## Security + +OpenVPN recommends running the openvpn binary as an unprivileged user after initialization (see https://openvpn.net/community-resources/hardening-openvpn-security/). The `awsvpnclinet.yml` file includes the `user` and `group` keys, demonstrating how to run +`openvpn` as the `nobody` user (and group). If those keys are not present, the binary will run continue to run as whichever +user launched it originally. + ## Todo * Unit tests * General Code Cleanup * Better integrate SAML HTTP server with a script or rewrite everything on golang + +# Using via Nix Flakes + +This program can be run via `nix`, using the `flakes` feature. You will need to know how to install nix and what flakes +are in order to follow these instructions. + +## Apps + +Two apps are defined. One makes it easy to open a tunnel with a given VPN profile, the other lets you run the original program (meaning +you must provide all arguments): + +- *default app* - Use `nix run .` (or replace `.` with the flake reference for this repo) to run the default program. Just give a path to the OpenVPN configuration file and it should work. Note you will likely +need to run under `sudo`: + +``` +$ sudo su +... +# nix run . -- ~/.config/AWSVPNClient/OpenVpnConfigs/ +``` + +Note that this app is hard-coded to run as the `nobody` user (and group). If that does not exist on your system, you will have +to override the existing configuration. + +- *aws-vpn-client-unwrapped app* - Use `nix run .#aws-vpn-client-unwrapped` to run the original program, allowing more control over arguments given. + +## Packages + +This flake provides two main packages, `aws-vpn-client` (also the default package) and `aws-vpn-client-unwrapped`. + +Besides those two packages, it also provides a patched `openvpn` client (necessary to using this program). + +### `aws-vpn-client-unwrapped` + +This is the original program from this repo, provided for more control over arguments. For convenience, a `awsvpnclient.yml` is generated when the program is installed and is placed +in the `bin` directory next to the executable. (It will not be used automatically tho - the original program always looks in the current workign directory or +your home directory for that file). +### `aws-vpn-client` + +This is a wrapper around the original program, updated so you can just pass the path to a VPN configuration and it will open that tunnel. + +## Shell (Development) + +This flake uses the excellent tools from `devensh.sh` to provide a Go environment for development. Use `nix develop` to +enter the shell. \ No newline at end of file diff --git a/awsvpnclient.go b/awsvpnclient.go index 6cc7683..14a5d15 100644 --- a/awsvpnclient.go +++ b/awsvpnclient.go @@ -57,6 +57,12 @@ func main() { Value: os.TempDir(), Usage: "Temp folder location of formatted openvpn configurations.", }, + &cli.StringFlag{ + TakesFile: false, + Name: "logLevel", + Value: "1", + Usage: "Logging detail. Should be an integer value between -1 and 5 (logging levels in the zerolog library). Defaults to '0' (Info level).", + }, }, }, } diff --git a/awsvpnclient.yml.example b/awsvpnclient.yml.example index 773b7dc..caa9f18 100644 --- a/awsvpnclient.yml.example +++ b/awsvpnclient.yml.example @@ -5,5 +5,7 @@ vpn: sudo: /usr/bin/sudo shellargs: - "-c" + user: nobody + group: nobody server: addr: "127.0.0.1:35001" \ No newline at end of file diff --git a/config.go b/config.go index 3faf896..b0e960d 100644 --- a/config.go +++ b/config.go @@ -13,6 +13,8 @@ type ( Sudo string Shell string ShellArgs []string + User string + Group string } server struct { diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..eab6dc2 --- /dev/null +++ b/flake.lock @@ -0,0 +1,234 @@ +{ + "nodes": { + "devenv": { + "inputs": { + "flake-compat": "flake-compat", + "nix": "nix", + "nixpkgs": "nixpkgs", + "pre-commit-hooks": "pre-commit-hooks" + }, + "locked": { + "lastModified": 1678113843, + "narHash": "sha256-m2oHgGnfdhZxBuA+ET0MGOxVkekYzu2paWyhd4mNNm0=", + "owner": "cachix", + "repo": "devenv", + "rev": "8e0478018a410f47d947b4c16de0927170f226bf", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "devenv", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1673956053, + "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-utils": { + "locked": { + "lastModified": 1667395993, + "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_2": { + "locked": { + "lastModified": 1676283394, + "narHash": "sha256-XX2f9c3iySLCw54rJ/CZs+ZK6IQy7GXNY4nSOyu2QG4=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "3db36a8b464d0c4532ba1c7dda728f4576d6d073", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "devenv", + "pre-commit-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1660459072, + "narHash": "sha256-8DFJjXG8zqoONA1vXtgeKXy68KdJL5UaXR8NtVMUbx8=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "a20de23b925fd8264fd7fad6454652e142fd7f73", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "lowdown-src": { + "flake": false, + "locked": { + "lastModified": 1633514407, + "narHash": "sha256-Dw32tiMjdK9t3ETl5fzGrutQTzh2rufgZV4A/BbxuD4=", + "owner": "kristapsdz", + "repo": "lowdown", + "rev": "d2c2b44ff6c27b936ec27358a2653caaef8f73b8", + "type": "github" + }, + "original": { + "owner": "kristapsdz", + "repo": "lowdown", + "type": "github" + } + }, + "nix": { + "inputs": { + "lowdown-src": "lowdown-src", + "nixpkgs": [ + "devenv", + "nixpkgs" + ], + "nixpkgs-regression": "nixpkgs-regression" + }, + "locked": { + "lastModified": 1676545802, + "narHash": "sha256-EK4rZ+Hd5hsvXnzSzk2ikhStJnD63odF7SzsQ8CuSPU=", + "owner": "domenkozar", + "repo": "nix", + "rev": "7c91803598ffbcfe4a55c44ac6d49b2cf07a527f", + "type": "github" + }, + "original": { + "owner": "domenkozar", + "ref": "relaxed-flakes", + "repo": "nix", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1677534593, + "narHash": "sha256-PuZSAHeq4/9pP/uYH1FcagQ3nLm/DrDrvKi/xC9glvw=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "3ad64d9e2d5bf80c877286102355b1625891ae9a", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-regression": { + "locked": { + "lastModified": 1643052045, + "narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2", + "type": "github" + } + }, + "nixpkgs-stable": { + "locked": { + "lastModified": 1673800717, + "narHash": "sha256-SFHraUqLSu5cC6IxTprex/nTsI81ZQAtDvlBvGDWfnA=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "2f9fd351ec37f5d479556cd48be4ca340da59b8f", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-22.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1678072060, + "narHash": "sha256-6a9Tbjhir5HxDx4uw0u6Z+LHUfYf7tsT9QxF9FN/32w=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "47c003416297e4d59a5e3e7a8b15cdbdf5110560", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-22.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "pre-commit-hooks": { + "inputs": { + "flake-compat": [ + "devenv", + "flake-compat" + ], + "flake-utils": "flake-utils", + "gitignore": "gitignore", + "nixpkgs": [ + "devenv", + "nixpkgs" + ], + "nixpkgs-stable": "nixpkgs-stable" + }, + "locked": { + "lastModified": 1677160285, + "narHash": "sha256-tBzpCjMP+P3Y3nKLYvdBkXBg3KvTMo3gvi8tLQaqXVY=", + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "rev": "2bd861ab81469428d9c823ef72c4bb08372dd2c4", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "type": "github" + } + }, + "root": { + "inputs": { + "devenv": "devenv", + "flake-utils": "flake-utils_2", + "nixpkgs": "nixpkgs_2" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..a6d91f4 --- /dev/null +++ b/flake.nix @@ -0,0 +1,94 @@ +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-22.11"; + devenv.url = "github:cachix/devenv"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, devenv, flake-utils, ... } @ inputs: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { + inherit system; + }; + openvpn = pkgs.openvpn.overrideAttrs (_: { patches = [ ./scripts/openvpn-v2.5.1-aws.patch ];}); + aws-vpn-client-unwrapped = pkgs.buildGoModule { + src = ./.; + pname = "aws-vpn-client-unwrapped"; + version = "0.0.1"; + vendorSha256 = "sha256-602xj0ffJXQW//cQeByJjtQnU0NjqOrZWTCWLLhqMm0"; + + postFixup = '' + cat > $out/bin/awsvpnclient.yml <<-HERE + vpn: + openvpn: ${openvpn}/bin/openvpn + port: 1194 + user: nobody + group: nobody + server: + addr: "127.0.0.1:35001" + HERE + ''; + }; + aws-vpn-client = pkgs.stdenv.mkDerivation { + pname = "aws-vpn-client"; + version = "0.0.1"; + buildInputs = [ aws-vpn-client-unwrapped ]; + nativeBuildInputs = [ pkgs.makeWrapper ]; + + dontUnpack = true; + dontPatch = true; + dontConfigure = true; + dontBuild = true; + doCheck = false; + + installPhase = '' + mkdir -p $out/bin + makeWrapper ${aws-vpn-client-unwrapped}/bin/aws-vpn-client $out/bin/aws-vpn-client \ + --chdir ${aws-vpn-client-unwrapped}/bin \ + --add-flags "serve --config" + ''; + }; + in { + devShells = { + default = devenv.lib.mkShell { + inherit inputs pkgs; + modules = [ + { + # https://devenv.sh/reference/options/ + packages = [ openvpn pkgs.inetutils pkgs.expect ]; + + languages.python.enable = true; + enterShell = '' + export PS1='\e[1;34mƛ > \e[0m' + ''; + } + ]; + }; + }; + packages = { + inherit + aws-vpn-client + aws-vpn-client-unwrapped; + openvpn = openvpn; + default = aws-vpn-client; + }; + apps = { + # This runs the program with the `serve` argument and `--config` flag, so you just have + # to provide the path to an OpenVPN config. + # + # Note you likely need to run this under `sudo`, as openvpn (which ultimately does the work) + # requires root access to manage VPN tunnels. + default = { + type = "app"; + program = "${self.packages.${system}.aws-vpn-client}/bin/aws-vpn-client"; + }; + # This runs the bare program, so you can pass any custom arguments. + aws-vpn-client-unwrapped = { + type = "app"; + program = "${self.packages.${system}.aws-vpn-client-unwrapped}/bin/aws-vpn-client"; + }; + }; + } + ); +} diff --git a/serve.go b/serve.go index 0196365..f443d67 100644 --- a/serve.go +++ b/serve.go @@ -9,6 +9,7 @@ import ( "os/exec" "strconv" + "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/urfave/cli/v2" "mvdan.cc/xurls/v2" @@ -27,6 +28,15 @@ type ( ) func serveAction(c *cli.Context) error { + + level, err := zerolog.ParseLevel(c.String("logLevel")) + if err != nil { + log.Info().Msg("Error parsing log level '" + c.String("logLevel") + "'. Defaulting to 'Info' level.") + zerolog.SetGlobalLevel(zerolog.InfoLevel) + } else { + zerolog.SetGlobalLevel(level) + } + log.Info().Msg("Loading configuration files...") openVPNConfig := c.String("config") @@ -160,14 +170,25 @@ func startOpenVPNConnection(handle *serveHandle) { log.Fatal().Err(err).Msg("Failed saving auth config for OpenVPN tunnel! " + errorSuffix) } - baseCommand := exec.Command( - handle.Config.Vpn.OpenVPN, - "--verb", "3", + args := []string{"--verb", "3", "--config", handle.OpenVPNConnectionConfig.Filename, "--proto", handle.OpenVPNConnectionConfig.Protocol, "--remote", handle.ServiceIPv4, strconv.FormatInt(int64(handle.OpenVPNConnectionConfig.Port), 10), "--script-security", "2", "--auth-user-pass", tmpAuthConifg, + } + + if handle.Config.Vpn.User != "" { + args = append(args, "--user", handle.Config.Vpn.User) + } + + if handle.Config.Vpn.Group != "" { + args = append(args, "--group", handle.Config.Vpn.Group) + } + + baseCommand := exec.Command( + handle.Config.Vpn.OpenVPN, + args..., ) if handle.Config.Vpn.Shell == "" {