Code Monkey home page Code Monkey logo

aws-vpn-client's Introduction

aws-vpn-client

This is PoC to connect to the AWS Client VPN with OSS OpenVPN using SAML authentication. Tested on macOS and Linux, should also work on other POSIX OS with a minor changes.

See my blog post for the implementation details.

P.S. Recently AWS released Linux desktop client, however, it is currently available only for Ubuntu, using Mono and is closed source.

Content of the repository

How to use

  1. Build patched openvpn version and put it to the folder with a script
  2. Start HTTP server with go run server.go
  3. Set VPN_HOST in the aws-connect.sh
  4. Replace CA section in the sample vpn.conf with one from your AWS configuration
  5. Finally run aws-connect.sh to connect to the AWS.

Additional Steps

Inspect your ovpn config and remove the following lines if present

  • auth-user-pass (we dont want to show user prompt)
  • auth-federate (propietary AWS keyword)
  • auth-retry interact (do not retry on failures)
  • remote and remote-random-hostname (already handled in CLI and can cause conflicts with it)

Todo

Better integrate SAML HTTP server with a script or rewrite everything on golang

aws-vpn-client's People

Contributors

ivankovnatsky avatar ninjapanzer avatar rnhurt avatar samm-git avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

aws-vpn-client's Issues

Issues with building on OSX

Platform: OSX Big Sur 11.2.3 (20D91)

As many I'm sure I found your repo after finding the AWS VPN Client very annoying. I downloaded it and then was trying to determine how to build it.. I think your README steps may be a bit outdated.

I have to confess that it took me longer than it should have to figure out what to do with the openvpn-aws.rb file, after trying to run it in Ruby, I finally figured out that it's some sorta local brew formula and I attempted to run it with

brew install --formula openvpn-aws.rb

It appeared to start working downloading, patching, but when it got to the compile section I ran into two errors:

==> ./configure --with-crypto-library=openssl --enable-pkcs11 --prefix=/usr/local/Cellar/openvpn-aws/2.5.1
Error: An exception occurred within a child process:
  Utils::Inreplace::Error: inreplace failed
sample/sample-plugins/Makefile:
  expected replacement of #<Pathname:/usr/local/Homebrew/Library/Homebrew/shims/mac/super/sed> with "/usr/bin/sed"

then

  ==> make install
Error: An exception occurred within a child process:
  Errno::ENOENT: No such file or directory @ apply2files - /usr/local/Cellar/openvpn-aws/2.5.1/share/doc/openvpn-aws/README.mbedtls

I did manage to get past both errors by commenting out those sections of the formula here is the DIFF:

diff --git a/openvpn-aws.rb b/openvpn-aws.rb
index 1f0d1b7..517c4cc 100644
--- a/openvpn-aws.rb
+++ b/openvpn-aws.rb
@@ -38,8 +38,8 @@ end
     inreplace "sample/sample-plugins/Makefile" do |s|
       s.gsub! HOMEBREW_LIBRARY/"Homebrew/shims/mac/super/pkg-config",
               Formula["pkg-config"].opt_bin/"pkg-config"
-      s.gsub! HOMEBREW_LIBRARY/"Homebrew/shims/mac/super/sed",
-              "/usr/bin/sed"
+      # s.gsub! HOMEBREW_LIBRARY/"Homebrew/shims/mac/super/sed",
+      #         "/usr/bin/sed"
     end
     system "make", "install"

@@ -51,7 +51,7 @@ end
     (etc/"openvpn").install doc/"samples/sample-config-files/server.conf"

     # We don't use mbedtls, so this file is unnecessary & somewhat confusing.
-    rm doc/"README.mbedtls"
+    # rm doc/"README.mbedtls"
   end

   def post_install

After this the build was successful at least as far as I could tell. However when I setup the vpn.conf file with all the settings correctly (as the AWS VPN Client) worked with them all i got was the dreaded

Sat Apr 24 12:01:14 2021 AUTH: Received control message: AUTH_FAILED,Invalid username or password

I spent a couple hours fighting this, but in the end I don't believe it's my setup, but possibly the openpn i built. The reason I say this, is I borrowed acvc_openvpn from the /Applications/AWS VPN Client/AWS VPN Client.app/Contents/Resources/openvpn/ and when I updated the aws-connect.sh to point to the acvc-openvpn boom my VPN connected perfectly. So I'm guessing somehow i messed up the build/patching OR I've seen references to issues with "large SAML" responses.. My saml-response.txt is 12,230 bytes if that helps.

Anyway I mostly opened this ticket to hopefully help solve the build issue I was having as I'd prefer to use your patched newer version if possible. I am open other tickets with some other ideas.

Consistent auth failed

Similar to #5
I am also getting AUTH_FAILED and cannot overcome it for the life of me. It is a 100% failure rate

