Nginx Ingress: passing client certificate info to backend - flask

I successfully implemented in my aks cluster an ingress with tls certification (https://learn.microsoft.com/en-us/azure/aks/ingress-own-tls), but I would like to pass the information contained in the client certificate to the backend.
I did try adding to my ingress the annotation nginx.ingress.kubernetes.io/auth-tls-pass-certificate-to-upstream: "true", but the information seems to be missing in my request headers (I am simply printing the content of request.headers from my flask application). Other headers are correctly shown, e.g. X-Forwarded-Proto: https or X-Forwarded-Port: 443 .
Could somebody confirm the expected behaviour of the annotation?
Do I need to configure the backend somehow with tls as well?
EDIT
I did access the ingress pod, and in the nginx config I could not find any reference to ssl_client_s_dn, which I would expect to be the best candidate to pass the certificate info into an header.
I tried to assign some custom headers following the steps in https://github.com/kubernetes/ingress-nginx/tree/master/docs/examples/customization/custom-headers, but also this seems not to work.

Which version of nginx-ingress are you using ?
At least with version 0.30 I'm able too see the client's certificate details passed to the backend properly.
The value of ssl_client_s_dn is being passed as Ssl-Client-Subject-Dn header with default nginx controller setup, no customization needed.
Here is the content of my default /etc/nginx/nginx.conf (converted from ConfigMap)
# Pass the extracted client certificate to the backend
proxy_set_header ssl-client-cert $ssl_client_escaped_cert;
proxy_set_header ssl-client-verify $ssl_client_verify;
proxy_set_header ssl-client-subject-dn $ssl_client_s_dn;
proxy_set_header ssl-client-issuer-dn $ssl_client_i_dn;
Request headers seen from backend perspective:
...
"Ssl-Client-Issuer-Dn": "CN=example.com,O=example Inc.",
"Ssl-Client-Subject-Dn": "O=client organization,CN=client.example.com",
"Ssl-Client-Verify": "SUCCESS",
"User-Agent": "curl/7.58.0",
"X-Forwarded-Host": "httpbin.example.com",
"X-Scheme": "https",
}
}
You can always add your own custom headers, as explained here
Example:
apiVersion: v1
data:
X-Client-Cert-Info: $ssl_client_s_dn
kind: ConfigMap
metadata:
...
which reflects at backend as:
...
"X-Client-Cert-Info": "O=client organization,CN=client.example.com",
"X-Forwarded-Host": "httpbin.example.com",
"X-Scheme": "https",
}
}

you can pass annotations in nginx ingress' service this
annotations:
service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout: '60'
service.beta.kubernetes.io/aws-load-balancer-proxy-protocol: "*"
if you want modify header based on ingress rule then you may also add annotations to ingress rules as well.

Related

AWS Load Balancer appends :80 to url on http to https 301 redirect

When I go to my example application: https://example.com/r/123 it works as expected.
If I go to http://example.com/r/123 it seems the load balancer changes the url to https://example.com:80/r/123.
This page results in a ERR_SSL_PROTOCOL_ERROR.
Is it possible to make AWS not add the port(:80) in the redirect?
My problem was solved by changing the server IP to 0.0.0.0 instead of the default value of localhost in the nuxt server config.
// nuxt.config.js
server: {
host: '0.0.0.0',
},

How to insist on https in browsable api

I have a Django Rest Framework running at: https://dev-example.domain.com in kubernetes behind a kubernetes Ingress with http traffic disabled.
Note this is not NGINX nor Traefik. It's an Ingress controller setup on GCP.
This concept is explained here
Therefore, trying to go to http://dev-example.domain.com returns a 404. Rightfully.
In the Browsable api, however, all links are prefixed with http:://
Therefore, when one of these links is clicked on, the redirect returns a 404.
Is there a setting that will allow that prefix to be https?
This means django doesnt know your request was over HTTPS, There are several methods you can use to tell it you are using HTTPS.
For example Django will respect the X-Forwarded-Proto Header so have Nginx or whatever you have in front of Django forward that to your application.
i.e
proxy_set_header X-Forwarded-Proto $scheme;

