Securing specific nginx-ingress location with client cert verification

10/23/2018

I'm setting up an instance of ghost and I'm trying to secure the /ghost path with client cert verification.

I've got an initial ingress up and running that serves the site quite happily with the path specified as /.

I'm trying to add a second ingress (that's mostly the same) for the /ghost path. If I do this and add the annotations for basic auth, everything seems to work. i.e. If I browse to /ghost I am prompted for credentials in the basic-auth secret, if I browse to any other URL it is served without auth.

I then switched to client cert verification based on this example: https://github.com/kubernetes/ingress-nginx/tree/master/docs/examples/auth/client-certs

When I try this either the whole site or none of the site is secured, rather than the path-based separation, I got with basic-auth. Looking at the nginx.conf from the running pod the proxy_set_header ssl-client-verify, proxy_set_header ssl-client-subject-dn & proxy_set_header ssl-client-issuer-dn elements are added under the root / path and the /ghost path. I've tried removing those (from the root only) and copying the config directly back to the pod but not luck there either.

I'm pulling nginx-ingress (Chart version 0.23.0) in as a dependency via Helm

Ingress definition for / location - this one works

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  annotations:
    certmanager.k8s.io/cluster-issuer: letsencrypt-staging
    kubernetes.io/ingress.class: nginx
    kubernetes.io/tls-acme: "true"
  labels:
    app: my-app
    chart: my-app-0.1.1
    heritage: Tiller
    release: my-app
  name: my-app
  namespace: default
spec:
  rules:
  - host: example.com
    http:
      paths:
      - backend:
          serviceName: my-app
          servicePort: http
        path: /
  tls:
  - hosts:
    - example.com
    secretName: mysite-tls

Ingress definition for /ghost location - this one doesn't work

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  annotations:
    nginx.ingress.kubernetes.io/auth-tls-verify-client: "on"
    nginx.ingress.kubernetes.io/auth-tls-secret: "default/auth-tls-chain"
    nginx.ingress.kubernetes.io/auth-tls-verify-depth: "1"
    nginx.ingress.kubernetes.io/auth-tls-error-page: "http://www.example.com/error-cert.html"
    nginx.ingress.kubernetes.io/auth-tls-pass-certificate-to-upstream: "false"
    kubernetes.io/ingress.class: "nginx"
  labels:
    app: my-app
    chart: my-app-0.1.1
    heritage: Tiller
    release: my-app
  name: my-app-secure
  namespace: default
spec:
  rules:
  - host: example.com
    http:
      paths:
      - backend:
          serviceName: my-app
          servicePort: http
        path: /ghost
  tls:
  - hosts:
    - example.com
    secretName: mysite-tls
-- rh072005
kubernetes
kubernetes-helm
nginx
nginx-ingress
ssl

2 Answers

10/2/2019

You could add a location-snippet

  annotations:
    nginx.ingress.kubernetes.io/location-snippet: |
       if ($location ^~ ghost) {
          set $ban_info = "location";
       }

       if ($ssl_client_s_dn !~ "CN=ok_client") {
          set $ban_info = "${ban_info}+no_client";
       }

       if ($ban_info = "location+no_client") {
         return 403;
       }
-- mulligan
Source: StackOverflow

10/25/2018

You need a '*' on your path on your second ingress if you want to serve all the pages securely under /ghost and if you want just /ghost you need another rule. Something like this:

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  annotations:
    nginx.ingress.kubernetes.io/auth-tls-verify-client: "on"
    nginx.ingress.kubernetes.io/auth-tls-secret: "default/auth-tls-chain"
    nginx.ingress.kubernetes.io/auth-tls-verify-depth: "1"
    nginx.ingress.kubernetes.io/auth-tls-error-page: "http://www.example.com/error-cert.html"
    nginx.ingress.kubernetes.io/auth-tls-pass-certificate-to-upstream: "false"
    kubernetes.io/ingress.class: "nginx"
  labels:
    app: my-app
    chart: my-app-0.1.1
    heritage: Tiller
    release: my-app
  name: my-app-secure
  namespace: default
spec:
  rules:
  - host: example.com
    http:
      paths:
      - backend:
          serviceName: my-app
          servicePort: http
        path: /ghost
      - backend:
          serviceName: my-app
          servicePort: http
        path: /ghost/*
  tls:
  - hosts:
    - example.com
    secretName: mysite-tls

However, if you want something like / unsecured and /ghost secured, I believe you won't be able to do it. For example, if you are using nginx, this is a limitation from nginx itself, when you configure a server {} block with TLS in nginx it looks something like this:

server {
    listen              443 ssl;
    server_name         example.com;
    ssl_certificate     example.com.crt;
    ssl_certificate_key example.com.key;
    ssl_protocols       TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers         HIGH:!aNULL:!MD5;
    ...
}

The ingress controller creates paths like this:

server {
    listen              443 ssl;
    server_name         example.com;
    ssl_certificate     example.com.crt;
    ssl_certificate_key example.com.key;
    ssl_protocols       TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers         HIGH:!aNULL:!MD5;
    ...

    location / {
       ...
    }

    location /ghost {
       ...
    }

}

So when you configure another server {} block with the same hostname and with no SSL it will override the first one.

You could do it with different - host: rules in your ingress for example ghost.example.com with TLS and main.example.com without TLS. So in your nginx.conf you would have different server {} blocks.

You can always shell into the ingress controller pod to check the configs, for example:

$ kubectl exec -it nginx-ingress-controller-xxxxxxxxx-xxxxx bash
www-data@nginx-ingress-controller-6bd7c597cb-8kzjh:/etc/nginx$ cat nginx.conf
-- Rico
Source: StackOverflow