2005 2006 2007 2008 2009 2010 2011 2015 2016 aspnet azure csharp debugging exceptions firefox javascriptajax linux llblgen powershell projects python security services silverlight training videos wcf wpf xag xhtmlcss

Running with Nginx

Stacks don't exist. As soon as you change your database you're no longer LAMP or MEAN. Drop the term. Even then, term only applies to technology; it doesn't describe you. If you are a "Windows guy", learn Linux. If you are a "LAMP" dude, you have to at least have some clue about .NET. Don't marry yourself to only AWS or Azure. Learn both. Use both. Some features of Azure make me drool, while others remind me of VB6. Some features of AWS make me feel like a kid in a candy store, while others make me wonder if they are actually April Fool's jokes.

Regardless of whatever you're into, you really should learn this epic tool called Nginx. I've been using it for a while and now have almost all my web sites touching it in some way.

So, what is it? The marketing says it's a "reverse proxy". Normally I mock marketing nonsense, but this description is actually pretty good. You could call Nginx a web server, but that misses the point. It can act as a web server, and I'm sure some millennial somewhere is saying "LOL!! IT SERVZ WEB CONTENTZ! LOL!!" because it can "serve files", but it's mainly a reverse proxy.

Adding SSL and Authentication

I'm going to start off with a classic example: adding SSL and username / password authentication to an existing web API.

Elasticsearch is one of my favorite database systems; yet, it doesn't have native support for SSL or authorization. There's a tool called Shield for that, but it's over kill when I don't care about multiple users. Nginx came to the rescue. Below is my basic Nginx config. You should be able to look at the following config to get an idea of what's going on. Of course, I'll add some commentary.

server {
    listen 10.1.60.3;

    auth_basic "ElasticSearch";
    auth_basic_user_file /etc/nginx/es-password;

    location / {
        proxy_pass http://127.0.0.1:9200;
        proxy_http_version 1.1;
        proxy_set_header Connection "Keep-Alive";
        proxy_set_header Proxy-Connection "Keep-Alive";
    }
}

In this example, I have a listener setup to listen on port 443. In the context of this listener, I'm setting configuration for /. I'm passing all traffic on to port 9200. This port is only bound locally, so HTTP isn't even publicly accessible. You can also see I'm setting some optional headers.

