Kubernetes Ingress behind Cloudflare Access

Suppose we want to cover important services (like Kubernetes dashboard) behind authentication, authorization and make everything encrypted

The easiest way will be to cover it with Cloudflare which will give us everything out of the box

In short here is how it will work:

G cluster_0 Kubernetes user user cloudflare cloudflare user->cloudflare  https google google cloudflare->google  anonymous ingress ingress cloudflare->ingress  authorized, https secret secret ingress->secret service service ingress->service pod pod service->pod

Things to accomplish:

  1. Communication between user and Cloudflare is encrypted by its certificates.
  2. Communication between Cloudflare and Ingress is always encrypted by Origin Certificate signed and trusted by Cloudflare.
  3. Ingress allows requests only from Cloudflare.
  4. Access to application is possible only after authentication and authorization via configured identity provider.

User to Cloudflare encryption

That is an easy one, all we need to do is to turn on "proxy" checkbox for desired (sub)domain.

proxied

Allow requests only from Cloudflare

Thats also easy one, Cloudflare has range of its IP addresses which might be added to a ingress annotation whitelist-source-range

TODO: create simple admission mutation webhook which will listen to ingress creation or updation events and inject list of addresses from config map, also it should watch for config map and if it changes - update all ingresses

Cloudflare to Ingress encryption

Here we have four options under SSL/TLS configurations:

  • Off - even not disqused
  • Flexible - its when requests will be user -(https:443)-> cloudflare -(http:80)-> origin - technically, technically, if we are allowing requests only from a Cloudflare it might be ok, but we want encryption everywhere
  • Full - now requests from cloudflare to origin will also be made over https, but Cloudflare will trust any self signed certificate
  • Strict - the most interesting for us, it will be encrypted and Cloudflare will validate certificates

If we are talking about certificates for our origin there are two options:

  • Create certificates with LetsEncrypt (requires cert-manager so 90 days certificates will be updated automaticaly)
  • Create origin certificate from Cloudflare (will give us certificate for years, trusted by Cloudflare)

We are going to try all options here but at the end stick with Cloudflare certificate

Flexible

Our starting poing, it wont have encryption between Cloudflare and origin but will be used for all further deployments

flexible.yml

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: flexible
  labels:
    app: flexible
spec:
  replicas: 1
  selector:
    matchLabels:
      app: flexible
  template:
    metadata:
      labels:
        app: flexible
    spec:
      containers:
        - name: flexible
          image: nginx:alpine
          ports:
            - name: flexible
              containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: flexible
spec:
  type: ClusterIP
  selector:
    app: flexible
  ports:
    - name: flexible
      protocol: TCP
      port: 80
      targetPort: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: flexible
  annotations:
    # ADDED
    nginx.ingress.kubernetes.io/whitelist-source-range: 173.245.48.0/20, 103.21.244.0/22, 103.22.200.0/22, 103.31.4.0/22, 141.101.64.0/18, 108.162.192.0/18, 190.93.240.0/20, 188.114.96.0/20, 197.234.240.0/22, 198.41.128.0/17, 162.158.0.0/15, 104.16.0.0/13, 104.24.0.0/14, 172.64.0.0/13, 131.0.72.0/22, 2400:cb00::/32, 2606:4700::/32, 2803:f800::/32, 2405:b500::/32, 2405:8100::/32, 2a06:98c0::/29, 2c0f:f248::/32

spec:
  rules:
    - host: flexible.marchenko.net.ua
      http:
        paths:
          - path: /
            pathType: ImplementationSpecific
            backend:
              service:
                name: flexible
                port:
                  number: 80

Full

For full we are ok to use self signed certificates, so need to create one:

openssl genrsa -out selfsigned.key 2048
openssl req -x509 \
  -new -nodes  \
  -days 10 \
  -key selfsigned.key \
  -out selfsigned.crt \
  -subj "/CN=selfsigned.marchenko.net.ua"

Then with help of commands copy and paste certificate and key to secret

cat selfsigned.crt | base64 | pbcopy
cat selfsigned.key | base64 | pbcopy

And apply following:

selfsigned.yml

---
apiVersion: v1
kind: Secret
metadata:
  name: selfsigned-tls
