Nginx/Pyramid 自定义 SSL 端口

Nginx/Pyramid custom SSL port

作为前缀,我已经使用以下堆栈一段时间并取得了巨大成功:

NGINX - 网络代理 SSL - 在 nginx 中配置 金字塔网络应用程序,由 gunicorn

提供服务

以上组合效果很好,这是一个有效的配置。

    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;
    }

}

现在,这就是我为面向 public 的应用程序提供服务的方式,效果很好。对于我所有的内部应用程序,我过去常常简单地将用户定向到一个内部域示例:http://subdomain.company.domain,这在很长一段时间内都运行良好。

现在在 KRACK 攻击之后,虽然我们有一些非常彻底的防火墙规则来防止很多攻击,但我想强制所有内部流量通过 SSL,并且我不想使用自签名证书,我想使用 lets encrypt 这样我就可以 auto-renew 使管理更容易(也更便宜)的证书。

为了使用 lets encrypt,我需要有一个 public 面向 DNS 和服务器来执行 ACME 质询(用于自动更新)。现在这又是在 nginx 中设置的一件非常容易的事情,下面的配置非常适合提供静态内容:

它的作用是,如果互联网用户访问 intranet.example.com,它只会显示禁止消息。但是,如果本地用户尝试,他们将被转发到 intranet.example.com:8002 并且端口 8002 仅在本地可用,因此外部用户无法访问此站点上的网页

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;
        }


}

现在,pyramid/nginx 结婚问题来了,如果我使用与上面相同的配置,但在 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;
    }

}

我 运行 遇到了各种各样的问题,首先在金字塔内部 我在我的 views/templates

中使用了以下代码
request.route_url # get route url for desired function

现在使用具有上述设置的 request.route_url 应该会导致 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 这又是不正确的。

如果我使用 route_url 和 NGINX 代理设置:

proxy_set_header Host $http_host;

我收到错误:NGINX 到 return 400 错误:

400: The plain HTTP request was sent to HTTPS port

并请求:https://intranet.example.com:8002/ gets reverted to: http://intranet.example.com/login(省略端口和 https)

然后我使用相同的 nginx 设置 (header $htto),但我想我会改为使用:

request.route_path

我的理论是这应该强制所有内容都保持相同的 url 前缀,并且只是转发来自 https://intranet.example.com:8002/login to https://intranet.example.com:8002/welcome 的用户,但实际上,此设置的执行方式与使用 [=78] 相同=].

proxy_set_header Host $http_host;

导航到 https://intranet.example.com:8002

时出现错误
400: The plain HTTP request was sent to HTTPS port

并请求:https://intranet.example.com:8002/ gets reverted to: http://intranet.example.com/login(省略端口和 https)

任何人都可以协助正确设置,以便我在 https://intranet.example.com:8002

上提供我的应用程序

编辑:

也试过:

    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;
        }

结果相同。

我检查了一个类似的配置,你的最后一个例子似乎是正确的, 至少对于简单的 gunicorn/pyramid 应用程序组合而言。

你的拼图好像少了点什么)

这是我的代码(我是 Pyramid 的新手,所以可以做得更好)

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>

我的 nginx 配置

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;
    }
}

我就是这样 运行 gunicorn:

gunicorn -w 1 -b 127.0.0.1:5678 helloworld:app

是的,它有效:

$ 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/

让我们找出您的情况可能出了什么问题

    当您通过 httP 而不是 httpS 到等待 httpS[= 的服务器时,通常会弹出
  • http 400 52=] 请求。如果 post 中没有拼写错误,并且当您导航到 https://intranet.example.com:8002 时它确实发生了,那么很高兴看到 curl 请求显示了这一点,并且 tcpdump 显示了正在发生的事情。实际上,您只需输入 http://intranet.example.com:8002
  • 即可轻松重现它
  • 另一个想法是,您正在从您的应用程序进行重定向,并且 link 在重定向发生时被破坏。我更好地描述用户如何从 https://intranet.example.com:8002/login 导航到 .../welcome 会有所帮助

  • 还有一个想法是您的应用程序并不那么简单,您使用了一些中间件/定制,这使得默认逻辑的工作方式不同,您的 X-Forwarded-Proto header 被忽略了 -在这种情况下,行为将与您描述的一样

这里的问题显然是后端生成的 Location 指令中缺少端口。

现在,为什么缺少端口?肯定是因为以下代码:

proxy_set_header Host $host;
  • 请注意,$host 本身不包含 $server_port,与 $http_host 不同,因此,如果您只需单独使用 $host

  • 请注意,defaultproxy_redirect 默认值期望 Locationproxy_pass 的值相对应,以便发挥其魔力(根据文档),因此,您的显式 header 设置可能会干扰此类逻辑。


因此,从 nginx 的角度来看,我看到了多个可能的独立解决方案:

  • 删除 proxy_set_header Host,让 proxy_redirect 发挥它的魔力;
  • 适当地设置proxy_set_header Host,以包括端口号,例如,使用您认为合适的$host:$server_port$http_host(如果这不起作用,那么可能是不足之处实际上在您的上游应用程序本身中,但不要害怕——阅读下文);
  • 提供自定义 proxy_redirect 设置,例如 proxy_redirect https://pyramid.lan/ /(相当于 proxy_redirect https://pyramid.lan/ https://pyramid.lan:8002/),这将确保所有 Location 响应都具有正确的端口;唯一不起作用的方法是,如果您的上游 non-HTTP 使用缺少的端口进行重定向。