Getting SAML redirect URL from the AUTH_FAILED response (host: 54.189.XXX.YYY:443)
Opening browser and wait for the response file...
Running OpenVPN with sudo. Enter password if requested
2021-12-31 01:28:28 OpenVPN 2.5.5 [git:makepkg/e3bac09f6a128260+] x86_64-pc-linux-gnu [SSL (OpenSSL)] [LZO] [LZ4] [EPOLL] [PKCS11] [MH/PKTINFO] [AEAD] built on Dec 31 2021
2021-12-31 01:28:28 library versions: OpenSSL 1.1.1l  24 Aug 2021, LZO 2.10
2021-12-31 01:28:28 NOTE: the current --script-security setting may allow this configuration to call user-defined scripts
2021-12-31 01:28:28 TCP/UDP: Preserving recently used remote address: [AF_INET]54.189.XXX.YYY:443
2021-12-31 01:28:28 Socket Buffers: R=[212992->212992] S=[212992->212992]
2021-12-31 01:28:28 UDP link local: (not bound)
2021-12-31 01:28:28 UDP link remote: [AF_INET]54.189.XXX.YYY:443
2021-12-31 01:28:28 TLS: Initial packet from [AF_INET]54.189.XXX.YYY:443, sid=100f471a e691210f
2021-12-31 01:28:28 VERIFY OK: depth=1, CN=Mason America Inc
2021-12-31 01:28:28 VERIFY KU OK
2021-12-31 01:28:28 Validating certificate extended key usage
2021-12-31 01:28:28 ++ Certificate has EKU (str) TLS Web Server Authentication, expects TLS Web Server Authentication
2021-12-31 01:28:28 VERIFY EKU OK
2021-12-31 01:28:28 VERIFY OK: depth=0, CN=VPN-Server
2021-12-31 01:28:28 Control Channel: TLSv1.2, cipher TLSv1.2 ECDHE-RSA-AES256-GCM-SHA384, peer certificate: 2048 bit RSA, signature: RSA-SHA256
2021-12-31 01:28:28 [VPN-Server] Peer Connection Initiated with [AF_INET]54.189.XXX.YYY:443
2021-12-31 01:28:29 SENT CONTROL [VPN-Server]: 'PUSH_REQUEST' (status=1)
2021-12-31 01:28:29 AUTH: Received control message: AUTH_FAILED,Invalid username or password
2021-12-31 01:28:29 SIGTERM[soft,auth-failure] received, process exiting       

My conf looks like this:

client
dev tun
proto udp
resolv-retry infinite
nobind
remote-cert-tls server
cipher AES-256-GCM
verb 3
<ca>
snipped cert
</ca>

auth-nocache
reneg-sec 0

We're also using Okta for SAML. I also found through experimenting that the port has to be 443 for anything to happen.

I'm trying this on manjaro linux. Here's the script for how I compiled openvpn 2.5.5: https://github.com/AnilRedshift/awsvpn-saml/blob/main/PKGBUILD

In that repository, I also forked my own code for the server & script, but I get the same error regardless if I try my code or the code in this repository.

Things I've tried

  • Changing ports
  • Changing the host to use the URL
  • Ensured that the SAML token coming back is valid (decoded the base64 and it looks like valid xml)
  • Ran tcpdump on the socket to verify that there is traffic
  • Tried with openvpn 2.4.11 instead of 2.5.5 (using the correct corresponding patch)

If you want to repro my exact setup, then either in arch or manjaro, you can run git clone https://github.com/AnilRedshift/awsvpn-saml and then makepkg -i
That will install openvpn, and then you just need to use OVPN_BIN="/usr/share/awsvpn-saml/bin/openvpn" to run the aws-connect.sh script in this repo.