type: kubernetes.io/tls
data:
  # cat selfsigned.crt | base64
  tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUN5RENDQWJBQ0NRQ3RMUzZwaEVLQ1pqQU5CZ2txaGtpRzl3MEJBUXNGQURBbU1TUXdJZ1lEVlFRRERCdHoKWld4bWMybG5ibVZrTG0xaGNtTm9aVzVyYnk1dVpYUXVkV0V3SGhjTk1qRXhNVEl6TVRnd056QTNXaGNOTWpFeApNakF6TVRnd056QTNXakFtTVNRd0lnWURWUVFEREJ0elpXeG1jMmxuYm1Wa0xtMWhjbU5vWlc1cmJ5NXVaWFF1CmRXRXdnZ0VpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCRHdBd2dnRUtBb0lCQVFEbDA2RXlXK2FRZmxWZWF6R2IKU0JaR09DeThqaC9RS2k4Uy84alhHZ3dUVXVpSlprd080MzdtTGxUVzdVRmkwS2ZxR2YrS21Cak5McE83RzQwTQpZR3FIakhiNERwZjlGSHNDWExsTDVKOUNhbGJheG1ZOUYwQ3pwbjRkNXNVbFhqVDZDY0dmYm92NUJxWDBEWXFiCjlFOWdGZGI2NXJ5eHhSSndKK25XeW14YWI4c2tWTytWbGVsNUdOREx3cGdXRUJDUnRNOFNZY1ZicDYrNXZDQi8KQjRiL1dBalQ3bjYwMHhmdWNHMkc0QTd0RkFTSkpzeVdFdzBoT0tvQVp5WmNDTUxmdWRVb1BYVndpdTVJV1Q3LwpzRG5tUUI0OSt0Yy9sNjFlYjdhZlJSaElIZ0lBZUIrU3ZqQTZIazhNV1R1b28wZ29hVWhwNVpRVnZ0UTlNN21ZCng2RzlBZ01CQUFFd0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFKcEVkQTlEUE5Nd3BSRUtBSFZKTCtLL0crTDkKbS9CYS9CcDlQUmNNa011M2VXRkhWTUR6dWQrblNLOWRoY0luV056MjZEZHh6RE5VeDgzVkhCQWNiZnRCTHFVSgpDVEZ5bk9WQzF1cVNoZ3FsVzFCMVdVL1ZiTmZBOWZRS09Kd29CTmtrOWRMdXlYOU55aHJ0TmUrNWpGL3FqM1BxCm1ON2ZORUdlZkViZGVFbVgvY3ZFR1JZL1JWRXpOcVgyQUNoTEdldmg2SnZqTG5FRmVSaElxWmxyT0VZVTV5Q0wKQWRkT0VWYTJhQmxXK015c1hDNnR4bUlzVk5UY3pFUW95em1qdnFrQjl6YnZaeWxKaGhuUDNFc1l2MmRqbnpZSQo3cXhWdURHbDVCOStNcmI1WVlXUXhqYTl4NmlkVzB4Z2pYUloxcUdDT2xDS0FobHhzenBrdEFFbWJTcz0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
  # cat selfsigned.key | base64
  tls.key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBNWRPaE1sdm1rSDVWWG1zeG0wZ1dSamdzdkk0ZjBDb3ZFdi9JMXhvTUUxTG9pV1pNCkR1Tis1aTVVMXUxQll0Q242aG4vaXBnWXpTNlR1eHVOREdCcWg0eDIrQTZYL1JSN0FseTVTK1NmUW1wVzJzWm0KUFJkQXM2WitIZWJGSlY0MCtnbkJuMjZMK1FhbDlBMkttL1JQWUJYVyt1YThzY1VTY0NmcDFzcHNXbS9MSkZUdgpsWlhwZVJqUXk4S1lGaEFRa2JUUEVtSEZXNmV2dWJ3Z2Z3ZUcvMWdJMCs1K3ROTVg3bkJ0aHVBTzdSUUVpU2JNCmxoTU5JVGlxQUdjbVhBakMzN25WS0QxMWNJcnVTRmsrLzdBNTVrQWVQZnJYUDVldFhtKzJuMFVZU0I0Q0FIZ2YKa3I0d09oNVBERms3cUtOSUtHbElhZVdVRmI3VVBUTzVtTWVodlFJREFRQUJBb0lCQVFEYUF4N3VmbThBc2xhWQo4bVprUHNXaVpyZEpSaWkzZHN5ZzRUTExtVkhMSXVLak0vOENRSnVvRGxzOFptT1lNUWFYODRHTkh5cW51SDg3CkJxK09CZ2dIL1I3OXdCbzlZS2d2WFhZSW9CN1RDYnpLVzFtd3FkWWVsTmtkMUVNL1oxWXB1Z2VLRFh1S0J0eDEKM2tLV0hmTU4ycW5MNkFVblBTSjRjZVJJZEV6RW0ydGY4NzJWNUtQNVU3dWtLQ3ZKQjdOOGxUaGFVb09lcmhtRAo5YXVsbzV0a3NNOWJWOVZsMk5JaVZVQkk1OWJnQ25EanBTT295c1VraDloWHJNZTU4R0ZvOUVMcUJPYVg4V1FkCllDSWQvN3hqbkxXYnVXTHVLR3FUNXNrYlZpZHlKd3drWlI4TktSejhiSnM2UWNxMmd5SlIzeUp0USszNzNpV3QKdVVBUWg5c0JBb0dCQVBtUWxrbDVPQmdrbXl5U0JNZWt6bjU2TXZtQ2VtblpmS2VKdnpBaEpzTS9CWUtkSjVxUwpuUTNGb1d0emE1US9LSDhPbFdLMVk0UlZDL1VaUkRoRUswYWtVeE5Yc2E2Wm1GV1JmSEhTcndWRERwQVJiZytLCjJsZE1EdzlmVjdSU044ejlCMnVKa3Ixb29td29SMXdoUTF3bDVJRW9INFY4OVFUblRJKzVWSHlwQW9HQkFPdkEKdjUxR2Zjd3pCWndXem1LQnRwazVDa3YyREZQMXFMM2NEWHBaN1lyVEZTS0M2Vml5REN0UjdJd1U3OW9FVXZWawozZUNML3VjTTR4ZUpRc0ZsRlVia1Qwa215cUVpQmxoN0JVSHU5eVh1WFc0dlFXRU91ZC9KNWJqdWJld3R0ZnhOCjYwMktiL1lLVmtSTzJhOVUreFI1NWVqWE5iaHJOZzNnM0daN0NqVDFBb0dBSFliMzRSV1FoM3E5U1ZhMWJoR1gKeE9zY1lOMEpNc0RpdU1mWmNRNWJFYnJCSWlVSjdJWHdReExnK0YrZG1OcW9KZkJKeUhZQm9rU1Z0NWdYdERpTQpqajBlL3lqZkF2TjFUSnA4Q05PT2xhYkxjRXcvbzJNZ1ZqZlBpRmdWZm1aRDUrTUFINzhWTjRmTHY5UThMVEJhClVlckIwRzQ0M0loZVRRZzR6TUIydUpFQ2dZRUEyZmZXOGFISUUzRWxBYUR3WnBGSmN6Y1N0eEtoYVVzYkhaY0QKK3hpRUVMT0N1ZXJSdGtqQUNabHdIRUFMVnNaaXNUSDJGbk01VlFSTVBtbVJCOS9lb1RIYndHV1dPUTZOVVdkRQpnQ1poNlhjT2VSNUYwMmZiSVBhNVo1ZENtSGZXNTVSZk5zMEZJVFdEZHhwMEY3QjVpQUFBT3B3bnRmK2g5WWp1CkhlNHFqTFVDZ1lCMmtFM3lFQlo4N3BjRTA0a0lGSW5UV0JqbnpHbDl5bitIdThoNlZIRFo1Yld4NnBHSDh2emIKeGJaZzBZcTlDTHRKOFdudUo5OU5iK1RGZGIwQWRaeG5QUlZvQlo5TStvM00xeVJVMTdKckozaFBFeVpoMDJZeApLQmMzelJYcUQ5WXhzN0VuRE9TTndJQ0tmdGNoZW5YTVZnSGJHanVIdENkejNUelFwSUcvUXc9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: selfsigned
  labels:
    app: selfsigned
