Homelab PKI: Caddy CA + HashiCorp Vault + cert-manager on Kubernetes

This post documents how to set up a proper PKI for a homelab environment by importing an existing Caddy CA into HashiCorp Vault, and wiring it up with cert-manager on Kubernetes so that services automatically get trusted TLS certificates. Overview The trust chain we’re building: Caddy Root CA → Vault PKI → cert-manager → Kubernetes Secret (tls.crt / tls.key) Assumptions: Caddy is running as a reverse proxy and already has an internal CA Caddy is running outside the Kubernetes cluster on a dedicated host Vault is running outside the Kubernetes cluster (e.g. in Docker) cert-manager is installed in the Kubernetes cluster Internal domain is fritz.box, my public domain is dannihome.de Step 1: Locate Caddy’s CA Certificate and Key Caddy stores its internal CA here by default: ...

March 1, 2026 · 4 min · Jens

Resolving Local Hostnames in k3s Using PiHole and CoreDNS

If you’re running a k3s cluster in your home network like me, you might run into a common problem: your pods can’t resolve local hostnames like keycloak.fritz.box or nas.fritz.box. That’s because CoreDNS inside the cluster doesn’t know about your local DNS server. In my case, I run a PiHole instance on a Raspberry Pi that handles DNS for my home network, including custom hostnames under the .fritz.box domain. Here’s how I made those hostnames available to all pods in my k3s cluster. ...

February 16, 2026 · 3 min · Jens

Manage secrets with Vault and K8S

The goal is to replicate values from a Vault secret to K8S. There is a K8S operator called external-secrets (ESO) for this purpose. It can be deployed quite conveniently with Helm: helm repo add external-secrets https://charts.external-secrets.io helm install external-secrets external-secrets/external-secrets -n external-secrets --create-namespace Next, we should first create a test secret in Vault. vault kv put secret/foo my-value=mytopsecretvalue On to the manifests! The SecretStore: Connecting Kubernetes to Vault apiVersion: external-secrets.io/v1 kind: SecretStore metadata: name: vault-backend spec: provider: vault: server: "https://vault.myhomenet.lan" path: "secret" version: "v2" caBundle: "..." # Base64-encoded CA certificate, see explanation below auth: tokenSecretRef: name: "vault-token" key: "token" What’s happening here? ...

September 15, 2025 · 2 min · Jens

Deploying Postgres and pgAdmin to Docker Swarm

How to create volumes and deploy Postgresql and pgAdmin mkdir -p /disk1/postgres_data mkdir -p /disk2/postgres_backup chmod -R 777 /disk1/postgres_data chmod -R 777 /disk2/postgres_backup Docker Swarm deployment manifest version: "3.8" services: postgres: image: postgres:15 deploy: placement: constraints: - "node.hostname == swarmnode1" volumes: - /disk1/postgres_data:/var/lib/postgresql/data - /disk2/postgres_backup:/backup environment: POSTGRES_USER: myuser POSTGRES_PASSWORD: mypassword POSTGRES_DB: mydatabase networks: - my_network ports: - "5432:5432" pgadmin: image: dpage/pgadmin4 deploy: placement: constraints: - "node.hostname == swarmnode1" volumes: - pgadmin_data:/var/lib/pgadmin environment: PGADMIN_DEFAULT_EMAIL: admin@example.com PGADMIN_DEFAULT_PASSWORD: adminpassword ports: - "7676:80" networks: - my_network networks: my_network: driver: overlay volumes: pgadmin_data:

February 2, 2025 · 1 min · Jens

Display Thymeleaf server side field validation error with bootstrap

How to display server side field validation errors with Bootstrap and Thymeleaf <form action="#" th:action="@{/}" th:object="${myForm}" method="post"> <!-- 'myform' is the form object --> <!-- start row 1 --> <div class="form row"> <!-- col 1 --> <div class="col-lg-6"> <div class="form-group"> <label th:for="*{firstname}">First name</label> <input th:field="*{firstname}" type="text" class="form-control" th:classappend="${#fields.hasErrors('firstname')}? is-invalid" <!-- adds this class in case there is a field error--> placeholder="first name" aria-describedby="firstnameFeedback"> <!-- see server-side validation from Bootstrap docs --> <div id="firstnameFeedback" th:if="${#fields.hasErrors('firstname')}" th:errors="*{firstname}" class="invalid-feedback"> <!-- is initially not displayed --> </div> </div> (...)

