Kubernetes Https Www Redirect Catch-All Service

What will I learn from this?

How to, in a Kubernetes cluster running Traefik as its Ingress controller:

  1. Ensure that all non-https URLs get redirected to https using Cloudflare
  2. Ensure that all non-www root URLs get redirected to www

Cloudflare

Cloudflare is a CDN and DNS provider that has a free tier which includes HTTPS between the browser and their servers. This allows you to provide your services over HTTPS without having to deal with certificates yourself. You might need or want to handle certificates yourself and ensure that communication between your cluster and your cdn is encrypted - this blog post is not for you! If you want fast and simple SSL encryption for your site then:

  1. Create a Cloudflare account
  2. Add your domain to it
  3. Point that domain at your cluster’s IP address
  4. Go to the Crypto tab
  5. Turn on ‘Always Use HTTPS’

Disappointed that this isn’t being handled by Kubernetes? Me too. I spent a good few hours trying to get Traefik to enforce both https and www without Cloudflare being involved and what I mostly got for my effort was infinite redirects or server errors. So I turned Cloudflare back on and left it at that. I’d love it if my cluster handled this all itself but honestly using Cloudflare or similar is faster and easier. I did look at using cert-manager and Let’s Encrypt but I wasn’t able to get it working easily so I decided to use my time elsewhere and let Cloudflare handle it. One less thing for me to worry about.

I’m certain that it’s possible and that I’m just missing some crucial knowledge, but I’ve spent enough time trying to fix something that isn’t broken.

Enforcing www. with Traefik and NGINX

This is actually handled by Kubernetes.

I use Traefik as my ingress controller, primarily because it does not require an external loadbalancer to function. In an ideal world I’d be using NGINX like the cool kids but keeping things cheap appeals to me more. It was also very easy to get running.

Something I found I was unable to do with Traefik was easily configure it to enforce www for my domains. After asking for help on /r/kubernetes here I was advised to run a service in the cluster that would become a last-ditch attempt to handle a URL before returning a 404.

Code

The catchall service is handled by a very small Docker image running a very simple NGINX configuration - that’s the Dockerfile and redirects.conf blocks below. The rest of the code samples are just the Kubernetes config required to get the service up and running.

Dockerfile

1
2
3
FROM nginx:alpine
COPY ./redirects.conf /etc/nginx/conf.d/default.conf
CMD nginx -g "daemon off;"
  1. Use the official NGINX alpine docker image
  2. Copy the NGINX config into the image
  3. Start NGINX with the daemon off command to run NGINX in the foreground (details below)

redirects.conf (NGINX configuration)

1
2
3
4
server {
listen 80;
return 301 https://www.$host$request_uri;
}
  1. Listen on port 80
  2. Permanently redirect the url with the prefix https://www.

Kubernetes yaml

Ingress
1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: catchall
annotations:
kubernetes.io/ingress.class: traefik
spec:
rules:
- http:
paths:
- path: /
backend:
serviceName: catchall
servicePort: 80
Deployment
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: apps/v1
kind: Deployment
metadata:
name: catchall
labels:
app: catchall
spec:
replicas: 1
selector:
matchLabels:
app: catchall
template:
metadata:
labels:
app: catchall
spec:
containers:
- name: catchall
image: terrarum/nginx-catchall:latest
ports:
- containerPort: 80
Service
1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: v1
kind: Service
metadata:
name: catchall
labels:
app: catchall
spec:
type: NodePort
ports:
- name: http
port: 80
targetPort: 80
selector:
app: catchall

The Deployment and Service are pretty standard so I won’t explain what’s going on in those.

The Ingress is where the magic happens - because it does not define a host it receives any request that does not match any existing Ingress that does define a host.

A More Detailed Explanation

If you kind of know what you’re doing in Kubernetes the above should have pointed you in the right direction, but I’ll expand on some things here.

Dockerfile

nginx:alpine

This is the official build of NGINX using Alpine Linux. This gives us a powerful url rewriting tool inside a very small Docker image - exactly what we want for a passive catchall service that ideally never gets hit.

COPY ./redirects.conf /etc/nginx/conf.d/default.conf

This should be pretty self-explanatory but for the sake of completion: this copies the redirects.conf NGINX configuration into the Docker image. This allows us to download a stock official Docker image and customise it.

CMD nginx -g "daemon off;"

This runs nginx with the deamon off argument set globally (-g). If you just run nginx, the process that this spawns creates a daemon that runs NGINX in a new process and then quits your original nginx process. When the command quits Docker goes sees this as the process exiting and then kills the container. In order for the Docker image to run forever we need the nginx command to run in a foreground process which the above command accomplishes.

redirects.conf

return 301 https://www.$host$request_uri;

NGINX gives you the $host and $request_uri variables to use in your config. Let’s say I was hosting https://www.example.com and someone visits http://example.com/about?utm_source=abc123:

  • $host would equal example.com
  • $request_uri would equal /about?utm_source=abc123

So the final result would be https://www.example.com/about?utm_source=abc123.

This also work for https://example.com, but does not work for http://www.example.com. At the moment I am using Cloudflare as my CDN/DNS provider and have enabled their ‘Always Use HTTPS’ option as I was unable to find a clean way to enforce https purely inside Traefik. Right now if I enable

Kubernetes

As stated above the Deployment and Service files are not doing anything special so explaining them is outside the scope of this post.

Ingress

Kubernetes Ingresses describe the hosts which they apply to like so:

1
2
3
4
5
6
7
8
9
spec:
rules:
- host: www.helm108.com
http:
paths:
- path: /
backend:
serviceName: helm108
servicePort: http

This instructs Kubernetes to forward any requests for www.helm108.com to the helm108 service.

Our catchall Ingress looks like this:

1
2
3
4
5
6
7
8
spec:
rules:
- http:
paths:
- path: /
backend:
serviceName: catchall
servicePort: 80

Almost identical, just missing a host. This makes it match any request that is not caught by a specific ingress such as the helm108.com ingress.