spec:
  replicas: 1
  selector:
    matchLabels:
      app: selfsigned
  template:
    metadata:
      labels:
        app: selfsigned
    spec:
      containers:
        - name: selfsigned
          image: nginx:alpine
          ports:
            - name: selfsigned
              containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: selfsigned
spec:
  type: ClusterIP
  selector:
    app: selfsigned
  ports:
    - name: selfsigned
      protocol: TCP
      port: 80
      targetPort: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: selfsigned
  annotations:
    # ADDED
    nginx.ingress.kubernetes.io/whitelist-source-range: 173.245.48.0/20, 103.21.244.0/22, 103.22.200.0/22, 103.31.4.0/22, 141.101.64.0/18, 108.162.192.0/18, 190.93.240.0/20, 188.114.96.0/20, 197.234.240.0/22, 198.41.128.0/17, 162.158.0.0/15, 104.16.0.0/13, 104.24.0.0/14, 172.64.0.0/13, 131.0.72.0/22, 2400:cb00::/32, 2606:4700::/32, 2803:f800::/32, 2405:b500::/32, 2405:8100::/32, 2a06:98c0::/29, 2c0f:f248::/32

spec:
  # ADDED
  tls:
    - hosts:
        - selfsigned.marchenko.net.ua
      secretName: selfsigned-tls

  rules:
    - host: selfsigned.marchenko.net.ua
      http:
        paths:
          - path: /
            pathType: ImplementationSpecific
            backend:
              service:
                name: selfsigned
                port:
                  number: 80