April 29, 2024 · 1 min · Jens

Add Traefik reverse proxy to Kubernetes

This task was astonishingly hard to configure. In my K3S cluster I have a Traefik reverse proxy deployed. What I wanted to achieve was: Make my apps accessible from the internet Automate TLS certificate provision Protect apps with basic auth Step 1 involved opening http and https ports to my clusters master node IP address. Traefik was quite easily deployed through its Helm-Chart. This is my values.yaml. As Ionos is my domain hoster, I’m using their DNS challenge provider to generate Let’s Encrypt-Certificates: ...

February 24, 2024 · 2 min · Jens

Avoid a mistake when extending Spring Data Repository

A little stupid mistake of mine that cost me some time. My application context would not start due to java.lang.IllegalArgumentException: Not an managed type: class java.lang.Object My interface looked like this: public interface MyUserRepository<MyUser, Long> extends JpaRepository<MyUser, Long> The correct definition, omitting the generic duplication which causes the error: public interface MyUserRepository extends JpaRepository<MyUser, Long>

September 5, 2023 · 1 min · Jens

Restoring deleted files from Git

Cheat-Sheet for restoring deleted files from Git repo #Get a list of deleted files git log --diff-filter=D --summary | grep delete #Show commits where the deleted file was involved, copy latest commit hash to clipboard git log --all -- <PATH_TO_DELETED_FILE> #Restore it into the working copy with: (^ means the commit BEFORE the commit where file was deleted) git checkout <COMMIT-HASH>^ -- <PATH_TO_DELETED_FILE>

February 5, 2022 · 1 min · Jens

Enabling and using Minikube Docker registry

Cheat-Sheet to enable and use Minikube internal Docker registry Enable access to insecure registry On Docker host machine, create or edit /etc/docker/daemon.json: { "insecure-registries" : ["192.168.49.2:5000"] } Save and restart Docker. Delete an existing Minikube cluster: minikube stop && minikube delete Start minikube with insecure registry access enabled: minikube start --insecure-registry "10.0.0.0/24" Enable the registry addon: minikube addons enable registry Tag an existing image and push it to minikube registry: ...

January 25, 2022 · 1 min · Jens

Testing Dockerized Webapp With Cucumber And Selenium

My task is to test a dockerized web application using Selenium. It is important that the tests are defined with Gherkin and of course run headless on Jenkins. Here is what I did to achieve this task. Cucumber And Testcontainer Specifics This is a snippet from the class responsible to pull and instantiate the Webapp container which contains the build of my webapp to test: @Slf4j public class WebappContainer { private static WebappContainer instance = new WebappContainer(); private static final String NETWORK_ALIAS = "WEBAPP"; private static final int EXPOSED_PORT = 7654; private static final DockerImageName dockerImageName = DockerImageName .parse("myecr.amazonaws.com/webapp/my-little-webapp:" + getWebappImageVersion()); public static final GenericContainer<?> container = new GenericContainer<>(dockerImageName) .withNetwork(NetworkUtils.getNetwork()) .withNetworkAliases(NETWORK_ALIAS) .waitingFor(Wait.forHttp("/").forStatusCode(200).forPort(PORT) .withStartupTimeout(Duration.ofSeconds(STARTUP_TIMEOUT))) .withExposedPorts(EXPOSED_PORT) .withLogConsumer(new Slf4jLogConsumer(log)) // next line maybe specific to my setup: Mount Spring Boot test config into container .withClasspathResourceMapping("application-test.yml", "/etc/config/application.yml", BindMode.READ_ONLY); private WebappContainer() { } public static WebappContainer getInstance() { return instance; } public void start() { container.start(); } public void stop() { container.stop(); } public boolean isRunning() { return container.isRunning(); } public int getHttpPort() { return container.getMappedPort(EXPOSED_PORT); } } For completeness sake here the static helper method to get the image ...

June 9, 2021 · 3 min · Jens