How to secure a connection to the GoPiGo-3 robot

Greetings!

(Inviting @cleoqc and @mitch.kremm to see this.)

Because of, (ahem!), “advances” in browser security, it may be necessary to present an outward-facing secure connection from the robot to the rest of the network.

  • In my case, in order to use the gamepad interface within the browser, the connection must be secure.

  • Though Flask/Werkzeug can be made secure relatively easily, other servers like streamserver cannot be made secure.

  • If you have multiple server services running on the same 'bot, trying to secure all of them at the same time can be a challenge, inviting error.

The answer is a secure “wrapper”, (proxy/reverse-proxy), that will create a network-facing secure connection and pass it through as insecure connections to the internal servers/services.

There are a number of solutions, but many are specific to Flask, and many won’t work with streamserver, which is the video-streaming server for the remote camera robot.

The solution I ultimately chose was nginx.  Nginx is advertised as “the Swiss army knife” of web servers/services and it will do everything except make breakfast.

  1. Advantages:

    • It works.  More important is that it is totally connection agnostic as far as the “upstream”, (inside), connections are concerned.  It can pass through connections either as secure or insecure connections while maintaining a secure outward facing connection.
      • Note that nginx is also famous as a load-balancer, automagic failover manager, etc. etc. etc., but these uses are more appropriate for more involved configurations.
    • Once you understand what it’s doing, the configuration borders on trivial as most of it is “boilerplate” for simple reverse-proxy configurations.
    • Since it acts as a “wrapper”, there is only one point of configuration which means you can leave the rest of the stuff alone.
      • The best way to think of nginx in this configuration is that it is similar to a router doing port-forwarding.
         
  2. Disadvantages:

    • The available literature on-line is abysmal at best and absolutely hideous as a general rule.
      • The typical use cases that are described in the available literature are either abysmally stupid or hideously complex; intended for server farms, e-commerce sites, monster database servers; with load balancing and automatic failover and recovery - etc. etc. etc.
      • They say there’s more than one way to skin a cat.  For nginx that’s very true and the shear number of methods is confusing to say the least.
      • I said “Once you understand what it’s doing, the configuration borders on trivial”, and that’s true.  Unfortunately, figuring it out is not a simple task.
        • The “understand what it’s doing” is the tough part as there is literally too much information out there, contradictory, possibly wrong, and recommending ways of configuring nginx that border on insane.  (i.e. One site actually recommends modifying the base nginx configuration file instead of adding a subsidiary configuration file to the “active servers” directory!  :man_facepalming:)

By and large, once you get it figured out, it’s not that difficult.

I discuss this in detail over on Stack Overflow:

The basic idea is to create a configuration file that directs traffic to the two different servers - though you might have to change the outward port number for them to maintain the incoming port numbers.

This is my annotated configuration file, cleaned up for Stack Overflow.  (they don’t allow “live” domain names in their articles)

Note that I pass through the Flask port as a secure connection and the streamserver port as an insecure connection.  It is important to note that this is not necessary, (and probably too much complexity), but I left it this way as an exercise and proof-of-concept.

Viz.:

# I commented out the server connection redirect for port 80
# as that is already used by the GoPiGo3 interface web page.
#
#server {
#   listen example.com:80;
#   server_name example.com;
#   return 301 https://example.com$request_uri;
# }

# This is the "web" server (command and control), running Flask/Werkzeug
# that must be passed through as a secure connection so that the
# joystick/gamepad works.
#
# Note that the internal Flask server must be configured to use a
# secure connection too. (Actually, that may not be true, but that's
# how I set it up. . .)
#
server {
   listen example.com:443 ssl;
   server_name example.com;
   ssl_certificate  /usr/local/share/ca-certificates/extra/combined.crt;
   ssl_certificate_key  /usr/local/share/ca-certificates/extra/example.com.key; 
   ssl_prefer_server_ciphers on;

   location / {
        proxy_pass https://example.com:5000;

        proxy_set_header        Host $host;
        proxy_set_header        X-Real-IP $remote_addr;
        proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header        X-Forwarded-Proto $scheme;
   }
}

# This is the video streaming port/server running streamserver
# which is not, and cannot be, secured.  However, since most
# modern browsers will not mix insecure and secure content on
# the same page, the outward facing connection must be secure.
#
server {
   listen example.com:5001 ssl;
   server_name example.com;
   ssl_certificate  /usr/local/share/ca-certificates/extra/combined.crt;
   ssl_certificate_key  /usr/local/share/ca-certificates/extra/www.example.com.key; 
   ssl_prefer_server_ciphers on;

# After securing the outward facing connection, pass it through
# as an insecure connection so streamserver doesn't barf.

   location / {
        proxy_pass http://example.com:5002;

        proxy_set_header        Host $host;
        proxy_set_header        X-Real-IP $remote_addr;
        proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header        X-Forwarded-Proto $scheme;
   }
}
1 Like

Update:

I removed the internal requirement for Flask to be secure but left it secure on the outside connection and it still works.

Flask:

class WebServerThread(Thread):
    '''
    Class to make the launch of the flask server non-blocking.
    Also adds shutdown functionality to it.
    '''
    def __init__(self, app, host, port):
        Thread.__init__(self)
        self.srv = make_server(host, port, app)
#        self.srv = make_server(host, port, app, ssl_context=('/usr/local/share/ca-certificates/extra/combined.crt', '/usr/local/share/ca-certificates/extra/www.gopigo3.com.key'))
        self.ctx = app.app_context()
        self.ctx.push()

The commented out line is what has to change to make Flask secure.
Viz.:

self.srv = make_server(host, port, app, ssl_context=('/usr/local/share/ca-certificates/extra/combined.crt', '/usr/local/share/ca-certificates/extra/www.gopigo3.com.key'))

By commenting out that line, and substituting the original line, Flask is returned to its original insecure state.

In nginx, to switch the internal connection to Flask from secure to insecure is just as simple:
Viz.:

server {
   listen gopigo3.com:443 ssl;
   server_name gopigo3.com;
   ssl_certificate  /usr/local/share/ca-certificates/extra/combined.crt;
   ssl_certificate_key  /usr/local/share/ca-certificates/extra/www.gopigo3.com.key; 
   ssl_prefer_server_ciphers on;

   location / {
#        proxy_pass https://gopigo3.com:5000;
        proxy_pass http://gopigo3.com:5000;

As you can see, I commented out the line that tells it to pass it through as a secure request and allowed it to pass through as an insecure request.

Note that the “server” stanza for this particular server connection, (the Flask server), is still listening on 443, still has the “ssl” flag set and still references the credential files.  The result of all this is that - as far as the outside world is concerned - the connection to both the Flask server and the video streamserver connection is a secure one, even though internally they’re not secure.

The bottom line is that by using nginx, you can take any network-facing process server and wrap it in a secure connection.

2 Likes

Additional update:

I tried experimenting with a self-signed certificate, including using the special utility “mkcert” to create a self-signed “trusted certificate authority”, and despite my best efforts, nothing worked.  I still get the strange warnings.

The only way I have found to avoid these warnings is to use a “real” certificate from a “real” trusted CA.

2 Likes