Search code examples
sslnginxkubernetesnginx-ingresskubernetes-helm

Securing specific nginx-ingress location with client cert verification


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

Solution

  • 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