443 is SSL, so I have my SSL cert and SSL key configured (in my real config, there's a lot more SSL config; just stuff to configure the ciphers).

Finally, you can see that I've setup basic user authentication. Prior to creating this config I used the Apache htaccess command to create a password file:

sudo htpasswd -c /srv/es-htpasswd searchuser

If you stare at the config enough, it will be demystified. Nginx is simply adding SSL and username/password auth to an existing working, open HTTP-only server.

SSL Redirect

Let's lighten up a bit with a simpler example...

There are myriad ways to redirect from HTTP to HTTPS. Nginx is my new favorite way:

server {
    listen 222.222.222.222:80;

    server_name mydomain.net;
    server_name www.mydomain.net;

    return 301 https://mydomain.net$request_uri;
}

Accessing localhost only services

The other day I needed to download some files from my Google Drive to my Linux Server. rclone seemed to be an OK way to do that. During setup, it wanted me to go through the OpenID/OAuth stuff to give it access. Good stuff, but...

If your browser doesn't open automatically go to the following link: http://127.0.0.1:53682/auth
Log in and authorize rclone for access
Waiting for code...
Got code

Uhh... 127.0.0.1? Dude, that's a remote server. I tried to go there with the text-based Lynx browser, but, wow... THAT. WAS. HORRIBLE. Then I had a random realization: Nginx! Here's what I did real quick:

server {
    listen 111.111.111.111:8080;

    location / {
        proxy_pass http://127.0.0.1:53682;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host 127.0.0.1;
    }
}

Then I could access the server in my own browser using http://111.111.111.111:53682/auth.

BOOM! I got the Google authorization screen right away and everything came together.

Making services local only

This brings up an intersting point: what if you had a public service you didn't want to be public, but didn't have a way to do it-- or, perhaps, you just wanted to change the port?

In a situation where I had to cheat, I'd cheat by telling iptables (Linux firewall) to block that port, then use Nginx to open the new one.

For example:

iptables -A INPUT -i lo -j ACCEPT
iptables -A INPUT -p tcp --dport 8080 -j ACCEPT
iptables -A INPUT -j DROP

This says: allow localhost and stuff to port 8080, but block everything else.

If you do this, you need to save the rules using something like iptables-save > /etc/iptables/rules.v4. On Ubuntu, you can get this via apt-get install iptables-persistent.

Then, you can do something like the previous Nginx example to take traffic from a differnet port.

File Serving

My new architecture for my websites involves a few components: public Azure Blob Storage for my assets, ASP.NET WebAPI for all backend processing, and Python/Django for all my websites (+Elasticsearch for queries and Redis is always preloaded with a full mirror of my database) . My netfxharmonics.com follows this exact architecture. I don't like my websites existing in the same world as anything that serves content to it. The architecture I've promoted for years finally has a name: microservices (thank goodness for a non-lame name! *cough* AJAX *cough*) I take a clean architectural approach: no assets on my website, no database access on my website, and no backend processing on my website. My websites only displays content. Databases (thus all connection strings) are behind my WebAPI wall.

OK... I said no assets. That's not completely true, which brings us to the point: how do I serve robots.txt and favicon.ico if I don't allow local assets? Answer: Nginx.

location /robots.txt {
    alias /srv/robots.txt;
}

location /favicon.ico {
    alias /srv/netfx/netfxdjango/static/favicon.ico;
}

Azure

So, you've got a free/shared Azure Web App. You've got you free hosting, free subdomain, and even free SSL. Now you want your own domain and your own SSL. What do you do? Throw money at it? Uh... no. Well, assuming you were proactive and keep a Linux server around.

This is actually a true story of how I run some of my websites. You only get so much free bandwidth and computing with the free Azure Web Apps, so you have to be careful. The trick to being careful is Varnish.

The marketing for Varnish says it's a caching server. As with all marketing, they're trying to make something sound less cool than it really it (though that's never their goal). Varnish can be a load-balancer or something to handle fail-over as well. In this case, yeah, it's a caching server.

Basically: I tell Varnish to listen to port 8080 on localhost. It will take traffic and provide responses. If it needs something, it will go back to the source server to get the content. Most hits to the server will be handled with Varnish. Azure breathe easy.

Because the Varnish config is rather verbose and because it's only tangentially related to this topic, I really don't want to dump a huge Varnish config here. So, I'll give snippets:

backend mydomain {
    .host = "mydomain.azurewebsites.net";
    .port = "80";
    .probe = {
         .interval = 300s;
         .timeout = 60 s;
         .window = 5;
         .threshold = 3;
    }
  .connect_timeout = 50s;
  .first_byte_timeout = 100s;
  .between_bytes_timeout = 100s;
}

sub vcl_recv {
    #++ more here
    if (req.http.host == "123.123.123.123" || req.http.host == "www.mydomain.net" || req.http.host == "mydomain.net") {
        set req.http.host = "mydomain.azurewebsites.net";
        set req.backend = mydomain;
        return (lookup);
    }
    #++ more here
}

This won't make much sense without the Nginx peice:

server {
        listen 123.123.123.123:443 ssl;

        server_name mydomain.net;
        server_name www.mydomain.net;
        ssl_certificate /srv/cert/mydomain.net.crt;
        ssl_certificate_key /srv/cert/mydomain.net.key;

        location / {
            proxy_pass http://127.0.0.1:8080;
            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 https;
            proxy_set_header X-Forwarded-Port 443;
            proxy_set_header Host mydomain.azurewebsites.net;
        }
}

Here's what to look for in this:

proxy_set_header Host mydomain.azurewebsites.net

Nginx sets up a listener for SSL on the public IP. It will send requests to localhost:8080.

On the way, it will make sure the Host header says "mydomain.azurewebsites.net". This does two things:

* First, Varnish will be able to detect that and send it to the proper backend configuration (above it).

* Second, Azure will give you a website based on the `Host` header. That needs to be right. That one line is the difference between getting your correct website or getting the standard Azure website template.

In this example, Varnish is checking the host because Varnish is handling multiple IP addresses, multiple hosts, and caching for multiple Azure websites. If you have only one, then these Varnish checks are superfluous.

Verb Filter

Back to Elasticsearch...

It uses various HTTP verbs to get the job done. You can POST, PUT, and to insert, update, or delete respectively, or you can use GET to do your searches. How about a security model where I only allow searches?

It might be a poorman's method, but it works:

server {
    listen 222.222.222.222:80;

    location / {
        limit_except GET {
            deny all;
        }
        proxy_pass http://127.0.0.1:9200;
        proxy_http_version 1.1;
        proxy_set_header Connection "Keep-Alive";
        proxy_set_header Proxy-Connection "Keep-Alive";
    }
}

Verb Filter (advanced)

When using Elasticsearch, you have the option of accessing your data directly without the need for a server-side anything. In face, your AngularJS (or whatever) applications can get data directly from ES. How? It's just an HTTP endpoint.

But, what about updating data? Surely you need some type of .NET/Python bridge to handle security, right? Nah.

Checkout the following location blocks:

location ~ /_count {
    proxy_pass http://elastic;
    proxy_http_version 1.1;
    proxy_set_header Connection "Keep-Alive";
    proxy_set_header Proxy-Connection "Keep-Alive";
}

location ~ /_search {
    proxy_pass http://elastic;
    proxy_http_version 1.1;
    proxy_set_header Connection "Keep-Alive";
    proxy_set_header Proxy-Connection "Keep-Alive";
}

location ~ /_ {
    limit_except OPTIONS {
        auth_basic "Restricted Access";
        auth_basic_user_file /srv/es-password;
    }

    proxy_pass http://elastic;
    proxy_http_version 1.1;
    proxy_set_header Connection "Keep-Alive";
    proxy_set_header Proxy-Connection "Keep-Alive";
}

location / {
    limit_except GET HEAD {
        auth_basic "Restricted Access";
        auth_basic_user_file /srv/es-password;
    }

    proxy_pass http://elastic;
    proxy_http_version 1.1;
    proxy_set_header Connection "Keep-Alive";
    proxy_set_header Proxy-Connection "Keep-Alive";
}

Here I'm saying: you can access anything with _count (this is how you get counts from ES), and anything with _search (this is how you query), but if you are accessing something else containing an underscore, you need to provide creds (unless it's an OPTION, which allows CORS to work). Finally, if you're accessing / directly, you can send GET and HEAD, but you need creds to do a POST, PUT, or DELETE.

You can add credential handling to your AngularJS/JavaScript application by sending creds via https://username:password@mydomain.net.

It works fine. Now you can throw away all your server-side code an stick with raw AngularJS (or whatver). If something requires a preprocessor, postprocessor, or server-side code at all (e.g. couldn't be developed in jdfiddle/plunkr directly), it's not web development (and you might not be a web developer). Here, you have solid, direct web development without the middle-man. Just the browser and the server-infrastructure. It's SPA with your own IAAS setup.

Killing 1990s "www."

Nobody types "www.", it's never on business cards, nobody says it, and most people forgot it exists. Why? This isn't 1997. The most important part of getting a pretty URL is removing this nonsense. Nginx to the rescue:

server {
    listen 222.222.222.222:80

    server_name mydomain.net;
    server_name www.mydomain.net;

    return 301 https://mydomain.net$request_uri;
}

server {
    listen 222.222.222.222:443 ssl http2;

    server_name www.mydomain.net;

    # ... ssl stuff here ...

    return 301 https://mydomain.net$request_uri;
}

server {
    listen 222.222.222.222:443 ssl http2;

    server_name mydomain.net;

    # ... handle here ...
}

All three server blocks listen on the same IP, but the first listens on port 80 to redirect to the actual domain (there's no such thing as a "naked domain"-- it's just the domain; "www." is a subdomain), the second listens for the "www." subdomain on the HTTPS port (in this case using HTTP2), and the third is where everyone is being directed.

SSL

This example simply expands the previous one by showing the actual SSL implemenation. Keep in mind that to use HTTP2, you have to have at least Nginx 1.9 (at the time of writing, this meant compiling it yourself-- not a big deal).

server {
    listen 222.222.222.222:80;

    server_name mydomain.net;
    server_name www.mydomain.net;

    return 301 https://mydomain.net$request_uri;
}

server {
    listen 222.222.222.222:443 ssl http2;

    server_name www.mydomain.net;

    ssl_certificate /srv/_cert/mydomain/mydomain.net.chained.crt;
    ssl_certificate_key /srv/_cert/mydomain/mydomain.net.key;

    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

    ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';

    ssl_prefer_server_ciphers on;

    ssl_dhparam /srv/_cert/dhparam.pem;

    return 301 https://mydomain.net$request_uri;
}

server {
    listen 222.222.222.222:443 ssl http2;

    server_name mydomain.net;

    ssl_certificate /srv/_cert/mydomain/mydomain.net.chained.crt;
    ssl_certificate_key /srv/_cert/mydomain/mydomain.net.key;

    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

    ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';

    ssl_prefer_server_ciphers on;

    ssl_dhparam /srv/_cert/dhparam.pem;

    location / {
        add_header Strict-Transport-Security max-age=15552000;
        add_header Content-Security-Policy "default-src 'none'; font-src fonts.gstatic.com; frame-src accounts.google.com apis.google.com platform.twitter.com; img-src syndication.twitter.com bible.logos.com www.google-analytics.com 'self'; script-src api.reftagger.com apis.google.com platform.twitter.com 'self' 'unsafe-eval' 'unsafe-inline' www.google.com www.google-analytics.com; style-src fonts.googleapis.com 'self' 'unsafe-inline' www.google.com ajax.googleapis.com; connect-src search.jampad.net jampadcdn.blob.core.windows.net mydomain.net";

        include         uwsgi_params;
        uwsgi_pass      unix:///srv/mydomain/mydomaindjango/content.sock;

        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 https;
        proxy_set_header X-Forwarded-Port 443;
        proxy_set_header Host mydomain.net;
    }
}

The certs that I use require the chain certs to get a solid A rating on ssllabs.com, this is just a matter of merging your cert with the chain cert (just Google it):

cat mydomain.net.crt ../positivessl.ca-bundle > mydomain.net.chained.crt

Verb Routing

Speaking of verbs, you could whip out a pretty cool CQRS infrastructure by splitting GET from POST.

This is more of a play-along than a visual-aide. You can actually try this one at home.

Here's a demo using a quick node server:

http = require('http');
port = parseInt(process.argv[2]);
server = http.createServer( function(req, res) {
    res.writeHead(200, {'Content-Type': 'text/html'});
    res.end(req.method + ' server ' + port);
});
host = '127.0.0.1';
server.listen(port, host);

Here's our nginx config:

server {
    listen 222.222.222.222:8192;

    location / {
        limit_except GET {
            proxy_pass http://127.0.0.1:6001;
        }
        proxy_pass http://127.0.0.1:6002;
    }
}

use nginx -s reload to quickly reload config without doing a full service restart

Now, to spin up two of them:

node server.js 6001 &
node server.js 6002 &

& runs something as a background process

Now to call them (PowerShell and curl examples provided)...

(wget -method Post http://192.157.251.122:8192/).content

curl -XPOST http://192.157.251.122:8192/

Output:

POST server 6001
(wget -method Get http://192.157.251.122:8192/).content

curl -XGET http://192.157.251.122:8192/

Output:

GET server 6002

Cancel background tasks with fg then CTRL-C. Do this twice to kill both servers.

There we go, your inserts go to one location you read from a different one.

Python / Django

Another great thing about Nginx is that it's not Apache. Aside from Apache simply trying to do far too much, it's an obsolete product from the 90s that needs to be dropped. Even then, using Apache for your Python/Django development isn't pleasant, that's what the runserver is for.

If you're using runserver for development and Apache on production, you have two completely different hosting systems. Nginx allows you to have the hosting setup in both places. The idea is this: in dev, you'll use runserver and in prod, you'll use uwsgi. But, isn't that still two different setups? Not really, because these are the things that run the Python content, Nginx will be what serves the content in both locations. Thus, you not only have a unified hosting model, but you have Python processing done by a proper python processer and HTTP traffic handled by Nginx.

Raw Python HTTP Processing

Python HTTP processing (it's not "web development" unless there's a web-browser) is all about WSGI: web software gateway interface. It's a pointless term, but the implementation is beautiful: it's a single interface that handles everything web-related for Python. The signature is as follows (with an example):

def name_does_not_matter(environment, response_code):
    response_code = '200 OK'
    return 'Your content type was {}'.format(environment['CONTENT_TYPE'])

This is even what Django does deep down.

You can use a service like UWSGI to do the processing for this. Like other things in Linux, this tool does one thing, does it well, and relies on other tools for other things. In the case of hosting, Nginx is a solid way to handle the HTTP hosting for UWSGI.

In addition to the config for UWSGI (not shown-- not relevant!), you have the following Nginx config:

server {
    listen 222.222.222.222:80;

    location / {
        include            uwsgi_params;
        uwsgi_pass         unix:/srv/raw_python_awesomeness/content/content.sock;

        proxy_redirect     off;
        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-Host $server_name;
    }
}

You could make UWSGI serve up something on localhost:8081 (or whatever port you want), but it's best to use sockets where you can.

You can see my WebAPI for Python project at https://github.com/davidbetz/webapipy for a fuller example.

Comments

Well, OK... there is no comment section. It's not that I don't care what you think, but we all know that one guy in the office who feels the need to peek over the cubicle to contribute unsolicited "help" every time someone says anything about anything and who who feels the need to express himself every time something looks different and gets offended anytime someone expresses discomfort at his constant intrusion and who thinks all his "interesting traits" makes him a "team player". I don't care what HE thinks. He's not welcome here.

Follow me on Twitter instead. Comment there. @netfxharmonics

Powered by
Python / Django / Elasticsearch / Azure / Nginx / CentOS 7

Mini-icons are part of the Silk Icons set of icons at famfamfam.com