Janw.xyz

Doing things on the internet.

How to set up a custom domain name and TLS certificate for Plex running on Kubernetes

The holy grail: Plex playing music to a remote IPv6 client via a secure connection
The holy grail: Plex playing music to a remote IPv6 client via a secure connection

My Plex server’s remote access setup finally reached a state that I’m happy with, and I want to share that here. The idea here is: I want to expose the server outside of my home network so that I can listen to my music on the go, and do so seamlessly and via TLS encrypted IPv4 and IPv6 connections. Especially the latter is an issue that Plex has not solved yet; the official Remote Access functionality in Plex only supports IPv4. But I got there, and this is roughly how I did it:

With a Fritz!Box as a router and its MyFRITZ!Net service acting as a dynamic DNS service, exposing Plex via IPv4 and IPv6 _outside of the official Remote Access functionality is actually pretty straight-forward. The MyFRITZ!Net service gives every router a unique DNS name à la asdf1234.myfritz.net that receives updated A and AAAA records, every time the Fritz!Box receives a new IP address assignment. That way services such as DuckDNS are not necessary; all I need to do is create the correct port-forwardings, add the DNS name as a custom server access URL in Plex and presto, the server is exposed to the outside world.

But this does not solve TLS encryption for me. With MyFRITZ!Net there is no way I can present a valid TLS certificate for asdf1234.myfritz.net from my homeserver. It’s not my domain after all. Hence, all connections towards the server, while working, will be “downgraded” to unencrypted due to the certificate mismatch. Now, to get around that, I created a CNAME record pointing to asdf1234.myfritz.net. on one of the domains that I own, allowing me to have TLS certificates issued for it by Let’s Encrypt (LE). This way, I just need to give Plex the LE certificate to present.

Kubernetes setup

This is where it gets interesting though, because my homeserver is running Kubernetes, using the lightweight distro k3s. Within Kubernetes, I’m running cert-manager (with a ClusterIssuer configured for Let’s Encrypt) to make the certificate juggling possible.

Please note that the following manifest snippets show only the relevant portions required to follow along. The full manifests can be found at the end of the article:

We start off by creating a Certificate resource for the DNS name:

 1apiVersion: cert-manager.io/v1
 2kind: Certificate
 3metadata:
 4  name: plex-janw-xyz-tls
 5  namespace: default
 6spec:
 7  dnsNames:
 8    - "plex.janw.xyz"
 9  issuerRef:
10    name: le-issuer-dns
11    kind: ClusterIssuer
12  secretName: plex-janw-xyz-tls

With that in place, I can now pass the certificate to Plex. Note here, that cert-manager currently does not generate PKCS#12-format certificate files but PEMs, while Plex expects PKCS#12 and does not work with PEMs. Therefore just mounting the cert’s Secret into the Plex container is not enough. We first must convert the format, and we do so in an initContainer:

 1initContainers:
 2  - name: convert-cert
 3    image: plexinc/pms-docker
 4    command: [bash]
 5    args:
 6      - c
 7      - |
 8        openssl \
 9          pkcs12 \
10          -export \
11          -out /tls-pkcs/tls.pfx \
12          -in /tls-cert/tls.crt \
13          -inkey /tls-cert/tls.key \
14          -password pass:SOMESECRET \
15          -name plex.janw.xyz \
16          -certpbe AES-256-CBC \
17          -keypbe AES-256-CBC \
18          -macalg SHA256
19
20        chmod 755 /tls-pkcs/tls.pfx
21        ls -lh /tls-pkcs/        
22    volumeMounts:
23      - mountPath: /tls-cert
24        name: tls-cert
25      - mountPath: /tls-pkcs
26        name: tls-pkcs
27  # …
28volumes:
29  # …
30  - name: tls-cert
31    secret:
32      secretName: myplex-tls
33  - name: tls-pkcs
34    emptyDir: {}

The script in the initContainer will convert the certificate and place it in the tls-pkcs emptyDir volume. This volume can now be passed to the Plex container itself:

 1containers:
 2  - name: plex
 3    image: plexinc/pms-docker
 4    volumeMounts:
 5      # …
 6      - mountPath: /tls-pkcs
 7        name: tls-pkcs
 8    ports:
 9      - name: pms
10        containerPort: 32400
11volumes:
12  # …
13  - name: tls-cert
14    secret:
15      secretName: plex-janw-xyz-tls
16  - name: tls-pkcs
17    emptyDir: {}

Plex settings UI

Now the certificate is available to Plex at /tls-pkcs/tls.pfx, and encrypted with the password SOMESECRET. Those details can be entered in the Plex server network settings now:

Custom certificate configuration in Plex’s network settings
Custom certificate configuration in Plex’s network settings

Further down on the settings page the custom server access URL must also be adjusted. Here we need the full external URL, including the port used in the forwarding setup:

Custom server access URL configuration in Plex’s network settings
Custom server access URL configuration in Plex’s network settings

Finally we’ll have to restart Plex, because it will load the certificate only on startup. After a rollout the certificate will be picked up though, and Plex should reachable via the new external hostname.

Notes on certificate renewal

You might have noticed the statefulset’s reloader.stakater.com/auto: "true" annotation. It belongs to Stakater Reloader, a light-weight controller that automatically performs rolling updates on workloads when the ConfigMaps or Secrets they reference are updated. It is particularly useful, for example, when you are doing configuration changes in a ConfigMap but otherwise don’t modify the Deployment that references it. Kubernetes would normally not perform a rollout because the workload did not change, and if there is no dedicated mechanism to reload the config, the workload continues running with the previous configuration. Reloader practically replaces a dedicated config reload mechanism by watching the ConfigMap itself, and doing a rolling update when it changes, thus ensuring the workload adopts the new configuration.