Create AWS Application Load Balancer Rule which trim off request's prefix without using additional reverse proxy like nginx, httpd

Basically, I have a couple of services. I want to forward every requests with prefix "/secured" to server1 port 80 and all other requests to server 2 port 80. The problem is that on server1, I am running service which accept the request without "/secured" prefix. In other words, I want to forward every requests such as "http://example.com/secured/api/getUser" to server1 as "http://example.com/api/getUser" (remove /secured from request' path).
With AWS ALB, currently the request is sent as http://example.com/secured/api/getUser; which forces me to update my server1's code so that the code handles requests with /secured prefix which doesn't look good.
Is there any easy way to solve this with ALB?
Thanks.
I can confirm that this is unfortunately not possible with the ALB alone - and I agree it really should be.
AWS states:
Note that the path pattern is used to route requests but does not
alter them. For example, if a rule has a path pattern of /img/*, the
rule would forward a request for /img/picture.jpg to the specified
target group as a request for /img/picture.jpg.
I had the same issue, and as Mark pointed out, you can use reverse proxy on your server and do something like this (this is Nginx configuration):
server {
listen 80 default_server;
location /secured/ {
proxy_pass http://localhost:{service_port}/;
}
}
This will strip the /secured part and proxy everything else to your service. Just be sure to have the trailing / after the service port.

Django CSRF Error Casused by Nginx X-Forwarded-host

I've been working on a django app recently and it is finally ready to get deployed to a qa and production environment. Everything worked perfectly locally, but since adding the complexity of the real world deployment I've had a few issues.
First my tech stack is a bit complicated. For deployments I am using aws for everything with my site deployed on multiple ec2's backed by a load balancer. The load balancer is secured with ssl, but the connections to the load balancer are forwarded to the ec2's over standard http on port 80. After hitting an ec2 on port 80 they are forwarded to a docker container on port 8000 (if you are unfamiliar with docker just consider it to be a standard vm). Inside the container nginx listens on port 8000, it handles a redirection for the static files in django and for web requests it forwards the request to django running on 127.0.0.1:8001. Django is being hosted by uwsgi listening on port 8001.
server {
listen 8000;
server_name localhost;
location /static/ {
alias /home/library/deploy/thelibrary/static/;
}
location / {
proxy_set_header X-Forwarded-Host $host:443;
proxy_pass http://127.0.0.1:8001/;
}
}
I use X-Forwarded host because I was having issues with redirects from google oauth and redirects to prompt the user to login making the browser request the url 127.0.0.1:8001 which will obviously not work. Within my settings.py file I also included
USE_X_FORWARDED_HOST = True
to force django to use the correct host for redirects.
Right now general browsing of the site works perfectly, static files load, redirects work and the site is secured with ssl. The problem however is that CSRF verification fails.
On a form submission I get the following error
Referer checking failed - https://qa-load-balancer.com/projects/new does not match https://qa-load-balancer.com:443/.
I'm really not sure what to do about this, its really through stackoverflow questions that I got everything working so far.
Rather than doing an HTTP proxy, I would use Nginx's built-in capacity to communicate with uWSGI. (This will still work if you are using separate Docker containers for Nginx and uWSGI since the communication is done over TCP)
A typical configuration (mine) looks like this:
location / {
uwsgi_pass http://127.0.0.1:8001;
include uwsgi_params;
}
You will have to remove the --http argument (or config-file equivalent) from your uWSGI invocation.
Additionally, in uwsgi_params (found in /etc/nginx or a custom location you specify) there are several directives to pass meta data through. Here's an excerpt from mine that looks like it could be related to your problem:
...
uwsgi_param REQUEST_URI $request_uri;
uwsgi_param DOCUMENT_ROOT $document_root;
uwsgi_param SERVER_PROTOCOL $server_protocol;
uwsgi_param HTTPS $https if_not_empty;
Relevant docs: http://uwsgi-docs.readthedocs.org/en/latest/WSGIquickstart.html#putting-behind-a-full-webserver
For users who cannot use Nginx's built-in facility, here's the root cause:
Starting in ~Djagno 1.9, the CSRF check requires that the Referer and Host match unless you specify a CSRF_TRUSTED_ORIGINS (see the code around REASON_BAD_REFERER here)
If you don't specify CSRF_TRUSTED_ORIGINS, the system falls back on request.get_host()
request.get_host() uses request._get_raw_host()
request._get_raw_host() checks sequentially HTTP_X_FORWARDED_HOST (if USE_X_FORWARDED_HOST is set), HTTP_HOST, and SERVER_NAME
Most recommended Nginx configurations suggest an entry like proxy_set_header X-Forwarded-Host $host:$server_port;
Eventually, the referrer (e.g. <host>) is compared to X-Forwarded-Host (e.g. <host>:<port>). These do not match so CSRF fails.
There isn't a lot of discussion about this, but Django ticket #26037 references RFC2616. The ticket states that a host without a port is "against spec", but that's not true as the spec actually says:
A "host" without any trailing port information implies the default port for the service requested
This leads to (at minimum) the following options (safest first):
include host and port in CSRF_TRUSTED_ORIGINS
remove port from X-Forwarded-Host in nginx configuration (on the assumption that the non-spec X-Forwarded-Host follows the same semantics as Host)
To avoid hard-coding domains in CSRF_TRUSTED_ORIGINS, the second option is attractive, but it may come with security caveats. Speculatively:
X-Forwarded-Proto should be used to clarify the protocol (since the absence of a port implies a default protocol)
The reverse proxy MUST use port 443 for HTTPS (i.e. the default for the protocol) and disallow non-HTTPS connection types (X-Forwarded-Proto might fix this).
I had the same issue running a Django project on GitPod: the X-Forwarded-Host was in the form hostname:443, causing the CSRF error.
I solved it with a custom middleware that strips the port from the header:
# myproject/middleware.py
from django.utils.deprecation import MiddlewareMixin
class FixForwardedHostMiddleware(MiddlewareMixin):
def process_request(self, request):
forwarded_host = request.META.get('HTTP_X_FORWARDED_HOST')
if forwarded_host:
forwarded_host = forwarded_host.split(':')[0]
request.META['HTTP_X_FORWARDED_HOST'] = forwarded_host
To use this middleware, you need to edit your settings.py, and insert the new middleware before the CSRF one, like so:
# myproject/settings.py
MIDDLEWARE = [
...
'myproject.middleware.FixForwardedHostMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
...
]
See clayton's answer to understand why this fixes the CSRF error.
I don't think this middleware introduces any security issue; please comment if you think otherwise.

django admin redirects to wrong port on save

I have a django project set up with nginx+apache. The http port for outside access is 20111 which is then forwarded to the server machine (which has an internal IP) to port 80. So nginx listens on port 80 (and passes relevant requests to apache on port 5000).
Now the initial login can be reached from the outside via http://externalip:20111 - but when I complete an admin action, like saving an entry, I get redirected to http://externalip/path/to/model -- without the port 20111. The result is a timeout. How can I tell django to use a specific hostname/port (i.e. http://externalip:20111) for all admin redirects?
When deploying applications behind a proxy or load balancer, it is common to rely on the X-Forwarded-Host header. Django has support for it
First of all, you have to setup nginx to send the proper headers. Add to your nginx host configuration (inside your location section):
proxy_set_header X-Forwarded-Host $host:20111;
Second, add to your settings.py:
USE_X_FORWARDED_HOST = True
It will allow django to trust X-Forwarded-Host headers from a request.
It should make it work for you. For security reasons, you should not trust every value sent in X-Forwarded-Host, so add your trusted domains/IPs to ALLOWED_HOSTS in settings.py