This week was mostly about making the homelab’s platform layer more predictable. I finished bringing Forgejo onto CloudNativePG, added Harbor as the internal registry and cache layer, tightened a few Kubernetes resource and database details, and landed several security-oriented fixes in DurpDeploy. I also split the DurpDeploy agent into its own repository so the agent can have a smaller, clearer delivery boundary than the server.
The common theme was removing assumptions that only work while a system is young. A hard-coded secret name, an implicit bootstrap owner, or an unbounded controller may appear harmless in a clean environment. They become operational problems when reconciliation, upgrades, migrations, and failure recovery start exercising the real interfaces. The work this week focused on the actual contracts between charts, operators, secrets, databases, registries, and applications.
CloudNativePG Secrets and Database Ownership
The most important GitOps work this week was correcting how Forgejo and Harbor receive PostgreSQL credentials from CloudNativePG. The initial goal was simple: run Forgejo on PostgreSQL through the CloudNativePG operator rather than preserving an older database arrangement during the Unraid migration. The surrounding details were less simple, because a CNPG cluster is not just a StatefulSet with a password Secret attached. It has an operator-controlled bootstrap process, generated credentials, application Secrets, and custom resources that each need to agree on the database role they represent.
I started by correcting chart versions for both CloudNativePG and Forgejo. This was required to get out of a CrashLoopBackOff state and onto versions whose values and API expectations were known. In a GitOps repository, pinning a version is not busywork. The chart is the executable contract for the manifest shape, image defaults, and generated workload behavior. A values file that appears valid against an older chart can silently target a different field or default after an update. Pinning the intended versions made the failure mode inspectable instead of leaving it dependent on whatever a repository index supplied at reconciliation time.
I also moved the Forgejo chart source to the official Forgejo Helm repository, code.forgejo.org/forgejo-helm, at version 17.1.5. This avoided relying on a community mirror when the upstream project publishes the chart itself. Along with that source correction, I aligned the Forgejo image registry and tag with the defaults expected by that chart and set the appropriate image pull policy. Those changes sound ordinary, but they remove a class of upgrade failure where Helm renders one version’s configuration while Kubernetes tries to retrieve a differently named or tagged image.
A specific CNPG compatibility fix followed: the passwordSecret reference changed from secretName to name in the v1.30 API. This is exactly the kind of small field mismatch that makes declarative infrastructure deceptive. The manifests can remain syntactically valid YAML while the controller receives a spec that does not mean what the author intended. Updating the field to the current CRD shape restored the direct connection between the database cluster and its managed password Secret.
The next issue was more subtle. I had configured bootstrap.initdb.owner for the Forgejo and Harbor clusters while also expecting passwordSecret to control the application credential. That combination does not produce the desired result. During initial database creation, the bootstrap owner path has its own semantics, and it prevented the password Secret from being the authoritative source I intended. I removed bootstrap.initdb.owner from both Forgejo and Harbor database definitions so that the declared password Secret can take effect consistently.
This is a useful distinction operationally. The bootstrap configuration describes how the initial database is created. The password Secret describes the durable credential contract the application consumes. When those concepts are mixed, a fresh cluster can behave differently from an already initialized cluster, and a secret rotation can stop matching the role actually used by the application. Removing the conflicting owner declaration made the password flow explicit rather than relying on bootstrap behavior to happen to match the chart’s database settings.
Harbor needed one additional piece because its database role needs to be represented correctly in CNPG. I introduced a DatabaseRole custom resource for harbor-db, then corrected the spec to use cluster.name, which is the relationship the operator expects. The role definition allows CloudNativePG to manage the application database role in the same control plane as the cluster rather than treating it as an incidental side effect of initial bootstrap.
The final Harbor change was to consume the CNPG auto-generated application Secret for the harbor-db password. That is the robust interface here. Harbor gets a Secret with the credentials generated and managed by the database operator, and its Helm configuration references that Secret rather than duplicating a password value or assuming a manually created one will remain synchronized. The result is one owner for database credentials, one consumer path for the application, and a clear recovery story when the cluster has to be recreated or credentials are rotated.
These fixes matter beyond Forgejo and Harbor. I want database-backed services in the homelab to follow the same model: the database operator owns lifecycle-sensitive credentials, external secret tooling owns the upstream secret material where appropriate, and application charts consume declared Secret references. That gives Argo CD a stable desired state without asking it to invent or copy database passwords during reconciliation.
Forgejo as a Managed Service
Forgejo was the first major service moved into this CNPG-backed pattern. The intent is not merely to replace one PostgreSQL endpoint with another. I want the source-control service to be treated like the rest of the platform: a GitOps application with an explicit chart source, versioned values, operator-managed storage dependencies, and predictable credentials.
The migration work included the CNPG integration itself and the chart source, image, and pull-policy corrections described above. Together, those changes remove ambiguity from the service definition. The chart identifies the upstream source, the image is compatible with the chart, the database role is defined through the operator, and the password is supplied through the supported Secret flow. Each layer can now be diagnosed independently when something changes.
I also kept the GitOps changes scoped to the infra environment where this service belongs. The repository had activity in both infra and dmz this week, but a database-backed internal developer service should not be spread across environments without a deliberate network and exposure design. Keeping the core service, database, and secrets in the internal platform environment reduces the number of places that need to agree before the service is healthy.
A recurring lesson from this work is that a successful first install is not enough evidence that a service is correctly configured. The better test is whether the system remains understandable after an operator upgrade, pod replacement, Secret reconciliation, or database recreation. A configuration that only works because an older initialization path happened once is a latent outage. The CNPG corrections were about making the steady-state and recovery behavior match the declarative intent.
Harbor Registry and Container Runtime Mirrors
I added Harbor to the GitOps repository as a full internal registry application, including its Argo CD application definition, Helm chart configuration, and Vault-backed secrets. Harbor provides a durable internal place for images and introduces a controlled caching boundary between the cluster and external registries.
The immediate reason for Harbor is practical: clusters should not have every node independently depend on public registry availability and rate limits. An internal registry can retain images that the homelab builds or promotes itself, while a proxy cache can reduce repeated external fetches. That improves repeatability during a rollout and makes it easier to understand where images are coming from when troubleshooting a deployment.
The Harbor configuration is not just a registry URL. It has a database dependency, credentials, storage requirements, chart values, and a routing model. The CNPG work and Harbor work were deliberately connected because a registry is an application with state, not a disposable cache. The operator-managed Harbor database role and generated application Secret establish the database contract, while Vault-backed secrets avoid committing credentials to the Git repository.
I added a containerd mirror DaemonSet and a proxy-cache setup script as part of the Harbor work. The purpose is to give each node a consistent local runtime configuration that can direct image pulls through Harbor’s proxy cache. The DaemonSet model is useful for a homelab with multiple nodes because it makes the runtime configuration rollout explicit and repeatable. A node joining the cluster should not need an undocumented manual edit before it can use the same image path as every other node.
There is an important operational distinction between configuring an application to use Harbor and configuring containerd mirrors. Application image references remain useful as standard upstream references when Harbor is acting as a cache. The runtime mirror defines where containerd resolves or fetches them. That allows the cluster to preserve normal image naming while centralizing the caching policy in node runtime configuration. It also keeps the choice reversible, which is valuable while validating the cache behavior and registry access controls.
The proxy-cache setup needs careful validation before it becomes a universal dependency. It must have correct DNS and TLS behavior from every node, suitable credentials where the upstream requires them, enough storage, and a clear fallback policy. A cache that silently returns stale or partial content is worse than a visible external pull failure. I kept the configuration focused on the necessary mirror and cache setup rather than adding a larger custom registry-management system around it.
The Harbor rollout also strengthens the division of responsibilities in the platform. Argo CD deploys the desired Harbor application. Helm renders the chart values. Vault supplies sensitive values through the existing secret path. CloudNativePG manages the database lifecycle and role credentials. Containerd consumes runtime mirror configuration on nodes. Each component has one understandable job, which is preferable to hiding all of those responsibilities inside one large bootstrap script.
MetalLB Resource Limits
I added resource limits to the MetalLB controller pod. This was a small commit with an outsized operational benefit. MetalLB is a foundational network component in this environment because it allocates and advertises service addresses for workloads that need LoadBalancer exposure. It should have enough reserved capacity to do its job, but it also should not be permitted to consume unbounded resources on a node during an unexpected condition.
Requests and limits provide a scheduling and containment contract. Requests help Kubernetes place the controller onto a node that can satisfy its baseline need. Limits cap the resource footprint if the process enters a pathological state. For a control-plane-adjacent workload such as MetalLB’s controller, the goal is not to tune it for maximum benchmark throughput. The goal is to keep it stable, schedulable, and incapable of starving unrelated platform components.
This is part of a broader cleanup pattern. Cluster add-ons often begin with upstream defaults because the first task is to get the feature online. Later, when the component becomes relied upon, defaults need to be reviewed against the cluster’s actual scheduling and reliability constraints. Adding limits is a concrete acknowledgement that MetalLB is no longer an experiment in the environment. It is infrastructure with a bounded resource budget.
DurpDeploy Security and Product Work
DurpDeploy received several focused changes this week. The important thread was reducing unnecessary disclosure of sensitive or internal details at HTTP boundaries, while also improving the product presentation and keeping dependencies current.
I fixed plaintext password exposure in user redirects. Passwords should never survive beyond the point where they are needed for authentication, and they must not be inserted into redirect targets, query parameters, locations, logs, browser history, referrer headers, or error reports. Redirects are particularly risky because they cross an application boundary and are often recorded by infrastructure that is not part of the application process. The fix removes that unsafe flow rather than attempting to redact it later downstream.
I also fixed internal error disclosure in HTTP responses. An internal error should give a client a suitable status and a stable, non-sensitive response, not a raw database error, stack context, internal service name, or implementation detail. Those details remain valuable for server-side logs and debugging with appropriate access, but returning them to clients increases the amount of information available to an attacker and makes the API contract dependent on incidental implementation behavior.
The viewer logout flow needed correction for read-only users. Authorization modes often expose bugs that privileged paths hide, because an application assumes a viewer will never reach a session-management action or assumes logout needs capabilities it does not actually need. Logging out should be safe and available regardless of whether the current user can modify deployment state. I fixed that behavior so read-only users can terminate their sessions cleanly without being blocked by authorization logic intended for mutating actions.
I hardened the SonarCloud workflow’s handling of secrets. CI secrets should be passed only where the job actually needs them, not exposed broadly through shell output, generated configuration, or unrelated workflow steps. This is especially important for automation that runs against pull requests and dependencies, where the trust level of the code under test can vary. Keeping secret scope narrow limits the blast radius if a workflow is misconfigured or a tool prints unexpected diagnostic data.
The repository also took five dependency updates: github.com/moby/go-archive, Testcontainers’ PostgreSQL module, modernc.org/sqlite, github.com/go-openapi/runtime, and golang.org/x/crypto. These upgrades are intentionally routine maintenance, not a feature banner. Go applications accumulate meaningful transitive surface area, particularly around archive processing, databases, API machinery, and cryptography. Taking dependency updates continuously is cheaper and safer than allowing them to pile up until a large version jump becomes unavoidable.
On the presentation side, I added a favicon and application screenshots. The favicon gives browser tabs and saved links a recognizable identity. Screenshots make documentation, project pages, and release communication more concrete by showing the current interface instead of requiring readers to infer it from a feature list. Neither change alters the deployment model, but both make the application easier to identify and evaluate.
Separating the DurpDeploy Agent
I extracted the standalone DurpDeploy agent into the durpdeploy-agent repository. This is a boundary change more than a code-volume change. The agent has a different execution environment and lifecycle from the DurpDeploy server. It needs a clear build, release, dependency, and security story that does not require a consumer to carry the entire server repository.
Separating the agent lets the server and agent evolve at their own appropriate pace. The server can focus on its API, UI, data model, and orchestration behavior. The agent can focus on authenticated communication, local execution, installation, and compatibility with the server protocol. That does not mean creating an abstraction layer for every shared line of code. It means recognizing that independently shipped software should have independently understandable ownership and release boundaries.
The next discipline here is compatibility. A separate repository makes versioning more explicit, so the server-agent protocol needs to be treated as a contract. New server behavior should either remain compatible with supported agents or clearly identify the minimum agent version it requires. The same applies in reverse for agent capabilities. Explicit compatibility is less exciting than a coupled development loop, but it is what keeps an upgrade from turning into a coordinated outage.
GitOps Repository Maintenance
The homelab GitOps repository had 33 commits across 78 files this week, touching Argo CD, Bitwarden, CloudNativePG, Forgejo, Harbor, the internal proxy, MetalLB, and shared repository handling. That looks broad, but the changes were concentrated around making a few platform services line up with their supported interfaces.
Argo CD remains the deployment convergence mechanism. The desired state lives in Git, Argo CD renders and applies it, and the cluster operators reconcile their own resources. When a value is corrected in the GitOps repository, the expectation is not that a person will run an ad hoc repair command on the cluster. The expectation is that the declaration can converge repeatedly. That expectation drove the CNPG secret changes, the official chart source correction, and the Harbor application structure.
The internal proxy and DMZ-related repository activity also reflect the need to keep exposure patterns explicit. Internal services should not gain external reachability merely because a chart can create an ingress. Network placement, proxy behavior, TLS, and authentication boundaries must be intentional. Harbor and Forgejo are useful internal platform services, but their value does not justify broadening their exposure by default.
The Bitwarden and Vault-related paths remain part of the secret delivery story. Sensitive values should enter cluster workloads through a controlled secret mechanism, not through committed Helm values or hand-maintained copies. Database credentials are especially sensitive because they are both long-lived and widely useful if leaked. The changes this week reduced duplication by using the CNPG-generated application Secret at the consumer boundary.
Looking Ahead
The immediate next step is to validate the new Harbor path under real pulls and node lifecycle events. I want to confirm that every intended containerd mirror resolves correctly, Harbor’s proxy cache behaves as expected for upstream images, storage growth is visible, and a node replacement does not require manual configuration to regain cache access. If the cache becomes a normal dependency, its monitoring and capacity thresholds need to be as deliberate as its initial setup.
For CloudNativePG, I will continue verifying the Forgejo and Harbor recovery paths, not just their healthy paths. That includes confirming application Secret references after reconciliation, checking behavior after controlled pod restarts, and making sure the role and cluster specifications continue to match the installed CNPG version. Database lifecycle configuration is only complete when recreation and rotation are understandable.
For DurpDeploy, the agent split creates a good point to define and test the server-agent compatibility contract. The security fixes this week covered clear boundary problems, and I will keep reviewing redirects, errors, CI workflows, and logs for places where internal or secret-bearing data can cross a trust boundary unnecessarily. The desired direction is straightforward: platform services should be declarative, credentials should have one owner, and applications should expose only the information clients need.