Nginx/Pyramid custom SSL port - nginx

As a prefix, I have been using the following stack for some time with great success:
NGINX - web proxy
SSL - configured in nginx
Pyramid web application, served by gunicorn
The above combo works great, here is a working configuration.
server {
# listen on port 80
listen 80;
server_name portalapi.example.com;
# Forward all traffic to SSL
return 301 https://www.portalapi.example.com$request_uri;
}
server {
# listen on port 80
listen 80;
server_name www.portalapi.example.com;
# Forward all traffic to SSL
return 301 https://www.portalapi.example.com$request_uri;
}
#ssl server
server {
listen 443 ssl;
ssl on;
ssl_certificate /usr/local/etc/letsencrypt/live/portalapi.example.com/fullchain.pem;
ssl_certificate_key /usr/local/etc/letsencrypt/live/portalapi.example.com/privkey.pem;
server_name www.portalapi.example.com;
client_max_body_size 10M;
client_body_buffer_size 128k;
location ~ /.well-known/acme-challenge/ {
root /usr/local/www/nginx/portalapi;
allow all;
}
location / {
proxy_set_header Host $host;
proxy_pass http://10.1.1.16:8005;
#proxy_intercept_errors on;
allow all;
}
error_page 404 500 502 503 504 /index.html;
location = / {
root /home/luke/ecom2/dist;
}
}
Now, this is how I serve my public facing apps, it works very well. For all my internal applications, I used to simply direct users to an internal domain example: http://subdomain.company.domain , again this worked well for a long time.
Now in the wake of KRACK attack although we have some very thorough firewall rules to prevent a lot of attacks, I want to force all internal traffic through SSL, and I don't want to use a self signed certificate, I want to use lets encrypt so I can auto-renew certificates which makes administration much easier (and cheaper).
In order to use lets encrypt, I need to have a public facing DNS and server to perform the ACME challenge (for auto renewing). Now again this was a very easy thing to setup in nginx, and the below config works perfectly for serving static content:
What it does is if a user from the internet accesses intranet.example.com it simply shows a forbidden message. However, if a local user tries, they get forwarded to intranet.example.com:8002 and the port 8002 is only available locally, so there is no way external users can access a webpage on this site
geo $local_user {
192.168.155.0/24 0;
172.16.10.0/28 1;
172.16.155.0/24 1;
}
server {
listen 80;
server_name intranet.example.com;
client_max_body_size 4M;
client_body_buffer_size 128k;
# Space for lets encrypt to perform challenges
location ~ /\.well-known/ {
root /usr/local/www/nginx/intranet;
}
if ($local_user) {
# If user is local, redirect them to SSL proxy only available locally
return 301 https://intranet.example.com:8002$request_uri;
}
# Default block all non local users see
location / {
root /home/luke/forbidden_html;
index index.html;
}
# This server block is only available to local users inside geo $local_user
# this block listens on an internal port only, so it is never availble to
# external networks
server {
listen 8002 default ssl; # listen on a port only accessible locally
server_name intranet.example.com;
ssl_certificate /usr/local/etc/letsencrypt/live/intranet.example.com/fullchain.pem;
ssl_certificate_key /usr/local/etc/letsencrypt/live/intranet.example.com/privkey.pem;
client_max_body_size 4M;
client_body_buffer_size 128k;
location / {
allow 192.168.155.0/24;
allow 172.16.10.0/28; # also add in allow/deny rules in this block (extra security)
allow 172.16.155.0/24;
root /home/luke/ecom2/dist;
index index.html;
deny all;
}
}
Now, here comes the pyramid/nginx marrying problem, if I use the same above configuration, but have the below settings for my server on 8002:
server {
listen 8002 default ssl; # listen on a port only accessible locally
server_name intranet.example.com;
ssl_certificate /usr/local/etc/letsencrypt/live/intranet.example.com/fullchain.pem;
ssl_certificate_key /usr/local/etc/letsencrypt/live/intranet.example.com/privkey.pem;
client_max_body_size 4M;
client_body_buffer_size 128k;
location / {
allow 192.168.155.0/24;
allow 172.16.10.0/28; # also add in allow/deny rules in this block (extra security)
allow 172.16.155.0/24;
# Forward all requests to python application server
proxy_set_header Host $host;
proxy_pass http://10.1.1.16:6543;
proxy_intercept_errors on;
deny all;
}
}
I run into all sorts of problems, first off inside pyramid I was using the following code in my views/templates
request.route_url # get route url for desired function
Now using request.route_url with the above settings should cause https://intranet.example.com:8002/login to route tohttps://intranet.example.com:8002/welcome but in reality, this setup would forward a user to: http://intranet.example.com/welcome Again this is not correct.
And if I use route_url with the NGINX proxy setting:
proxy_set_header Host $http_host;
I get the error: NGINX to return a 400 error:
400: The plain HTTP request was sent to HTTPS port
And a request to: https://intranet.example.com:8002/ gets reverted to: http://intranet.example.com/login (omitting port and https)
Then I used the same nginx settings (header $htto), but thought I would change to using:
request.route_path
My theory was this should force everything to stay on the same url prefix, and just forward a user from https://intranet.example.com:8002/login to https://intranet.example.com:8002/welcome but in reality, this setup performed the same way as using route_url.
proxy_set_header Host $http_host;
I then get an error when navigating to https://intranet.example.com:8002
400: The plain HTTP request was sent to HTTPS port
And a request to: https://intranet.example.com:8002/ gets reverted to: http://intranet.example.com/login (omitting port and https)
Can anyone assist with the correct setup in order for me to serve my application on https://intranet.example.com:8002
EDIT:
Have also tried:
location / {
allow 192.168.155.0/24;
allow 172.16.10.0/28; # also add in allow/deny rules in this block (extra security)
allow 172.16.155.0/24;
# Forward all requests to python application server
proxy_set_header Host $host:$server_port;
proxy_pass http://10.1.1.16:8002;
proxy_intercept_errors on;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# root /home/luke/ecom2/dist;
# index index.html;
deny all;
}
Which gives the same result.

I’ve checked a similar configuration and your last example seems correct,
at least for a simplistic gunicorn/pyramid app combination.
Seems something is missing in your puzzle )
Here’s my code (I’m new to Pyramid so something might be done better)
helloworld.py
from pyramid.config import Configurator
from pyramid.renderers import render_to_response
def main(request):
return render_to_response('templates:test.pt', {}, request=request)
with Configurator() as config:
config.add_route('main', '/')
config.add_view(main, route_name='main')
config.include('pyramid_chameleon')
app = config.make_wsgi_app()
templates/test.pt
<html>
<body>
Route url: ${request.route_url('main')}
</body>
</html>
My nginx config
server {
listen 80;
server_name pyramid.lan;
location / {
return 301 https://$server_name:8002$request_uri;
}
}
server {
listen 8002;
server_name pyramid.lan;
ssl on;
ssl_certificate /usr/local/etc/nginx/cert/server.crt;
ssl_certificate_key /usr/local/etc/nginx/cert/server.key;
location / {
proxy_set_header Host $host:$server_port;
proxy_pass http://127.0.0.1:5678;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
This is how I run gunicorn:
gunicorn -w 1 -b 127.0.0.1:5678 helloworld:app
And yes, it works:
$ curl --insecure https://pyramid.lan:8002/
<html>
<body>
Route url: https://pyramid.lan:8002/
</body>
</html>
$ curl -D - http://pyramid.lan
HTTP/1.1 301 Moved Permanently
Server: nginx/1.12.2
Date: Thu, 02 Nov 2017 20:41:50 GMT
Content-Type: text/html
Content-Length: 185
Connection: keep-alive
Location: https://pyramid.lan:8002/
Lets figure out what might go wrong in your case
http 400 usually pops up when you go over httP instead of httpS to a server awaiting httpS requests. If there’s no typo in the post and it indeed occurs when you navigate to https://intranet.example.com:8002 it would be nice to see a curl request showing this and a tcpdump showing what’s happening. Actually you can easily reproduce it by simply typing http://intranet.example.com:8002
another idea is that you’re doing a redirect from your app and the link gets broken when the redirect occurs. I better description on how the user may navigate from https://intranet.example.com:8002/login to .../welcome would be helpful
one more idea is that your app is not that simple and you use some middlewares / customization that makes the default logic work differently and your X-Forwarded-Proto header gets ignored - in this case the behavior would be just as you described

The issue here is, obviously, the missing port within the Location directives that your backend produces.
Now, why is the port missing? Most certainly, because of the following code:
proxy_set_header Host $host;
Note that $host itself does not contain $server_port, unlike $http_host, so, your backend would have no way of knowing which port you meant if you just use $host all by itself.
Note that proxy_redirect default of default expects Location to correspond with the value from proxy_pass in order to do its magic (according to documentation), so, your explicit header setting likely interferes with such logic.
As such, from the nginx point of view, I see multiple possible independent solutions:
remove proxy_set_header Host, and let proxy_redirect do its magic;
set proxy_set_header Host appropriately, to include the port number, e.g., using $host:$server_port or $http_host as you see fit (if that doesn't work, then perhaps the deficiency is actually within your upstream app itself, but fear not -- read below);
provide a custom proxy_redirect setting, e.g., proxy_redirect https://pyramid.lan/ / (equivalent to proxy_redirect https://pyramid.lan/ https://pyramid.lan:8002/), which will ensure that all the Location responses will have the proper port; the only way this wouldn't work is if your upstream does non-HTTP redirects with the missing port.

Related

How can I ensure nginx resolves variable DNS in proxy_pass?

I pass some requests to this location (this is a simplified version of the configuration for clarity). ONBOARDING_URL is set in the environment and replaced when the server is run by a URL. This URL is resolved to an IP once. It needs to be resolved for each request when the TTL has expired. Initially, the configuration looked like this:
Initial Configuration Template
server {
listen 443 ssl;
server_name localhost;
resolver ${AWS_DNS};
location /onboarding {
proxy_cache onboarding;
proxy_set_header Host $proxy_host;
proxy_cache_valid 200 10m;
proxy_cache_key $cacheKey;
proxy_set_header Host $proxy_host;
proxy_pass ${ONBOARDING_URL}/api-users;
}
}
I discovered this was only resolved one time. Changes to the IP underlying the DNS resolution had no effect; requests were routed to the IP that was resolved initially.
So following guidance in the proxy_pass documentation, and from other questions / sources (below), we modified the configuration.
https://serverfault.com/a/593003/310436
https://stackoverflow.com/a/26957754/2081835
https://forum.nginx.org/read.php?2,215830,215832#msg-215832
from: http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_pass
Parameter value can contain variables. In this case, if an address is specified as a domain name, the name is searched among the described server groups, and, if not found, is determined using a resolver.
The new configuration was intended to replace the template variable, populated from the environment at runtime, with a configuration variable that will always be resolved. However, it doesn't seem to be. Once an IP is resolved, it is used for the life of the process. Any ideas how to get around this? This seems a bit complex to test, too, which further complicates the fix.
New Configuration Template
server {
listen 443 ssl;
server_name localhost;
resolver ${AWS_DNS};
set $onboardingUrl ${ONBOARDING_URL};
location ~ ^/onboarding/?(.*)$ {
proxy_cache onboarding;
proxy_set_header Host $proxy_host;
proxy_cache_valid 200 10m;
proxy_cache_key $cacheKey;
proxy_set_header Host $proxy_host;
proxy_pass $onboardingUrl/api-users/$1;
}
}
Yet still, for the life of the process only the initially resolved IP is used.

Routing meant for a subdomain is also being applied to the root domain

Consider two websites hosted on the same server: domain.com and foo.domain.com. I want to start up a monitoring panel for each site on port 5555. Each site has a separate monitoring panel so I need to use nginx to route domain.com:5555 and foo.domain.com:5555 to two different places.
Here is the configuration for foo.domain.com:
server {
listen 5555;
server_name foo.domain.com;
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://localhost:5678;
}
}
While this works fine for foo.domain.com:5555, it is also routing domain.com:5555 to the monitoring panel. This is acting like I had defined server_name domain.com foo.domain.com, but clearly I only defined it for foo.domain.com.
The only other nginx configs on the server are for ports 80 and 443. Neither of those configs use any wildcards and explicitly use the full name.
nginx always has a default server - if you do not define a default server, it will use the first server block with a matching listen directive.
If you want to discourage this behaviour, you will need to define a catch-all server for port 5555.
For example:
server {
listen 5555 default_server;
return 444;
}
See this document for more.

nginx upstream subdomain on the same server

I am configuring Nginx load balance with Nginx upstream module, configuration as follow:
upstream load {
server loadapi.example.com;
server loadapi.anotherdomain.com down;
}
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://load;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
server {
listen 80;
server_name loadapi.example.com;
root /disk/projects/load/loadapi;
index index.html index.htm index.shtml index.php;
...
...
error_page 404 /404.html;
}
Notice that the api.example.com and loadapi.example.com are on the same server. loadapi.anotherdomain.com is resolved to another server which provides the same service.
Everything works fine with loadapi.anotherdomain.com, which are on another server.
But when I use the loadapi.example.com as the backend, it seems that Nginx cannot handle it correctly. I can get my service up and running on loadapi.example.com. But it is not working with the upstream.(look like Nginx cannot resolve the subdomain name correctly).
any advice? thx in advance.
nginx uses the Host header to determine which server block to use to process a request.
When the request passes through the proxy_pass http://load; statement, the Host header is set to the value load by default.
To make nginx choose the server block containing the server_name loadapi.example.com; statement, it either needs to be the default_server server, or include the name load in its server_name, or set the Host header using:
proxy_set_header Host loadapi.example.com;
Of course, using upstream for load balancing means that both servers receive the same value for the Host header, and must both respond correctly to it.
See this document for more.

How do I get Nginx to return 444 if the request doesn't match a path?

Short version:
I want to use NGINX as a reverse proxy so that a client accessing the public facing URL gets served API data from the internal Gunicorn server sitting behind the proxy:
external path (proxy) => internal app
<static IP>/ABC/data => 127.0.0.1:8001/data
I'm not getting the location mapping correct.
Long version:
I am setting up NGINX for the first time and am attempting to use it as a reverse proxy for a rest api served by Gunicorn. The api is served at 127.0.0.1:8001 and I can access it from the server and get the appropriate responses, so that piece I believe is working correctly. It's running persistently using Supervisord.
I'd like to access one of the API endpoints externally at <static IP>/ABC/data. On the Gunicorn server, this endpoint available at localhost:8001/data. Eventually I'd like to serve other web apps through NGINX with roots like <static IP>/foo, <static IP>/bar, etc. Each of these web apps would be from an independent Python app. But currently, when I try to access the endpoint externally, I get a 444 error code, so I think I am not configuring NGINX correctly.
I put together my first attempt at an NGINX config from the config posted on the Guincorn site. Instead of a single config, I've split it into a global config and a site specific one. My global config at etc/nginx/nginx.conf looks like:
user ops;
worker_processes 1;
pid /run/nginx.pid;
error_log /tmp/nginx.error.log;
events {
worker_connections 1024; # increase if you have lots of clients
accept_mutex off; # set to 'on' if nginx worker_processes > 1
use epoll;
# 'use epoll;' to enable for Linux 2.6+
# 'use kqueue;' to enable for FreeBSD, OSX
}
http {
include mime.types;
# fallback in case we can't determine a type
default_type application/octet-stream;
access_log /tmp/nginx.access.log combined;
sendfile on;
server_tokens off;
server {
# if no Host match, close the connection to prevent host spoofing
listen 80 default_server;
return 444;
}
gzip on;
gzip_disable "msie6";
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
Then my site specific configuration that is in /etc/nginx/sites-available (and is symlinked in /etc/nginx/sites-enabled) is:
upstream app_server {
# fail_timeout=0 means we always retry an upstream even if it failed
# to return a good HTTP response
# for UNIX domain socket setups
# server unix:/tmp/gunicorn_abc_api.sock fail_timeout=0;
# for a TCP configuration
server 127.0.0.1:8001 fail_timeout=0;
}
server {
# use 'listen 80 deferred;' for Linux
# use 'listen 80 accept_filter=httpready;' for FreeBSD
listen 80 deferred;
client_max_body_size 4G;
# set the correct host(s) for your site
server_name _;
keepalive_timeout 100;
# path for static files
#root /path/to/app/current/public;
location /ABC {
# checks for static file, if not found proxy to app
try_files $uri #proxy_to_app;
}
location #proxy_to_app {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# enable this if and only if you use HTTPS
# proxy_set_header X-Forwarded-Proto https;
proxy_set_header Host $http_host;
# we don't want nginx trying to do something clever with
# redirects, we set the Host: header above already.
proxy_redirect off;
proxy_pass http://app_server;
}
# error_page 500 502 503 504 /500.html;
# location = /500.html {
# root /path/to/app/current/public;
# }
}
The configs pass service nginx checkconfig, but I end up seeing the following in my access log:
XXX.XXX.X.XXX - - [09/Sep/2016:01:03:18 +0000] "GET /ABC/data HTTP/1.1" 444 0 "-" "python-requests/2.10.0"
I think I've somehow not configured the routes properly. Any suggestions would be appreciated.
UPDATE:
I have it working now with a few changes. I commented out the following block:
server {
# if no Host match, close the connection to prevent host spoofing
listen 80 default_server;
return 444;
}
I can't figure out how to get the behavior of returning 444 unless there is a valid route. I'd like to, but I'm still stuck on this part. This block seems to eat all incoming requests. I've also changed the app config to:
upstream app_server {
server 127.0.0.1:8001 fail_timeout=0;
}
server {
# use 'listen 80 deferred;' for Linux
# use 'listen 80 accept_filter=httpready;' for FreeBSD
listen 80 deferred;
client_max_body_size 100M;
# set the correct host(s) for your site
server_name $hostname;
keepalive_timeout 100;
location /ABC {
# checks for static file, if not found proxy to app
try_files $uri #proxy_to_app;
}
location #proxy_to_app {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# enable this if and only if you use HTTPS
# proxy_set_header X-Forwarded-Proto https;
proxy_set_header Host $http_host;
# we don't want nginx trying to do something clever with
# redirects, we set the Host: header above already.
proxy_redirect off;
rewrite ^/ABC/(.*) /$1 break;
proxy_pass http://app_server;
}
}
Basically I seem to have had to explicity set server_name and also use rewrite to get the correct mapping to the app server.
This works fine for me, returns 444 (hangs up connection) only if no other server name is matched:
server {
listen 80;
server_name "";
return 444;
}

All hosts redirecting to single nginx proxy_pass

I have the following in my .conf file:
server {
listen 80;
server_name mydomain.net;
access_log /var/log/nginx/mydomain.net.access.log main;
location / {
proxy_pass http://127.0.0.1:9000;
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
}
}
Which works just fine... except everything that hits the server is getting fed to this server block. My IP, another domain pointing at this block, and the actual mydomain.net all point to what only mydomain.net is pointing to.
As the documentation states:
In this configuration nginx tests only the request’s header field “Host” to determine which server the request should be routed to. If its value does not match any server name, or the request does not contain this header field at all, then nginx will route the request to the default server for this port. In the configuration above, the default server is the first one — which is nginx’s standard default behaviour.
This was the case here. I performed the suggested step to drop undefined hosts:
server {
listen 80 default_server;
server_name "";
return 444;
}
Which solved my issue.

Resources