If for some reason something does not work check ingress logs with something like:

kubectl -n ingress logs -l name=nginx-ingress-microk8s

Also check what cetificate is for:

openssl x509 -in cloudflare.crt -text -noout

In my case I got bunch of errors complaining local SSL certificate cf/selfsigned-key was not found. Using default certificate because I did removed comments and make certificates in a single line, then because of a wrong secret type.

Now if you will try to connect everything should be ok:

curl -s -i -vv https://selfsigned.marchenko.net.ua

Buf if you will try to bypass cloudflare with help of:

curl -k -s -i --resolve selfsigned.marchenko.net.ua:443:178.20.154.97 https://selfsigned.marchenko.net.ua/

You will receive 403 forbidden

So we already have encryption everywhere plus connections are allowed only from Cloudflare

Strict - Letsencrypt

For strict to work we gonna need real certificates, so here is a demo just in case it will be needed in future

letsencrypt.yml

---
apiVersion: v1
kind: Secret
metadata:
  name: letsencrypt-tls
  # placeholder, will be filled in with certificates by cert-manager
type: Opaque
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: letsencrypt
  labels:
    app: letsencrypt
spec:
  replicas: 1
  selector:
    matchLabels:
      app: letsencrypt
  template:
    metadata:
      labels:
        app: letsencrypt
    spec:
      containers:
        - name: letsencrypt
          image: nginx:alpine
          ports:
            - name: letsencrypt
              containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: letsencrypt
spec:
  type: ClusterIP
  selector:
    app: letsencrypt
  ports:
    - name: letsencrypt
      protocol: TCP
      port: 80
      targetPort: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: letsencrypt
  annotations:
    # ADDED
    nginx.ingress.kubernetes.io/whitelist-source-range: 173.245.48.0/20, 103.21.244.0/22, 103.22.200.0/22, 103.31.4.0/22, 141.101.64.0/18, 108.162.192.0/18, 190.93.240.0/20, 188.114.96.0/20, 197.234.240.0/22, 198.41.128.0/17, 162.158.0.0/15, 104.16.0.0/13, 104.24.0.0/14, 172.64.0.0/13, 131.0.72.0/22, 2400:cb00::/32, 2606:4700::/32, 2803:f800::/32, 2405:b500::/32, 2405:8100::/32, 2a06:98c0::/29, 2c0f:f248::/32
    # ask cert-manager to create certificates for us
    cert-manager.io/cluster-issuer: 'letsencrypt'

spec:
  # ADDED
  tls:
    - hosts:
        - letsencrypt.marchenko.net.ua
      secretName: letsencrypt-tls

  rules:
    - host: letsencrypt.marchenko.net.ua
      http:
        paths:
          - path: /
            pathType: ImplementationSpecific
            backend:
              service:
                name: letsencrypt
                port:
                  number: 80

The check will be the same as in previous example

Strick - Cloudflare

And now is a cool part, in Cloudflare under SSL/TLS there is an option to create certificates for your origin server which will be trusted by Cloudflare and will be signed for years

origin

After creating certificate Cloudflare will create certificate and key for you, so you can follow exactly same approach as in flexible/selfsigned example

Deployment and checks are the same

Authenticate

Navigate to teams dashboard to add application and its authentication providers

There is no hidden knowledge and everything is done in just few clicks including identity providers

applications

applauncher

What is cool you can configure different rules per application/group/region/whatever

Whenever you will try open any of your apps, you will be redirected to choosen identity provider for authentication and only after redirected back to an app, and because we are not allowing bypassing Cloudflare its the same as disalowing anonymous access at all.

Also there few other cool features in Cloudflare Acess like Gateways and Tunnels which may make even crazier things