This week was primarily about making the DMZ cluster more useful without making it less understandable. I added two services to the GitOps graph, SearXNG and an Authentik remote-cluster outpost, then worked through the integration failures that appeared when their manifests met the real environment. I also removed an unnecessary internal registry dependency from cert-manager in every cluster environment. Alongside the homelab work, I initialized Durfuel, a private fuel-tracking application with a deliberately explicit API, database, and local operating model.
The GitOps repository saw 19 commits touching 31 files across dev, DMZ, infra, and production. Several of those are merge commits. I treat the corresponding implementation commits as the substantive change and the merges as the record that the work reached main. This matters in a weekly roundup because it avoids describing the same manifest change twice while still preserving the deployment history. The outcome is a better-defined DMZ application path: Argo CD owns delivery, External Secrets uses the cluster’s actual Vault store name, SearXNG starts despite upstream engine restrictions, and Authentik has a dedicated component to serve a remote Kubernetes environment.
Registering SearXNG With Argo CD
I added an Argo CD Application named searxng in the infrastructure cluster. The application takes its source from the main revision of https://gitlab.durp.info/durfy/homelab/gitops.git, at dmz/searxng, and deploys it to the searxng namespace on the registered dmz destination cluster. The application uses directory recursion, so the chart and its manifests are delivered as a coherent unit rather than requiring a second manually maintained application definition.
The sync policy is intentionally the normal GitOps posture: automated sync, pruning, and self-healing, with CreateNamespace=true. Automatic sync removes the need for an operator to notice every desired-state revision and press a button. Prune is equally important because a repository is not authoritative if deleted resources remain alive indefinitely. Self-healing completes the contract by returning a changed live object to the declared manifest on the next reconciliation. CreateNamespace=true makes the first deployment repeatable, which is the only time an otherwise ordinary namespace omission becomes a bootstrap failure.
This application registration is small, but it is the boundary that matters. I do not want a service to exist because an ad hoc Helm command happened to work from an administrator workstation. The service should exist because its configuration is committed, Argo CD can observe it, and the environment can converge back to it. That gives the DMZ cluster a known owner for SearXNG’s resources and makes removal just as explicit as installation.
SearXNG is a metasearch service, so its useful behavior depends on a long chain outside the pod: DNS, egress routing, the remote engines, and the engine configuration syntax accepted by the installed SearXNG version. That is different from deploying a purely internal application. A Kubernetes deployment can be healthy while search is operationally broken. The changes this week were therefore focused on making the startup path and secret dependency accurate, rather than pretending that a rendered chart is the entire service.
Correcting the SearXNG Secret Boundary
The first configuration correction changed the ExternalSecret reference from a ClusterSecretStore named vault to vault-backend. The target Kubernetes Secret remains searxng-secret; what changed is the controller object used to retrieve the source material. This is a root-cause fix. External Secrets resolves the name declared in secretStoreRef, and a valid Vault server somewhere in the environment does not help when the referenced Kubernetes store object has a different name.
Using a cluster-scoped store is a useful fit for this deployment model. It separates the provider setup and trust configuration from the application’s declaration of the values it needs. The SearXNG chart asks for a named secret; the External Secrets controller reads it through the established Vault backend and materializes the Kubernetes Secret. That keeps secret values out of Git while retaining a declarative, observable resource describing the dependency.
The important operational lesson is that naming is part of an API even when both sides are YAML. A field like secretStoreRef.name is not documentation or a suggestion. It is a foreign key to a controller-managed object. I corrected it at the manifest level so the next Argo CD reconciliation uses the existing shared backend rather than requiring a duplicate store called vault or a manual secret workaround. Duplicating the store would have been the larger and less correct fix because it would create a second source of cluster-level Vault configuration just to preserve an inaccurate application reference.
This also preserves the desired access path. Vault remains the system of record, External Secrets is the delivery controller, and SearXNG receives only the Kubernetes Secret it needs at runtime. There is no secret copied into the chart values file and no one-off imperative command that future reconciliation could overwrite. For a service in the DMZ, keeping this ownership path crisp is particularly valuable. The network location changes the exposure profile, but it should not lead to weaker secret handling.
Fixing Engine Configuration From Symptoms to Startup Semantics
The most iterative work this week was SearXNG engine configuration. Wikidata, Ahmia, and Torch were failing initialization with HTTP 403 responses from the cluster’s egress address. These failures were not a generic pod scheduling or resource problem. They were remote services declining requests associated with the DMZ’s outbound identity, and SearXNG was encountering those engines during its initialization path.
My first change declared the affected engines as disabled. The initial YAML structure represented engines as mapping entries. The next correction changed that configuration to a list of dictionaries, with each item carrying a name. This was not cosmetic formatting. SearXNG’s settings schema expects engine entries in list form, so a map that looks natural to a human does not necessarily represent valid engine configuration to the application. The configuration became:
engines:
- name: wikidata
disabled: true
- name: ahmia
disabled: true
- name: torch
disabled: true
That resolved the structural mismatch, but it exposed the actual behavioral distinction in the application. disabled: true prevents an engine from being offered for search, but it does not prevent Wikidata’s initialization method from running. In this environment that is too late. The initialization call still receives a 403 and can block startup. I changed all three entries to inactive: true, retaining the list-of-dictionaries format. The final manifest documents why: SearXNG runs Wikidata initialization even when disabled, and the remote 403 from cloud IPs blocks startup.
This is exactly why I prefer recording the reason next to a non-obvious configuration choice. Someone later looking only at the UI effect could reasonably assume disabled and inactive are interchangeable. They are not interchangeable during this version’s lifecycle. inactive is the correct state when the engine must not initialize at all. The comment makes the operational constraint visible without requiring the next person to rediscover it through a failed rollout.
The three engines were not removed wholesale. Keeping them as explicit inactive entries captures an intentional limitation and leaves a clear re-enablement path if egress changes, the providers permit the cluster address, or a future SearXNG version changes initialization behavior. Silent deletion would make it harder to distinguish a deliberate availability decision from an incomplete engine set. Conversely, continuing to retry engines known to fail from this network would turn a remote policy decision into an avoidable deployment outage.
The sequence also reinforces a mundane but important GitOps practice: when a configuration change appears not to solve an observed failure, validate both the data shape and the consuming application’s runtime semantics. A syntactically valid ConfigMap is not proof that its embedded YAML matches the application schema. A semantically plausible flag is not proof that it gates the stage that is failing. The resulting configuration is smaller than any workaround involving custom egress or a forked engine implementation, and it directly addresses the startup failure.
Deploying an Authentik Outpost to the DMZ
I also added authentik-remote-cluster for the DMZ. The chart is a local Helm wrapper around Authentik’s upstream authentik-remote-cluster chart from https://charts.goauthentik.io, pinned at version 2026.2.3. The wrapper’s own chart version is 0.1.0, and its declared application version matches the upstream chart. Pinning matters here because an authentication proxy participates in a protocol and configuration contract with the central Authentik service. I want upgrades to be reviewed as changes, not selected incidentally by a fresh dependency resolution.
The role of this component is narrow. It deploys an Authentik outpost pod on the DMZ cluster that proxies authentication requests back to the central Authentik server in the infrastructure cluster. That avoids treating every remote cluster as an independent identity system. Authentik remains the central authority, while the outpost is the Kubernetes-local integration point for services that need Authentik’s proxy behavior in DMZ.
The values set remoteName: dmz-cluster and logLevel: info. The outpost is exposed internally as a ClusterIP service on port 9000, with both service and target port set to 9000. A ClusterIP is deliberate. This is not an instruction to expose an authentication component directly at the edge. It gives workloads inside the cluster a stable service endpoint, while ingress decisions remain explicit at the application or proxy layer that needs them.
Resource requests are 50 millicores of CPU and 128 MiB of memory, with a 256 MiB memory limit. These numbers establish a schedulable baseline and an upper memory boundary for an auxiliary proxy service without inventing oversized reservations. They should be revisited using observed behavior, especially if the number of protected applications or concurrent sessions increases, but they are a reasonable initial contract for a component whose job is to relay rather than host the identity database itself.
The Argo CD application is created under infra/argocd/templates/authentik-remote-cluster-dmz.yaml. Like SearXNG, it tracks the GitOps repository at main, targets the dmz registered cluster, deploys to the authentik namespace, and enables automatic sync, prune, self-healing, and namespace creation. Placing the application definition in the infrastructure cluster’s Argo CD templates makes the cross-cluster ownership visible. The central Argo CD instance owns the desired graph, and the destination cluster receives only the workload resources it is meant to run.
There is one manual integration step that should remain explicit rather than being guessed in configuration. After chart installation, the generated kubeconfig output needs to be added in the Authentik UI under the outpost’s Kubernetes integration. The outpost then registers itself and can begin proxying. This is an integration boundary between the deployed chart and the central identity service. The chart comments document the necessary sequence, which is better than implying that Kubernetes resource creation alone completes enrollment.
Removing the Registry Dependency From cert-manager
The certificate-management change was simple but high leverage. I removed the explicit registry field from cert-manager controller, webhook, and CA injector image settings in dev, DMZ, infra, and production. The old values directed the images through registry.durp.info in dev, DMZ, and infra, and through registry.internal.durp.info in production. The repository values already specify the upstream jetstack image repositories, so removing the registry override restores direct upstream image resolution from Quay.
This was prompted by the practical failure mode of an internal registry being present in the critical path for a foundational cluster controller. cert-manager is needed by much more than one application. It manages certificate issuance and renewal for ingress and other TLS consumers. If its images cannot be pulled because a registry mirror, DNS path, credential configuration, or availability condition is wrong, the blast radius expands beyond the registry itself.
Direct pulls from upstream quay.io are the right current tradeoff. They remove an unnecessary dependency layer and make the manifests state what they actually intend to consume. A registry cache can be useful when it supplies a concrete benefit such as controlled mirroring, constrained egress, or reproducible internal supply paths, but it must then be operated as production infrastructure with its own availability and image synchronization guarantees. An implicit redirect that merely adds another failure point is not an improvement.
The change was made consistently across four environments and all three cert-manager workloads. Partial fixes are a common source of misleading success, for example when a controller pulls but its webhook remains stuck in ImagePullBackOff. Updating controller, webhook, and CA injector together preserves the release’s component set. The values retain pullPolicy: Always, so nodes check the configured upstream image when starting these workloads rather than silently relying on stale local cache state.
I also normalized a few whitespace details in the DMZ and production values while touching the files, including the DMZ hostAliases formatting. That is not the reason for the change, but configuration files are safest when insignificant formatting does not obscure the meaningful diff. The meaningful result is that certificate-manager images no longer depend on the internal registries to start.
Initializing Durfuel
Outside the cluster repository, I initialized the Durfuel application project in durfy/apps/durfuel. The first project commit, 965fbd70, added 38,899 lines. It is a full private, multi-user liquid-fuel tracking application rather than an empty repository with a placeholder README. The initial scope includes a Go HTTP service, embedded web assets, PostgreSQL schema and ordered Goose migrations, an OpenAPI 3.1 contract, scoped personal-access-token API behavior, web CSV import and export, and charts or statistics support.
The application’s domain model makes several choices explicit. Fuel volume uses a named unit, including US gallons and liters, rather than assuming a locale. Currency is represented as a constrained code. Decimal quantities are represented as decimal strings at the API boundary rather than JSON floating-point values. Dates are calendar dates with no timezone, while timestamps use explicit RFC 3339 offsets. These are boring choices, which is exactly the point. Fuel records are accounting-adjacent data, and ambiguity in units, currency, or floating-point rounding becomes expensive once reports and imports depend on it.
The OpenAPI contract describes vehicles, fuel entries, aggregate statistics, time-series chart data, pagination, standardized problem responses, conditional writes using strong ETags, and location headers for created resources. The API also defines scoped token permissions for reading and writing vehicles and fuel entries, statistics reads, and exports. A contract this explicit provides a useful implementation boundary for the Go service and any later client. It is easier to reject an unsupported request shape than to let ad hoc behavior become a compatibility promise.
The local operating model is captured in compose.yaml. PostgreSQL 18 Alpine provides the database, Mailpit provides local mail capture, a migration service runs before the application, and the application exposes its readiness endpoint on port 8080. The compose setup includes health checks and makes the application depend on successful migrations and a healthy mail service. The app container is read-only and receives a small writable /tmp tmpfs with noexec and nosuid. Those are practical defaults for a web service, not a substitute for broader deployment hardening, but they reduce accidental mutable state in the image filesystem.
The repository also includes pinned frontend and quality tooling: Bun, Biome, TypeScript, esbuild, Playwright, axe-core, HTMX, Alpine, Chart.js, Lucide, and fonts. Biome is configured with recommended linting plus strict rules around unused imports and variables, explicit any, non-null assertions, parameter reassignment, and default exports. The point is not to collect tooling. It is to have one committed formatter and linter policy for the web assets from the first implementation rather than negotiate formatting after every change.
A second commit, 0bdd1409, reduced the README from 70 lines to one line. That follow-up is small in diff volume but worth noting because repository documentation is part of the delivered interface. The substantive application implementation remains in the initial commit; the current README should be rebuilt into useful operational documentation before the project is handed to another operator or deployed beyond its development context.
Looking Ahead
The next step for SearXNG is operational verification, not more configuration churn. I need to confirm that the Argo CD application reaches a healthy state in the DMZ, that External Secrets materializes searxng-secret through vault-backend, and that the service starts with the three restricted engines inactive. From there I can evaluate search quality and decide whether any alternate engines or egress policy changes are justified. I should not re-enable a known failing engine until its initialization path succeeds from the actual cluster network.
For the Authentik outpost, I need to complete the documented kubeconfig registration in the central Authentik configuration and verify the outpost’s registration, service endpoints, and proxy flow from a DMZ workload. Resource requests should be reviewed after there is real traffic. The desired result is centralized identity with a narrow remote-cluster component, not an independently administered identity plane in the DMZ.
For cert-manager, I will verify image pulls and controller, webhook, and CA injector readiness in all four environments, then observe a real certificate reconciliation. The registry removal fixes the declared image path, but a certificate issuance check validates the whole controller chain. Finally, Durfuel needs its README restored as an accurate runbook and its initial service exercised against its Compose dependencies before I decide on deployment, registry, or cluster integration. The project has a deliberate local foundation; it does not need speculative platform work yet.