|
| 1 | +--- |
| 2 | +layout: post |
| 3 | +title: "Host own CA for K3S" |
| 4 | +date: 2025-10-21 22:51:05 +0800 |
| 5 | +categories: K3S |
| 6 | +--- |
| 7 | +My K3S is not exposed to the Internet. Therefore, I never felt the necesity to use HTTPS for all WebUIs. Also because it is hard to use Let's Encrypt in this case. |
| 8 | + |
| 9 | +However, when I setup Nextcloud to backup my desktop, the client required the transmission to be TLS encrypted. This led to this article, hosting an own CA. |
| 10 | + |
| 11 | +I do not know if there are other options, I use **Step CA** from *smallstep.com*. |
| 12 | + |
| 13 | +1. Install **step-ca**/**step**. |
| 14 | + |
| 15 | + 1. Generate step-ca helm values. |
| 16 | + |
| 17 | + `step ca init --helm` |
| 18 | + |
| 19 | + Be careful with those questions. Some are tied to other configurations that are not here or changeable. |
| 20 | + |
| 21 | + ```text |
| 22 | + ✔ Deployment Type: Standalone |
| 23 | + |
| 24 | + What would you like to name your new PKI? |
| 25 | + ✔ (e.g. Smallstep): Smallstep |
| 26 | + |
| 27 | + What DNS names or IP addresses would you like to add to your new CA? |
| 28 | + # This is the only available value in K8S setup, except the namespace part. |
| 29 | + ✔ (e.g. ca.smallstep.com[,1.1.1.1,etc.]): step-certificates.default.svc.cluster.local |
| 30 | + |
| 31 | + What IP and port will your new CA bind to (it should match service.targetPort)? |
| 32 | + # The service is on 9000 by default. Change the value here does not affect the service. |
| 33 | + ✔ (e.g. :443 or 127.0.0.1:443): :9000 |
| 34 | + |
| 35 | + What would you like to name the CA's first provisioner? |
| 36 | + # Unlike said in doc, a pre-configured "admin" provisioner, this is the only one in helm installation. |
| 37 | + ✔ (e.g. you@smallstep.com): me@example.org |
| 38 | + |
| 39 | + Choose a password for your CA keys and first provisioner. |
| 40 | + # If chose "generate", the password is shown until press Enter. Copy it and base64 it for used later. |
| 41 | + ✔ [leave empty and we'll generate one]: |
| 42 | + ``` |
| 43 | +
|
| 44 | + 2. Install step-ca |
| 45 | +
|
| 46 | + `helm upgrade -i step-certificates smallstep/step-certificates -f step-ca-values.yaml --set inject.secrets.ca_password="${BASE64_PASSWORD_FROM_ABOVE}" --set inject.secrets.provisioner_password="${BASE64_PASSWORD_FROM_ABOVE}"/` |
| 47 | +
|
| 48 | +2. Install **step-issuer**. |
| 49 | +
|
| 50 | + 1. `helm install step-issuer smallstep/step-issuer` |
| 51 | +
|
| 52 | + 2. Setup the issuer. |
| 53 | +
|
| 54 | + ```Shell |
| 55 | + #!/usr/bin/env bash |
| 56 | + set -eu -o pipefail |
| 57 | + |
| 58 | + # Same as the one in answer. |
| 59 | + CA_URL=https://step-certificates.default.svc.cluster.local |
| 60 | + CA_ROOT_B64=$(kubectl get -o jsonpath="{.data['root_ca\.crt']}" configmaps/step-certificates-certs | step base64) |
| 61 | + # Same as the one in answer. |
| 62 | + CA_PROVISIONER_NAME=magicloud@magicloud.lan |
| 63 | + CA_PROVISIONER_KID=$(kubectl get -o jsonpath="{.data['ca\.json']}" configmaps/step-certificates-config | jq -r .authority.provisioners[0].key.kid) |
| 64 | + |
| 65 | + kubectl apply -f - << EOF |
| 66 | + --- |
| 67 | + apiVersion: certmanager.step.sm/v1beta1 |
| 68 | + kind: StepClusterIssuer |
| 69 | + metadata: |
| 70 | + name: step-issuer |
| 71 | + spec: |
| 72 | + # The CA URL: |
| 73 | + url: $CA_URL |
| 74 | + # The base64 encoded version of the CA root certificate in PEM format: |
| 75 | + caBundle: $CA_ROOT_B64 |
| 76 | + # The provisioner name, kid, and a reference to the provisioner password secret: |
| 77 | + provisioner: |
| 78 | + name: $CA_PROVISIONER_NAME |
| 79 | + kid: $CA_PROVISIONER_KID |
| 80 | + passwordRef: |
| 81 | + name: step-certificates-provisioner-password |
| 82 | + namespace: default |
| 83 | + key: password |
| 84 | + --- |
| 85 | + EOF |
| 86 | + ``` |
| 87 | +
|
| 88 | +3. Install **cert-manager**. |
| 89 | +
|
| 90 | + `kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.19.1/cert-manager.yaml` |
| 91 | +
|
| 92 | +4. Use Step CA for Ingress. |
| 93 | +
|
| 94 | + The above steps create a `StepClusterIssuer`. Note that it is not a `ClusterIssuer`, since step-ca is an out of tree issuer. Its usage differs slightly from that of the in tree ones, such as ACME. |
| 95 | +
|
| 96 | + There are three annotations to be used in Ingress for this case. According to the documentation, I think two of them are enough. |
| 97 | +
|
| 98 | + ```YAML |
| 99 | + annotations: |
| 100 | + # `step-issuer` is the name created above. |
| 101 | + cert-manager.io/issuer: step-issuer |
| 102 | + # Either of the following should work. |
| 103 | + cert-manager.io/issuer-group: certmanager.step.sm |
| 104 | + cert-manager.io/issuer-kind: StepClusterIssuer |
| 105 | + ``` |
| 106 | +
|
| 107 | +5. Expose the Step CA interface outside the cluster. |
| 108 | +
|
| 109 | + The interface itself is protected by a cert signed by the current CA. Its SAN is apparently the service address. However, Traefik does not know about the CA, and Traefik requires the target must be in the SAN while the target is the pod (not the service) with its IP address. Therefore, Traefik refuses to perform ingressing for Step CA. |
| 110 | +
|
| 111 | + The solution is using `IngressRouteTCP` with `spec.tls.passthrough`. |
| 112 | +
|
| 113 | + ```YAML |
| 114 | + apiVersion: traefik.io/v1alpha1 |
| 115 | + kind: IngressRouteTCP |
| 116 | + metadata: |
| 117 | + name: step-ca |
| 118 | + spec: |
| 119 | + routes: |
| 120 | + - match: HostSNI(`step-ca.magicloud.lan`) |
| 121 | + services: |
| 122 | + - name: step-certificates |
| 123 | + port: 443 |
| 124 | + tls: |
| 125 | + passthrough: true |
| 126 | + ``` |
| 127 | +
|
| 128 | + This leads to another issue, ExternalDNS does not know how to monitor this object. Therefore, we need its CRD. |
| 129 | +
|
| 130 | + First, apply [the dnsendpoint CRD](https://raw.githubusercontent.com/kubernetes-sigs/external-dns/master/config/crd/standard/dnsendpoints.externaldns.k8s.io.yaml). Then in the ExternalDNS helm values file, add the following and upgrade. `service` and `ingress` are the default values, we need `crd`. |
| 131 | +
|
| 132 | + ```YAML |
| 133 | + sources: |
| 134 | + - service |
| 135 | + - ingress |
| 136 | + - crd |
| 137 | + ``` |
| 138 | +
|
| 139 | + Now create the DNS record. |
| 140 | +
|
| 141 | + ```YAML |
| 142 | + apiVersion: externaldns.k8s.io/v1alpha1 |
| 143 | + kind: DNSEndpoint |
| 144 | + metadata: |
| 145 | + name: step-ca |
| 146 | + spec: |
| 147 | + endpoints: |
| 148 | + - dnsName: step-ca.magicloud.lan |
| 149 | + recordType: A |
| 150 | + targets: |
| 151 | + - 192.168.0.102 |
| 152 | + ``` |
| 153 | +
|
| 154 | +6. Install the CA. |
| 155 | +
|
| 156 | + In theory, the CA should be installed everywhere. For other pods in the cluster, the tool is`autocert`. However, existing pods are problematic. Luckily I do not need that. All I need to do is install the CA to my desktop so that it can communicate with the secured services in the cluster. |
| 157 | +
|
| 158 | + After installing the `step` tool on the desktop, run the command `step ca bootstrap --ca-url step-ca.magicloud.lan --fingerprint $FINGER_PRINT` to initialize. `$FINGER_PRINT` could be found in the first few lines of the step-certificates pod log. If you missed viewing the log, you can also use the command `step certificate fingerprint $(step path)/certs/root_ca.crt` within the step-certificates pod to find it. Then run the command `step certificate install $CRT_PATH_SHOWED_IN_LAST_STEP`. |
0 commit comments