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