In the setup described above I’m relying on Reloader to be a crude “certificate reloader” for the time when the Let’s Encrypt certificate is renewed. Unfortunately Plex does not support reloading its custom certificate, it is only loaded once at startup time. With the Reloader in place, the renewal will cause the certificate’s Secret to be updated, which in turn causes a new rollout of Plex, now with the renewed certificate. This is far from ideal, because the renewal can happen at any time, while you’re watching something, and that would cause the stream to be interrupted. But in my opinion it is still better than manually restarting the server whenever the renewal occurs.

Full manifests

Tap/click here to expand the full manifests
 1---
 2apiVersion: cert-manager.io/v1
 3kind: Certificate
 4metadata:
 5  name: plex-janw-xyz-tls
 6  namespace: default
 7spec:
 8  dnsNames:
 9    - "plex.janw.xyz"
10  issuerRef:
11    name: letsencrypt-issuer
12    kind: ClusterIssuer
13  secretName: plex-janw-xyz-tls
14
15---
16kind: StatefulSet
17apiVersion: apps/v1
18metadata:
19  name: plex
20  namespace: default
21  annotations:
22    reloader.stakater.com/auto: "true"
23spec:
24  replicas: 1
25  serviceName: plex
26  selector:
27    matchLabels:
28      app: plex
29  template:
30    metadata:
31      labels:
32        app: plex
33        app.kubernetes.io/name: plex
34    spec:
35      hostNetwork: true
36      initContainers:
37        - name: convert-cert
38          image: plexinc/pms-docker
39          command: [bash]
40          args:
41            - -c
42            - |
43              openssl \
44                pkcs12 \
45                -export \
46                -out /tls-pkcs/tls.pfx \
47                -in /tls-cert/tls.crt \
48                -inkey /tls-cert/tls.key \
49                -password pass:SOMESECRET \
50                -name plex.janw.xyz \
51                -certpbe AES-256-CBC \
52                -keypbe AES-256-CBC \
53                -macalg SHA256
54
55              chmod 755 /tls-pkcs/tls.pfx
56              ls -lh /tls-pkcs/              
57
58          volumeMounts:
59            - mountPath: /tls-cert
60              name: tls-cert
61            - mountPath: /tls-pkcs
62              name: tls-pkcs
63      containers:
64        - name: plex
65          image: plexinc/pms-docker
66          resources:
67          volumeMounts:
68            - mountPath: /media
69              name: media-vol
70              readOnly: true
71            - mountPath: /transcode
72              name: transcode-vol
73            - mountPath: /config
74              name: config-vol
75            - mountPath: /tls-pkcs
76              name: tls-pkcs
77          ports:
78            - name: pms
79              containerPort: 32400
80          livenessProbe:
81            httpGet:
82              path: /identity
83              port: 32400
84      volumes:
85        - name: media-vol
86          hostPath:
87            path: /mnt/media
88        - name: transcode-vol
89          hostPath:
90            path: /mnt/cache
91        - name: config-vol
92          hostPath:
93            path: /mnt/config
94        - name: tls-cert
95          secret:
96            secretName: plex-janw-xyz-tls
97        - name: tls-pkcs
98          emptyDir: {}

Switch between Fn and Media keys automatically on macOS

Expanded menu of Fluor to configure its behavior
Expanded menu of Fluor to configure its behavior

Have you ever been going back and forth about the most appropriate default function key behavior on the Mac? Going back and forth about it, because after you thought you found it your setting, it turned out that your muscle memory is just set on Fn keys for some applications, while in others you just must have media keys, and you end up frustrated with both?

Fair enough. But I have. And with Fluor I finally found a solution to that! Fluor allows you to set up a default Fn key behavior on a per-app basis. As you can see above, I have my Mac set to Media keys by default (in keeping with the OS’s initial value) but for developer tools, I use Fluor change that to Fn keys on the fly. That way, when I switch the active application to Visual Studio Code for example, I have its debugger hot keys right at my fingertips, without the need to press the fn button.

Syntax-aware redefinition of kill-word in IPython

Although IPython to me is the best of all the Python REPLs, there is something that bothered me about it for a while: Alt-Backspace is one of the shortcuts I use most. In many shell contexts, as well as generally in text editing, it removes the last word behind the cursor, i.e. the last word I wrote. It is a very useful shortcut to fix missspelled words or when after writing it I notice a word does not really work in a context after all and I want to replace it. This removal the last word is often called the “kill-word command”.

Read more …

Schöne Gendergaps mit LaTeX erzeugen

Kürzlich habe ich festgestellt, wie furchtbar ein Gendergap in LaTeX aussieht, wenn es auf üblichem Wege (mittels Unterstrich \textunderscore) erzeugt wird. Besonders stört mich daran, dass der Unterstrich in der Schriftart Computer Modern (dem LaTeX-Default) ungewöhnlich lang ist und etwas zu tief steht. Hinzu kommt, dass in der deutschen Silbentrennung vor einem Unterstrich natürlich kein Wortumbruch vorgesehen ist, auch wenn dies bei langen ge-genderten durchaus sinnvoll sein kann. Besonders die “_innen”-Endung ist lang genug, um den sauberen Blocksatz für LaTeX unmöglich zu machen.
Weiterlesen …

Prevent macOS from automatically mounting external drives

Just recently I have added an external USB harddisk to my desktop setup. I keep my Lightroom photo library on it, since keeping the photos on my NAS and mounting that to work on the pictures is just too slow. Obviously I am not spending every day working on those pictures, just every now and then, yet still I’d like to keep the HDD connected to the OWC USB-C Dock that connects all the accessories to my MacBook Adorable (a differentiating phrase initially coined by David Smith) when I’m seated.
Read more …