(or you can run the awsvpn shell script installed by my makepkg if you want to use 100% of my code

Build patch process

Would you please add more details about the build patch process?, because I applied the patch but When I tried to build the openvpn binary file, I got the error message:
rpmbuild -tb openvpn-patched.tar.gz
error: Found more than one spec file in openvpn-patched.tar.gz

I couldn't found the configure file, trying to follow the other approach to build it.

Suggestions for server.go / aws-connect.sh

Hey while using your great work to get around the issue of openvpn not supporting auth-federate and the larger saml tokens. I did find the fact that you had to start and stop the server.go by hand everytime. I guess one could leave it running, but I was curious to expand my meager skills with golang So I made some changes:

aws-connect.sh

diff --git a/aws-connect.sh b/aws-connect.sh
index 8f297fb..3a55e6c 100755
--- a/aws-connect.sh
+++ b/aws-connect.sh
@@ -27,6 +27,9 @@ SRV=$(dig a +short "${RAND}.${VPN_HOST}"|head -n1)
 # cleanup
 rm -f saml-response.txt

+# start the saml response server and background it
+go run server.go >> /tmp/aws-connect-saml-server.log 2>&1 &
+
 echo "Getting SAML redirect URL from the AUTH_FAILED response (host: ${SRV}:${PORT})"
 OVPN_OUT=$($OVPN_BIN --config "${OVPN_CONF}" --verb 3 \
      --proto "$PROTO" --remote "${SRV}" "${PORT}" \

The change is simple, I started the server.go when we run the aws-connect.sh and background it.

server.go

diff --git a/server.go b/server.go
index 48fe1f5..45908e9 100644
--- a/server.go
+++ b/server.go
@@ -6,31 +6,51 @@ import (
        "log"
        "net/http"
        "net/url"
+       "os"
+       "time"
 )

 func main() {
-       http.HandleFunc("/", SAMLServer)
-       log.Printf("Starting HTTP server at 127.0.0.1:35001")
-       http.ListenAndServe("127.0.0.1:35001", nil)
-}
+       var timeoutchan chan(bool) = make(chan bool)

-func SAMLServer(w http.ResponseWriter, r *http.Request) {
-       switch r.Method {
-       case "POST":
-               if err := r.ParseForm(); err != nil {
-                       fmt.Fprintf(w, "ParseForm() err: %v", err)
-                       return
-               }
-               SAMLResponse := r.FormValue("SAMLResponse")
-               if len(SAMLResponse) == 0 {
-                       log.Printf("SAMLResponse field is empty or not exists")
+       SAMLServer := func(w http.ResponseWriter, r *http.Request) {
+               switch r.Method {
+               case "POST":
+                       if err := r.ParseForm(); err != nil {
+                               fmt.Fprintf(w, "ParseForm() err: %v", err)
+                               return
+                       }
+                       SAMLResponse := r.FormValue("SAMLResponse")
+                       // fmt.Println(url.QueryEscape(SAMLResponse))
+
+                       if len(SAMLResponse) == 0 {
+                               log.Printf("SAMLResponse field is empty or not exists")
+                               return
+                       }
+                       ioutil.WriteFile("saml-response.txt", []byte(url.QueryEscape(SAMLResponse)), 0600)
+                       fmt.Fprintf(w, "Got SAMLResponse field, it is now safe to close this window\n")
+                       log.Printf("Got SAMLResponse field and saved it to the saml-response.txt file")
+                       timeoutchan <- true
                        return
+               default:
+                       fmt.Fprintf(w, "Error: POST method expected, %s recieved", r.Method)
                }
-               ioutil.WriteFile("saml-response.txt", []byte(url.QueryEscape(SAMLResponse)), 0600)
-               fmt.Fprintf(w, "Got SAMLResponse field, it is now safe to close this window\n")
-               log.Printf("Got SAMLResponse field and saved it to the saml-response.txt file")
-               return
-       default:
-               fmt.Fprintf(w, "Error: POST method expected, %s recieved", r.Method)
+       }
+
+       go func() {
+               http.HandleFunc("/", SAMLServer)
+               log.Printf("Starting HTTP server at 127.0.0.1:35001")
+               http.ListenAndServe("127.0.0.1:35001", nil)
+       }()
+
+       select {
+               case <-timeoutchan:
+                       log.Println("Work completed exiting...");
+                       os.Exit(0)
+                       break
+               case <-time.After(15 * time.Second):
+                       fmt.Println("Exiting due to 15 second timer... this likely indicates something went wrong.")
+                       os.Exit(1)
+                       break
        }
 }

Now as I mentioned golang isn't a strong language for me.. so I'm sure there are improvements. I made a few changes. first I added go routines to make the process more dynamic. Now when a user successfully does POST and gets a SAML response the server.go shuts down automatically. I did also add an automatic timeout for the server.go the idea was if something went wrong it would run forever just 15 seconds, but after thinking about it.. This could be an issue because if your browser isn't caching the SAML provider tokens, u will need to login which likely will take more than 15 seconds.

Anyway I thought I would share the idea and see what you think. I also am submitting another ticket.. with a new spin that doesn't require server.go or listening for a POST response, at least for OKTA SAML provider.

If you don't like the changes no worries, just close the ticket just wanted to share the ideas.

Suggestion to remove server.go and listen for POST SAML response

Hey so you may have already read my other tickets, as you can guess I spent a lot of time and had lots of frustrating fun learning the ins and outs of AWS VPN with Okta SAML provider. My company ONLY uses Okta SAML so these changes will likely only work for OKTA but I'm guessing the "logic" should work for others with some tweaking.

I got to wondering why do we need to open a browser window and login and run a server.go to listen for any SAML responses, while a really ingenious idea I was curious if there might be another option. Now I won't lie I spent many many hours fighting with this stuff enough so that were I do it again I probably would have stopped with your solution, which is great and has the advantage of using the browser cache for tokens so you don't have to login constantly.

Using Firefox's Network Tools I was able to track the transactions going through to see how the process worked, and I could see how when the browser had an okta token cached it never had to login but just automatically sent the POST and got the SAML reponse. I was curious could I use normal REST calls to perform the same transactions and to perform an OKTA Login and get a SID and SessionToken which I could then submit to the AWS Client VPN endpoint and connect openvpn.

Well the short answer I was able to do so, now of course the main concern here is.. if we have to do a login everytime that would be a big pain for the user, which is one of the advantages of your solution that using the browser you usually don't have to login but 1x per day. However with lots of trial and error I learned that as long as you preserve the SessionToken and Okta SID in a file, you could use those to create a new SAML reponse multiple times and connect to AWS Client VPN without every having to enter your user/pass/mfa info. Great!

Now please bear in mind this code is rough and I'm no hard-core developer, so please feel free to improve if you like. But I created a NodeJS script that performs all the HTTP calls. The logic is pretty straight-forward.

  • when you run aws-connect-okta.sh it no longer needs the server.go instead it starts the nodejs script: get-saml-response.js
  • this script first checks to see if it already has a OKTA SID (saml-sid.txt) and Okta Session Token.
    ** If they exists it then tries to immediately get a new SAML response,
    ** if successful you are done we return back to aws-connect-okta.sh and opevpn connects and everyone is happy.
  • If the SID/Token do not exist or have expired the SAML response request will fail and
    ** this will trigger new HTTP requests to OKTA to "login w/ user/password" and
    ** perform an MFA challenge which in turn gets us a sessionToken.
    ** We then send the sessionToken to AWS VPN Client saml app and this returns us both an OKTA SID (useful to allow future SAML response requests without login) and the SAML Response, which the script then saves to files and exits returning back to aws-connect-okta.sh for the VPN connect.

Now as a reminder I have only tested this on OKTA, I'm sure other providers will require some changes to the HTTP URLs and if you don't use MFA the same, but the logic is pretty cool I think. I don't know if it's worth trying to explore this further for other scenarios, but my objective of OKTA Provider + AWS Client VPN seems to work fairly nicely. I'm sure there are a few bugs in there to work out, but I wanted to share and say:

Thank you for your hard work which made this all possible

Attached here are the two files: You will need to set the following to make this work:
company, provider id, username, password

aws-connect.sh
Add the following just before the wait_file "saml_response.txt"

node get-saml-response.js

get-saml-response.js

#!/usr/bin/env node

const axios = require("axios").default;
const fs = require("fs").promises;
const he = require("he");
const prompt = require("prompt");

const username = "USERNAME/EMAIL";
const password = "PASSWORD";

const properties = [
    {
        name: 'mfaToken',
        hidden: true
    }
];

(async function () {
    var myArgs = process.argv.slice(2);

    checkExistingCredentials = async function () {
        try {
            console.log("Reading saml-sid.txt")
            oktaSid = await fs.readFile("saml-sid.txt");
            console.log("Reading saml-sessionToken.txt")
            sessionToken = await fs.readFile("saml-sessionToken.txt");
            console.log(`Existing Creds Found: SessionToken: ${sessionToken} OktaSID: ${oktaSid}`)
            await getSamlResponse(sessionToken, oktaSid);
            return true
        } catch (err) {
            console.log("Error while checking for existing Credentials, proceeding with new session request.");
            console.log(err);
            return false;
        }
    };

    getSamlResponse = async function (sessionToken, oktaSid) {
        options = {};
        if (oktaSid) {
            options = {
                headers: {
                    Cookie: "sid=" + oktaSid,
                }
            };
        }
        try {
            response = await axios.get(
                `https://<company>.okta.com/app/aws_clientvpn/<provider id>/sso/saml?sessionToken=${sessionToken}`,
                options
            );
            oktaSid = null;
            response.headers["set-cookie"].forEach((cookie) => {
                match = /sid=(.*); Path=.*/.exec(cookie);
                if (match) {
                    oktaSid = match[1];
                }
            });
            if (!oktaSid) {
                console.log("Error parsing Sid cookie, we have to abort");
                process.exit(4);
            }
            console.log(`Okta SID: ${oktaSid}`);
            await fs.writeFile("saml-sid.txt", oktaSid);

            // Third.3 - We need to parse SAMLResponse out of XML response
            xmlOneLine = response.data.toString().replace(/[\n\r]+/g, "");
            match = /.*SAMLResponse" type="hidden" value="(.*)"\/>\s+ <input name.*/g.exec(xmlOneLine);
            samlResponseBase64 = he.decode(match[1]);
            samlResponseUriEncoded = encodeURIComponent(samlResponseBase64);
            await fs.writeFile("saml-response.txt", samlResponseUriEncoded);
            console.log('SamlResponse written')
            return true;
        } catch (err) {
            if (err.response && err.response.status === 302) {
                console.log("Warning: Redirect detects on SAML response, this usually means your token has expired");
                return false;
            } else {
                console.log("Error Unknown, stopping");
                console.log(err);
                process.exit(3);
            }
        }
    };

    getOktaLogin = async function (username, password) {
        try {
            response = await axios.post("https://<company>.okta.com/api/v1/authn", {
                username: username,
                password: password,
                options: {
                    multiOptionalFactorEnroll: true,
                    warnBeforePasswordExpired: true,
                },
            });
        } catch (err) {
            console.log("Error authenticating with user/pass");
            console.log(err.response.data);
            process.exit(1);
        }
        let stateToken = response.data.stateToken;
        let mfaVerifyUrl = response.data._embedded.factors[0]._links.verify.href;
        console.log(`StateToken received: ${stateToken}`);
        console.log(`MFA Verify URL: ${mfaVerifyUrl}`);
        return { stateToken, mfaVerifyUrl }
    };

    getMfaVerify = async function (stateToken, mfaToken, mfaVerifyUrl) {
        // Second we need to perform MFA Verify
        try {
            response = await axios.post(mfaVerifyUrl, {
                stateToken: stateToken,
                passCode: mfaToken,
            });
        } catch (err) {
            console.log("Error verifying MFA, did you reuse a token?");
            console.log(response.data);
            process.exit(2);
        }
        sessionToken = response.data.sessionToken;
        console.log(`SessionToken: ${sessionToken}`);
        await fs.writeFile("saml-sessionToken.txt", sessionToken);
        return sessionToken;
    };

    //////////////////////////////////////////////////////////////////////
    //////////////////         M  A  I  N         ////////////////////////
    //////////////////////////////////////////////////////////////////////
    credsCheck = await checkExistingCredentials();
    if (credsCheck) {
        console.log("Existing Credentials are still valid, SAML Response written to file.");
        process.exit(0);
    }
    
    // We need to prompt the user for the MFA Token
    prompt.start()
    const { mfaToken } = await prompt.get(properties)

    const { stateToken, mfaVerifyUrl } = await getOktaLogin(username, password);
    sessionToken = await getMfaVerify(stateToken, mfaToken, mfaVerifyUrl);
    credCheck = await getSamlResponse(sessionToken, false);
    if (credCheck) {
        console.log("Credentials appear valid, SAML Response written to file.");
        process.exit(0);
    }
})();

AUTH: Received control message: AUTH_FAILED,Invalid username or password

Thanks so much for this amazing deep dive. I've learned a lot about what the AWS VPN is doing under the covers. I've tried getting this to work on OSX (as a test to then work on linux) and I can get openvpn compiled and can get all the way through the saml handshake but no matter what I try I simply can not get it to fully auth.

I have tried both a udp and tcp VPC endpoint and various other things I can think of. As well as modifying the server.go to not url.QueryEscape() wondering if this is something that would change things. No matter what I try I can't seem to get past the auth failed.

Below is my config.

client
dev tun
proto tcp
resolv-retry infinite
nobind
remote-cert-tls server
cipher AES-256-GCM
verb 3
<ca>
--TRIMMED--
</ca>

auth-nocache
reneg-sec 0

I recognize this is a bit of a "see anything wrong" with not a lot of details request, so can understand if there is not you can do from this. I feel like I am "so close" on getting this working and it must just be something simple I'm missing. Below is the output of my connection attempts

Mon Mar 29 16:06:53 2021 WARNING: file '/dev/fd/63' is group or others accessible
Mon Mar 29 16:06:53 2021 OpenVPN 2.4.9 x86_64-apple-darwin19.6.0 [SSL (OpenSSL)] [LZO] [LZ4] [PKCS11] [MH/RECVDA] [AEAD] built on Mar 26 2021
Mon Mar 29 16:06:53 2021 library versions: OpenSSL 1.1.1j  16 Feb 2021, LZO 2.10
Mon Mar 29 16:06:53 2021 NOTE: the current --script-security setting may allow this configuration to call user-defined scripts
Mon Mar 29 16:06:53 2021 TCP/UDP: Preserving recently used remote address: [AF_INET]<redacted>:443
Mon Mar 29 16:06:53 2021 Socket Buffers: R=[131072->131072] S=[131072->131072]
Mon Mar 29 16:06:53 2021 Attempting to establish TCP connection with [AF_INET]<redacted>:443 [nonblock]
Mon Mar 29 16:06:54 2021 TCP connection established with [AF_INET]<redacted>:443
Mon Mar 29 16:06:54 2021 TCP_CLIENT link local: (not bound)
Mon Mar 29 16:06:54 2021 TCP_CLIENT link remote: [AF_INET]<redacted>:443
Mon Mar 29 16:06:54 2021 TLS: Initial packet from [AF_INET]<redacted>:443, sid=8cb8040f 77016deb
Mon Mar 29 16:06:54 2021 VERIFY OK: depth=3, C=US, ST=Arizona, L=Scottsdale, O=Starfield Technologies, Inc., CN=Starfield Services Root Certificate Authority - G2
Mon Mar 29 16:06:54 2021 VERIFY OK: depth=2, C=US, O=Amazon, CN=Amazon Root CA 1
Mon Mar 29 16:06:54 2021 VERIFY OK: depth=1, C=US, O=Amazon, OU=Server CA 1B, CN=Amazon
Mon Mar 29 16:06:54 2021 VERIFY KU OK
Mon Mar 29 16:06:54 2021 Validating certificate extended key usage
Mon Mar 29 16:06:54 2021 ++ Certificate has EKU (str) TLS Web Server Authentication, expects TLS Web Server Authentication
Mon Mar 29 16:06:54 2021 VERIFY EKU OK
Mon Mar 29 16:06:54 2021 VERIFY OK: depth=0, CN=*.scpsbx.net
Mon Mar 29 16:06:54 2021 Control Channel: TLSv1.2, cipher TLSv1.2 ECDHE-RSA-AES256-GCM-SHA384, 2048 bit RSA
Mon Mar 29 16:06:54 2021 [*.scpsbx.net] Peer Connection Initiated with [AF_INET]<redacted>:443
Mon Mar 29 16:06:56 2021 SENT CONTROL [*.scpsbx.net]: 'PUSH_REQUEST' (status=1)
Mon Mar 29 16:06:56 2021 AUTH: Received control message: AUTH_FAILED,Invalid username or password
Mon Mar 29 16:06:56 2021 SIGTERM[soft,auth-failure] received, process exiting

The only difference I see here versus in the native AWS client logs is that there are two PUSH_REQUEST messages in the native log.

If there is anything that jumps out at you I'd be grateful for the assist. But I also realize this is not a lot to go on and so if nothing jumps out that's ok and appreciate at least taking the time to scan the details.

Thanks!

RFC: Improvements to the script & go server

TL;DR

I made some improvements that I'd be happy to backport to this repository if there's interest. Please let me know which pieces are valued and I'll get to work.

Take a look at
https://github.com/AnilRedshift/awsvpn-saml/blob/main/main.go
and https://github.com/AnilRedshift/awsvpn-saml/blob/main/awsvpn
Usage of my codebase: Install (on arch) with makepkg -i . Then, just run awsvpn configure to set your variables, and then run awsvpn to run the vpn

Prefer the existing environment variables

Basically anyone who uses the current aws-connect.sh has to modify the first few lines to set the environment variables. You could make this useable without any code changes with something like this:

VPN_CONF="${VPN_CONF:=$HOME/aws.ovpn}"
VPN_PORT="${VPN_PORT:=443}"
VPN_PROTO="${VPN_PROTO:=udp}"

which only overrides the values if they're not already set on the command line.

Use a configuration file

My script models after the aws-cli binary and has a awsvpn configure option. It creates a file (awsvpn.env) which it then loads.

if [ -f "$config_file" ]; then
  echo "sourcing $config_file"
  export $(egrep -v '^#' "$config_file" | xargs)
else
  echo "No env file found"
fi

Start/stop the go server from within the script

Instead of requiring the user to manually start/stop the go server, I'm doing it automatically:

coproc "$bin_dir/awsvpnserver"
srv_pid=$!
exec 3>&"${COPROC[0]}"
clean_up () {
  arg=$?
  echo "Stopping the server for SAML authentication ($srv_pid)"
  kill -9 "$srv_pid" > /dev/null 2>&1 || true
  exit "$arg"
}

Use stdout instead of SAMLresponse.txt to capture the saml token.

I modified the go server to write to stdout fmt.Println(saml) instead of writing to a file. Then, using coproc, I just read that directly from the script, instead of having this file lying around

Stop the go server once authenticated

Pretty simple idea - this doesn't need to be a long-lived process, so I nuke it once the connection succeeds:

http.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
		if saml := r.FormValue("SAMLResponse"); saml != "" {
			rw.WriteHeader(http.StatusOK)
			rw.Write([]byte("succeeded"))
			go func() {
				time.Sleep(time.Second * 5) // Let the POST request return to the caller successfully before returning the SAML token
				fmt.Println(saml)
				ctx, onComplete := context.WithTimeout(context.Background(), time.Second*5)
				srv.Shutdown(ctx)
				onComplete()
			}()
		} else {
			rw.WriteHeader(http.StatusBadRequest)
		}
	})

Fix quoting in the bash script

This line is bugged:

sudo bash -c "$OVPN_BIN --config "${OVPN_CONF}" \
    --verb 3 --auth-nocache --inactive 3600 \
    --proto "$PROTO" --remote $SRV $PORT \
    --script-security 2 \
    --route-up '/usr/bin/env rm saml-response.txt' \
    --auth-user-pass <( printf \"%s\n%s\n\" \"N/A\" \"CRV1::${VPN_SID}::$(cat saml-response.txt)\" )"

Because you have sudo bash -c "" (with the quotes around the bash command), you need to escape every quote within the script. OVPN_CONF and PROTO are not escaped, causing them to be literals.

Remove broken openvpn tests

The aws patch breaks the openvpn test suite (because it's a dumb hack).
This patch removes the broken tests, so you can run make check on openvpn and verify the other behavior works:
https://github.com/AnilRedshift/awsvpn-saml/blob/main/skip-broken-tests-2.5.1.patch

Passthrough remaining arguments to openvpn

Added `"$@"' to the openvpn binary to pass through any custom arguments via the script

Failing to apply patch

Hi, apologies if this is something I'm doing wrong, still quite new to nix and flakes.
I tried to add this to my system, but it seems to be failing with the following:

Hunk #2 FAILED at 2581.
1 out of 2 hunks FAILED -- saving rejects to file src/openvpn/manage.c.rej
patching file src/openvpn/manage.h
patching file src/openvpn/misc.h
Hunk #1 succeeded at 72 (offset 3 lines).
patching file src/openvpn/options.h
patching file src/openvpn/ssl.c
Hunk #1 succeeded at 2146 (offset 36 lines).
Hunk #2 succeeded at 2161 (offset 36 lines).
Hunk #3 succeeded at 2438 (offset 35 lines).

I cloned the repo and ran nix build . and got the same thing

[FYI] AWS provides modified source code for openvpn

Hi,
just wanted to say that you can access modified source code by going to the About menu in the application:
Capture

here is the link: https://amazon-source-code-downloads.s3.amazonaws.com/aws/clientvpn/wpf-v1.2.0/openvpn-2.4.5-aws-1.tar.gz

I got this patch by comparing with the upstream openvpn 2.4.5 version (modulo the travis/git/ide related files):

--- upstream/configure.ac	2020-09-24 10:41:50.783784639 +0100
+++ aws/configure.ac	2020-04-23 00:17:22.588339119 +0100
@@ -1310,9 +1310,10 @@ if test "${enable_werror}" = "yes"; then
 	CFLAGS="${CFLAGS} -Werror"
 fi
 
-if test "${WIN32}" = "yes"; then
-	test -z "${MAN2HTML}" && AC_MSG_ERROR([man2html is required for win32])
-fi
+# Disable the check, as it is only required when PKCS is enabled.
+#if test "${WIN32}" = "yes"; then
+#	test -z "${MAN2HTML}" && AC_MSG_ERROR([man2html is required for win32])
+#fi
 
 if test "${enable_plugin_auth_pam}" = "yes"; then
 	PLUGIN_AUTH_PAM_CFLAGS="${LIBPAM_CFLAGS}"
diff -pruN -x.git -x.gitignore -x.mailmap -x.gitmodules -x.travis -x.travis.yml -x.svncommitters upstream/src/openvpn/buffer.h aws/src/openvpn/buffer.h
--- upstream/src/openvpn/buffer.h	2020-09-24 10:41:50.787784643 +0100
+++ aws/src/openvpn/buffer.h	2020-04-23 00:17:22.629163776 +0100
@@ -27,7 +27,7 @@
 #include "basic.h"
 #include "error.h"
 
-#define BUF_SIZE_MAX 1000000
+#define BUF_SIZE_MAX 1 << 21
 
 /*
  * Define verify_align function, otherwise
diff -pruN -x.git -x.gitignore -x.mailmap -x.gitmodules -x.travis -x.travis.yml -x.svncommitters upstream/src/openvpn/common.h aws/src/openvpn/common.h
--- upstream/src/openvpn/common.h	2020-09-24 10:41:50.787784643 +0100
+++ aws/src/openvpn/common.h	2020-04-23 00:17:22.630187533 +0100
@@ -77,7 +77,7 @@ typedef unsigned long ptr_type;
  * maximum size of a single TLS message (cleartext).
  * This parameter must be >= PUSH_BUNDLE_SIZE
  */
-#define TLS_CHANNEL_BUF_SIZE 2048
+#define TLS_CHANNEL_BUF_SIZE 1 << 18
 
 /*
  * This parameter controls the maximum size of a bundle
diff -pruN -x.git -x.gitignore -x.mailmap -x.gitmodules -x.travis -x.travis.yml -x.svncommitters upstream/src/openvpn/error.h aws/src/openvpn/error.h
--- upstream/src/openvpn/error.h	2020-09-24 10:40:23.155700218 +0100
+++ aws/src/openvpn/error.h	2020-04-23 00:17:22.636651409 +0100
@@ -36,7 +36,10 @@
 #ifdef ENABLE_PKCS11
 #define ERR_BUF_SIZE 8192
 #else
-#define ERR_BUF_SIZE 1280
+/*
+ * Increase the error buffer size to 256 KB.
+ */
+#define ERR_BUF_SIZE 1 << 18
 #endif
 
 struct gc_arena;
diff -pruN -x.git -x.gitignore -x.mailmap -x.gitmodules -x.travis -x.travis.yml -x.svncommitters upstream/src/openvpn/manage.c aws/src/openvpn/manage.c
--- upstream/src/openvpn/manage.c	2020-09-24 10:41:50.791784647 +0100
+++ aws/src/openvpn/manage.c	2020-04-23 00:17:22.644763448 +0100
@@ -2159,7 +2159,7 @@ man_read(struct management *man)
     /*
      * read command line from socket
      */
-    unsigned char buf[256];
+    unsigned char buf[MANAGEMENT_SOCKET_READ_BUFFER_SIZE];
     int len = 0;
 
 #ifdef TARGET_ANDROID
@@ -2499,7 +2499,7 @@ man_connection_init(struct management *m
          * Allocate helper objects for command line input and
          * command output from/to the socket.
          */
-        man->connection.in = command_line_new(1024);
+        man->connection.in = command_line_new(COMMAND_LINE_OPTION_BUFFER_SIZE);
         man->connection.out = buffer_list_new(0);
 
         /*
diff -pruN -x.git -x.gitignore -x.mailmap -x.gitmodules -x.travis -x.travis.yml -x.svncommitters upstream/src/openvpn/manage.h aws/src/openvpn/manage.h
--- upstream/src/openvpn/manage.h	2020-09-24 10:41:50.791784647 +0100
+++ aws/src/openvpn/manage.h	2020-04-23 00:17:22.645034771 +0100
@@ -37,6 +37,9 @@
 #define MANAGEMENT_ECHO_BUFFER_SIZE           100
 #define MANAGEMENT_STATE_BUFFER_SIZE          100
 
+#define COMMAND_LINE_OPTION_BUFFER_SIZE OPTION_PARM_SIZE
+#define MANAGEMENT_SOCKET_READ_BUFFER_SIZE OPTION_PARM_SIZE
+
 /*
  * Management-interface-based deferred authentication
  */
diff -pruN -x.git -x.gitignore -x.mailmap -x.gitmodules -x.travis -x.travis.yml -x.svncommitters upstream/src/openvpn/misc.h aws/src/openvpn/misc.h
--- upstream/src/openvpn/misc.h	2020-09-24 10:41:50.795784650 +0100
+++ aws/src/openvpn/misc.h	2020-04-23 00:17:22.646430770 +0100
@@ -180,7 +180,10 @@ struct user_pass
 #ifdef ENABLE_PKCS11
 #define USER_PASS_LEN 4096
 #else
-#define USER_PASS_LEN 128
+/*
+ * Increase the username and password length size to 128KB.
+ */
+#define USER_PASS_LEN 1 << 17
 #endif
     char username[USER_PASS_LEN];
     char password[USER_PASS_LEN];
diff -pruN -x.git -x.gitignore -x.mailmap -x.gitmodules -x.travis -x.travis.yml -x.svncommitters upstream/src/openvpn/options.h aws/src/openvpn/options.h
--- upstream/src/openvpn/options.h	2020-09-24 10:41:50.799784654 +0100
+++ aws/src/openvpn/options.h	2020-04-23 00:17:22.656371156 +0100
@@ -55,8 +55,8 @@
 /*
  * Max size of options line and parameter.
  */
-#define OPTION_PARM_SIZE 256
-#define OPTION_LINE_SIZE 256
+#define OPTION_PARM_SIZE USER_PASS_LEN
+#define OPTION_LINE_SIZE OPTION_PARM_SIZE
 
 extern const char title_string[];
 
diff -pruN -x.git -x.gitignore -x.mailmap -x.gitmodules -x.travis -x.travis.yml -x.svncommitters upstream/src/openvpn/ssl.c aws/src/openvpn/ssl.c
--- upstream/src/openvpn/ssl.c	2020-09-24 10:41:50.803784658 +0100
+++ aws/src/openvpn/ssl.c	2020-04-23 00:17:22.674291148 +0100
@@ -2144,7 +2144,7 @@ write_string(struct buffer *buf, const c
     {
         return false;
     }
-    if (!buf_write_u16(buf, len))
+    if (!buf_write_u32(buf, len))
     {
         return false;
     }
@@ -2440,6 +2440,10 @@ key_method_2_write(struct buffer *buf, s
         }
     }
 
+    // Write key length in the first 4 octets of the buffer.
+    uint32_t length = BLEN(buf);
+    memcpy(buf->data, &length, sizeof(length));
+
     return true;
 
 error:

rootless socks5 tunnel, openvpn 2.6

I created an openvpn fork - https://github.com/bendlas/openvpn-tuna - which has patches from this repository, rebased on openvpn's release/2.6 branch, and on top of code from openvpn-tunpipe.

With this I can have openvpn run a userland proxy, like ocproxy or tunsocks, without any elevated privileges at all.

Besides the patches, I took from this repo the server.go, as well as the basis for the connection script.

@samm-git thank you very much for your fantastic work. If you would like anything from openvpn-tuna, I'd be happy to prepare PRs and start treating this as upstream.

  • rebased patches (release-2.6 as of writing)
  • user proxy support
  • nix flake

Linux: transient authentication failures

Hi, I got this working on Linux!

However, I'm running into a problem where authentication will fail what seems like somewhere between 30 and 50% of the time, with no changes at all between retries:

AUTH: Received control message: AUTH_FAILED,Invalid username or password

I'm not familiar enough with the internals of the authentication scheme to know what would cause this. Given the transient nature though, I wonder if maybe the message is being truncated or improperly formatted at some point in the process.

I realize that this issue description isn't much to go on, so if you have any suggestions of other information I could provide, or techniques I could use to narrow down the source of the problem, let me know.

Rust based client

Hi! I was looking for alternate implementations of the aws vpn client, I came across this repo and another one that is based on this implementation.

https://github.com/JonathanxD/openaws-vpn-client

I thought I'd mention it since they refer back to your thoughtful reverse engineering.

Thanks again for your trouble.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.