---
title: "QuenchWorks: A Zero-CVE, Built-From-Source Replacement for the Bitnami Catalog"
lang: "en"
author: "Mohammad Abu Mattar"
canonical: https://mkabumattar.com/post/quenchworks-zero-cve-bitnami-alternative-wolfi
---

![Blog post image for QuenchWorks: A Zero-CVE, Built-From-Source Replacement for the Bitnami Catalog - When Broadcom moved the free Bitnami catalog to a legacy tier, thousands of teams lost their supply of maintained, hardened container images overnight. QuenchWorks is my answer: over 150 container images and 120 Helm charts, rebuilt from source on Wolfi, scanned to zero fixable CVEs, cosign-signed, and pinned by digest. Here is why I built it and how it actually works.](/_astro/hero.DloumiN1_28OMVK.webp)

[Home](/)›[Blog](/blog)›[All Categories](/blog/categories)›[DevOps](/blog/categories/devops)

Blog

[Prev in DevOpsPolicy as Code with Open Policy Agent: A Technical and Governance Perspective](/blog/post/policy-as-code-opa-guide)[Next in DevOpsScaling Up, Staying Strong: Hands-On AWS CloudFormation Techniques for Building Resilient and Scalable Systems](/blog/post/scaling-up-staying-strong-hands-on-aws-cloudformation-techniques-for-building-resilient-and-scalable-systems)

[DevOps](/blog/categories/devops)[Containers](/blog/categories/containers)[Supply Chain Security](/blog/categories/supply-chain-security)[Kubernetes](/blog/categories/kubernetes)[Open Source](/blog/categories/open-source)

# QuenchWorks: A Zero-CVE, Built-From-Source Replacement for the Bitnami Catalog

[Mohammad Abu Mattar](/authors/mohammad-abu-mattar)Published: 08 Jul 2026Updated: 22 Jul 202617 Mins read24 Mins listen

[Markdown for AI(opens in a new tab)](/post/quenchworks-zero-cve-bitnami-alternative-wolfi/index.md "Open the plain-Markdown version of this page, for pasting into an AI tool")

TL;DR

When Broadcom moved the free Bitnami catalog to a legacy tier, thousands of teams lost their supply of maintained, hardened container images overnight. QuenchWorks is my answer: over 150 container images and 120 Helm charts, rebuilt from source on Wolfi, scanned to zero fixable CVEs, cosign-signed, and pinned by digest. Here is why I built it and how it actually works.

Series

[Container Security & DevSecOps](/series/container-security--devsecops)6/6

[PreviousWhat's the Deal with Shift-Left Security, and Why Should You Care?](/blog/post/shift-left-security-sast-dast-sca-cicd)

All posts in this series (6)

Blog6

1.  [Policy as Code with Open Policy Agent: A Technical and Governance Perspective](/blog/post/policy-as-code-opa-guide)
2.  [HashiCorp Vault vs. AWS Secrets Manager vs. SOPS: Which One Fits Your Setup](/blog/post/secrets-management-vault-secrets-manager-sops)
3.  [The Democratization of Container Security: Docker Hardened Images](/blog/post/democratization-docker-hardened-images-container-security)
4.  [Container Image Signing with Cosign: A Hands-On Guide to Secure Your Supply Chain](/blog/post/container-image-signing-cosign-guide)
5.  [What's the Deal with Shift-Left Security, and Why Should You Care?](/blog/post/shift-left-security-sast-dast-sca-cicd)
6.  [QuenchWorks: A Zero-CVE, Built-From-Source Replacement for the Bitnami CatalogYou are here](/blog/post/quenchworks-zero-cve-bitnami-alternative-wolfi)

### QuenchWorks: A Zero-CVE, Built-From-Source Replacement for the Bitnami Catalog

Contents

[What actually happened to Bitnami?](#what-actually-happened-to-bitnami)[So what is QuenchWorks?](#so-what-is-quenchworks)[Why build on Wolfi instead of Debian or Alpine?](#why-build-on-wolfi-instead-of-debian-or-alpine)[What "zero CVE" actually means (and why I refuse to fake it)](#what-zero-cve-actually-means-and-why-i-refuse-to-fake-it)[Signed, documented, and pinned by digest](#signed-documented-and-pinned-by-digest)[The Helm charts, and the piece that keeps them consistent](#the-helm-charts-and-the-piece-that-keeps-them-consistent)[How to actually use it](#how-to-actually-use-it)[How do you verify an image before you trust it?](#how-do-you-verify-an-image-before-you-trust-it)[Example 1: A full observability stack you can trust](#example-1-a-full-observability-stack-you-can-trust)[What's in the box](#whats-in-the-box)[The repository layout](#the-repository-layout)[Step by step: install the stack](#step-by-step-install-the-stack)[Sending data to the collector](#sending-data-to-the-collector)[Common questions](#common-questions)[Example 2: CI/CD and GitOps that deploys itself](#example-2-cicd-and-gitops-that-deploys-itself)[Bootstrap the controller](#bootstrap-the-controller)[The CI half: verify before you promote](#the-ci-half-verify-before-you-promote)[Keeping digests fresh automatically](#keeping-digests-fresh-automatically)[GitOps questions people actually ask](#gitops-questions-people-actually-ask)[Example 3: Shipping a real application end to end](#example-3-shipping-a-real-application-end-to-end)[Example 4: Staying zero-CVE after day one](#example-4-staying-zero-cve-after-day-one)[Where it stands, honestly](#where-it-stands-honestly)

If you run anything on Kubernetes, there’s a good chance you were pulling Bitnami images without even thinking about it. `bitnami/postgresql`, `bitnami/redis`, `bitnami/nginx`, the whole Helm charts library sitting on top of them. For years it was just there, free, maintained, and reasonably well hardened. You built on it and moved on.

Then in 2025 that quietly stopped being true.

Broadcom, which now owns VMware and Bitnami, announced they were moving the free Bitnami catalog on Docker Hub to a `bitnamilegacy` tier and pushing the actively maintained, security-hardened images into a paid product called Bitnami Secure Images. In plain terms: the free images you were pulling would keep existing, but they would stop getting patched. If you wanted the hardened, CVE-managed versions going forward, you needed a subscription.

For a lot of teams that’s a genuine problem, not just an annoyance. You suddenly have production workloads sitting on a base of container images that nobody upstream is patching anymore. So I did the thing every engineer eventually does when a tool they depend on disappears. I rebuilt it.

This is the story of **QuenchWorks**, and how it actually works under the hood.

## [What actually happened to Bitnami?](#what-actually-happened-to-bitnami)

Let me be precise here, because “Bitnami is dead” is not quite right and I don’t want to spread the panicky version.

The free catalog didn’t vanish. What changed is the maintenance story. The images that used to receive regular security updates for free got moved behind Bitnami Secure Images, and the free tier became a frozen legacy snapshot. A frozen container image is fine on day one and a slowly growing liability on day ninety, because CVEs get discovered in software you already shipped. An image that isn’t rebuilt is an image that only accumulates known vulnerabilities.

So the gap isn’t “there are no more Bitnami images.” The gap is “there is no more free, continuously patched, hardened catalog that a small team can just adopt.” That’s the hole QuenchWorks fills.

## [So what is QuenchWorks?](#so-what-is-quenchworks)

QuenchWorks is a security-first, from-scratch replacement for the Bitnami catalog. Right now it ships **over 150 container images and more than 120 Helm charts**, and it’s growing on a daily build cadence.

The rules I set for myself on day one:

1.  **Every image is built from source.** Not repackaged, not re-tagged, not “pull the upstream binary and slap a label on it.” The source is compiled inside the build.
2.  **Zero fixable CVEs, enforced by the build itself.** If a scan finds a vulnerability that has a fix available, the build fails. I’ll come back to the word “fixable” because it matters a lot.
3.  **Minimal and locked down by default.** Nonroot, read-only root filesystem, no shell where one isn’t needed, multi-arch for `x86_64` and `arm64`.
4.  **Signed and verifiable.** Every image is cosign-signed and ships a software bill of materials and build provenance you can check yourself.
5.  **Clean-room.** QuenchWorks does not copy Bitnami’s charts or configs. It’s an independent implementation, not a fork.

The whole thing lives at [quench-works.com](https://quench-works.com), the images are on GitHub Container Registry, and the charts are published to ArtifactHub as a verified publisher.

## [Why build on Wolfi instead of Debian or Alpine?](#why-build-on-wolfi-instead-of-debian-or-alpine)

This is the decision everything else hangs on, so it’s worth explaining.

Most base images are general-purpose Linux distributions. Debian, Ubuntu, Alpine. They’re built to be a complete operating system you can log into and work in. That’s exactly the problem for a container. A container running Postgres does not need a package manager, a shell, `curl`, `wget`, or ninety other packages. Every one of those is more code, and more code means more CVEs that have nothing to do with the app you actually care about.

**Wolfi** is different. It’s a distroless-friendly, container-native Linux built by Chainguard specifically to have as little in it as possible and to get security fixes fast. Two tools go with it:

-   **melange** builds a package from source into a signed APK.
-   **apko** assembles a container image from a declarative list of those APKs, with no Dockerfile and no layers full of leftover build tools.

The payoff is that the final image contains the app, its runtime dependencies, and almost nothing else. There’s no `apt`, no busybox shell, no build toolchain hiding in a layer. A smaller image isn’t just faster to pull. It’s a smaller attack surface and a much shorter CVE scan.

Here’s the shape of the pipeline for a single app:

```
1   upstream source (a specific, pinned version)2              │3              ▼4        melange build          ← compile from source, CGO off where possible,5              │                   stamp the real version, sign the APK6              ▼7         apko assemble          ← nonroot uid 1001, read-only rootfs,8              │                   multi-arch, only the runtime deps9              ▼10      Trivy scan (the gate)      ← fail the build on ANY fixable CVE11              │12              ▼13   cosign sign + SBOM + SLSA      ← keyless signature, bill of materials,14              │                    build provenance attestation15              ▼16        GHCR, pinned by digest
```

The Trivy step is the one that makes the whole project mean something, so let’s talk about it honestly.

## [What “zero CVE” actually means (and why I refuse to fake it)](#what-zero-cve-actually-means-and-why-i-refuse-to-fake-it)

“Zero CVE” is a phrase people love to put on a marketing page, and most of the time it’s not true. Any non-trivial piece of software has open vulnerabilities somewhere in its dependency graph at any given moment. Anyone who tells you their image has literally zero known vulnerabilities is either not scanning hard enough or not telling you the whole story.

So here’s the precise claim QuenchWorks makes: **zero fixable CVEs.**

The scanner runs in thorough mode and I filter to vulnerabilities that have a fix available. If a CVE has a patched version out there, the build must include it or the build fails. No exceptions, no allowlist to sneak something past the gate. That’s the part I control, and I hold it at zero.

What I can’t control is upstream. If a project depends on a library that has a known CVE and there’s simply no patched release yet, no amount of rebuilding fixes it. The fix doesn’t exist. In that case QuenchWorks reports the unfixable count openly instead of pretending it’s zero. When upstream ships the patch, the nightly rebuild picks it up automatically and the number drops.

I think that distinction is the most important thing about the project. “Zero fixable CVEs, and here’s the honest count of what’s still unfixable upstream” is a claim you can actually trust and verify. “Zero CVEs” with no asterisk is a claim you should be suspicious of.

## [Signed, documented, and pinned by digest](#signed-documented-and-pinned-by-digest)

A hardened image you can’t verify is just a hardened image you’re taking on faith. So every QuenchWorks image ships three things beyond the bits themselves:

Every image is built from source on Wolfi (no upstream binaries), scanned with the build failing on any CVE, then signed with cosign and shipped with an SBOM and provenance, and finally pinned by digest in the registry.

-   **A cosign signature**, using keyless Sigstore signing. You can verify the image was built by the QuenchWorks pipeline and not swapped out somewhere in transit.
-   **An SBOM**, a software bill of materials in SPDX format that lists exactly what’s inside. When the next big CVE drops, you can grep your SBOMs instead of guessing which images are affected.
-   **SLSA provenance**, an attestation of how and where the image was built.

You verify them with the GitHub CLI:

Terminal window

```
gh attestation verify \  oci://ghcr.io/quenchworks/images/postgresql:<version> \  --owner quenchworks
```

And in your charts and manifests, images are always **pinned by digest, never by a floating tag**. A tag like `:16` can be moved to point at different bits tomorrow. A digest like `@sha256:...` is the content. If the digest matches, you’re running exactly what was signed. This is the difference between “probably the image I tested” and “provably the image I tested.”

## [The Helm charts, and the piece that keeps them consistent](#the-helm-charts-and-the-piece-that-keeps-them-consistent)

Images are half the story. The other half is the 120-plus Helm charts, because that’s how most people actually deploy this stuff.

The thing I wanted to avoid was 120 charts that each reinvent the same security boilerplate slightly differently. So there’s a shared library chart called **quench-common** that every application chart depends on. It defines all the cross-cutting pieces in one place: the pod and container security contexts, the labels, how the image digest gets wired in, the probes, the service account handling.

An individual chart, say the one for SeaweedFS or Tempo, only has to describe what’s actually specific to that app. The hardening comes from the library, so it’s identical everywhere and I fix it in exactly one spot. Every chart is validated in a real `kind` cluster before release, published to ArtifactHub with verified-publisher metadata, and pinned to its image by digest.

## [How to actually use it](#how-to-actually-use-it)

The nice part is there’s almost nothing to learn. If you were pulling Bitnami, you already know the shape of this.

Pull an image directly:

Terminal window

```
docker pull ghcr.io/quenchworks/images/postgresql:<version>
```

Or install a chart from the OCI registry:

Terminal window

```
helm install my-postgres \  oci://ghcr.io/quenchworks/charts/postgresql \  --version <chart-version>
```

Browse the full catalog, with the live CVE count for every image, at [quench-works.com](https://quench-works.com), and find the charts on ArtifactHub under the QuenchWorks publisher.

Now let’s stop talking about it and actually use it. The rest of this post is hands-on. I’ll build a full observability stack, wire up GitOps so it deploys itself, ship a real application end to end, and keep the whole thing verifiable and zero-CVE over time. Every command and manifest here is real and runs against the live catalog.

Tip

Everything below pins images by digest and verifies signatures before trusting them. If you copy nothing else from this post, copy that habit. A tag tells you what an image is called. A digest tells you what it actually is.

## [How do you verify an image before you trust it?](#how-do-you-verify-an-image-before-you-trust-it)

Before we deploy anything, let’s do the step almost everyone skips: proving the image is what it claims to be. QuenchWorks signs every image with keyless cosign and attaches an SBOM and SLSA provenance, so you can check all of it in about ten seconds.

cosign fetches the signature and attestations from the registry and confirms the image's identity and digest. Only after verification passes do you (or CI) deploy the pinned digest, so an unsigned or swapped image never reaches the cluster.

1.  Install the tools you need. You want the GitHub CLI for attestation checks and cosign for signature verification.

Install verification tooling

```
# GitHub CLI (attestations live on GHCR)brew install gh        # macOS / Linuxbrew# cosign for Sigstore signaturesbrew install cosign
```

2.  Verify the build provenance. This proves the image was built by the QuenchWorks pipeline in GitHub Actions and not swapped out somewhere along the way.

Verify SLSA provenance + signature

```
gh attestation verify \  oci://ghcr.io/quenchworks/images/postgresql:17.5 \  --owner quenchworks
```

3.  Inspect the software bill of materials. When the next headline CVE drops, you grep this instead of guessing whether you’re affected.

Pull and read the SBOM

```
gh attestation verify \  oci://ghcr.io/quenchworks/images/postgresql:17.5 \  --owner quenchworks \  --predicate-type https://spdx.dev/Document \  --format json | jq '.[].verificationResult.statement.predicate.name'
```

4.  Resolve the tag to its digest, and use that digest everywhere from now on.

Get the immutable digest

```
docker buildx imagetools inspect \  ghcr.io/quenchworks/images/postgresql:17.5 \  --format '{{.Manifest.Digest}}'# sha256:... <- this is what you pin in values.yaml and manifests
```

Careful here

If `gh attestation verify` fails, do not deploy the image. A failed verification means either the image was tampered with or you’re pointing at something that isn’t a QuenchWorks build. That’s exactly the signal the whole supply chain exists to give you.

## [Example 1: A full observability stack you can trust](#example-1-a-full-observability-stack-you-can-trust)

Let’s start with the thing every cluster needs and nobody enjoys wiring up: observability. Metrics, logs, and traces, with dashboards on top. The classic Grafana stack is often called LGTM (Loki, Grafana, Tempo, Mimir), and QuenchWorks ships all of the pieces plus an umbrella chart that assembles them.

Here’s the data flow we’re building:

Apps and nodes send telemetry to an OpenTelemetry Collector, which fans metrics to Prometheus, logs to Loki, and traces to Tempo, all surfaced in Grafana. Every component runs a QuenchWorks zero-CVE, signed, digest-pinned image.

```
1   your apps + kubernetes            collection            storage                    query + view2  ┌─────────────────────┐      ┌──────────────────┐   ┌────────────────────┐      ┌──────────────┐3  │  app metrics /logs   │────▶│  OTel Collector   │──▶│ VictoriaMetrics      │─┐    │              │4  │  traces (OTLP)       │      │  (receivers +     │   │  (metrics, TSDB)     │ │    │              │5  ├─────────────────────┤      │   processors +    │──▶│ Loki (logs)          │ ├──▶│   Grafana    │6  │  node_exporter       │────▶│   exporters)      │   │ Tempo (traces)       │ │    │  dashboards  │7  │  kube-state-metrics  │      │                   │──▶│                      │─┘    │              │8  │  Vector (log tail)   │────▶│                   │   └────────────────────┘      └──────────────┘9  └─────────────────────┘      └──────────────────┘10        all images: nonroot, read-only rootfs, 0 fixable CVEs, pinned by digest
```

### [What’s in the box](#whats-in-the-box)

-   [Components](#tab-panel-49)
-   [Why these choices](#tab-panel-50)

The `lgtm-stack` umbrella chart pulls these QuenchWorks charts as dependencies, every one of them a hardened, signed, digest-pinned image:

-   **VictoriaMetrics** for metrics storage (a faster, lighter drop-in for Prometheus TSDB)
-   **Loki** for logs
-   **Tempo** for distributed traces
-   **Grafana** for dashboards and querying
-   **OTel Collector** as the single ingest point for OTLP metrics, logs, and traces
-   **Vector** for tailing and shipping node and pod logs
-   **kube-state-metrics** and **node-exporter** for cluster and host metrics
-   **Alertmanager** for routing alerts

A few opinionated defaults worth calling out:

-   **VictoriaMetrics over vanilla Prometheus** for storage. It speaks PromQL, uses far less memory, and handles long retention without falling over. You can still scrape with Prometheus conventions.
-   **OTel Collector as the front door.** Everything sends OTLP to one place. If you later swap a backend, your apps don’t change, only the collector’s exporters do.
-   **Vector for logs** because it’s a single fast binary with backpressure handling, rather than a JVM log shipper eating a gig of RAM per node.

### [The repository layout](#the-repository-layout)

I’m going to do this the GitOps way from the start, so here’s the repo we’ll build up across the next two examples:

-   Directoryplatform-gitops/
    
    -   Directoryclusters/
        
        -   Directoryproduction/
            
            -   **kustomization.yaml** the entry point Argo CD or Flux watches
            -   **observability.yaml** the Argo CD Application for this stack
            -   **platform.yaml** cert-manager, ingress, external-secrets
            
        -   Directorystaging/
            
            -   kustomization.yaml
            
        
    -   Directorystacks/
        
        -   Directoryobservability/
            
            -   **values.yaml** the lgtm-stack values we’re about to write
            -   Directory**dashboards/** your own Grafana dashboard JSON
                
                -   …
                
            
        -   Directoryplatform/
            
            -   cert-manager-values.yaml
            -   ingress-values.yaml
            
        
    -   Directoryapps/
        
        -   Directoryacme-web/
            
            -   **values.yaml** the app we ship in example 3
            -   **database.yaml** postgresql + redis
            -   ingress.yaml
            
        
    -   README.md
    

### [Step by step: install the stack](#step-by-step-install-the-stack)

1.  Create a namespace for everything observability-related.

Namespace

```
kubectl create namespace observability
```

2.  Write the stack’s values. This is the real shape of `lgtm-stack` values, with the toggles that matter for a first install.

stacks/observability/values.yaml

```
1# Grafana: the front end. Stateless by design; dashboards come from config.2grafana:3  enabled: true4  adminPassword: '' # leave empty, pull from a secret (see example 3)5  persistence:6    enabled: false # dashboards are provisioned, not hand-drawn7  ingress:8    enabled: true9    hostname: grafana.internal.example.com10
11# Metrics storage. PromQL-compatible, memory-friendly.12victoriametrics:13  enabled: true14  retentionPeriod: 30d15  persistence:16    enabled: true17    size: 50Gi18
19# Logs.20loki:21  enabled: true22  persistence:23    enabled: true24    size: 50Gi25
26# Traces.27tempo:28  enabled: true29  persistence:30    enabled: true31    size: 20Gi32
33# The single ingest point for OTLP metrics, logs, and traces.34otelCollector:35  enabled: true36
37# Log shipping from every node.38vector:39  enabled: true40
41# Cluster + host metrics.42kubeStateMetrics:43  enabled: true44nodeExporter:45  enabled: true46
47# Alert routing.48alertmanager:49  enabled: true50
51# Provision your own dashboards from JSON in the repo.52dashboards:53  enabled: true
```

3.  Install the umbrella chart from the OCI registry, pointing at your values.

Install the LGTM stack

```
helm install lgtm \  oci://ghcr.io/quenchworks/charts/lgtm-stack \  --version 0.1.0 \  --namespace observability \  --values stacks/observability/values.yaml \  --wait
```

4.  Confirm everything reached Ready.

Check rollout

```
kubectl -n observability get podskubectl -n observability rollout status deploy/lgtm-grafana
```

5.  Port-forward Grafana and log in. The metrics, logs, and traces data sources are already wired to VictoriaMetrics, Loki, and Tempo.

Open Grafana

```
kubectl -n observability port-forward svc/lgtm-grafana 3000:3000# http://localhost:3000
```

Note

The values above are the ones you’ll touch on day one. For the full schema, run `helm show values oci://ghcr.io/quenchworks/charts/lgtm-stack --version 0.1.0`. Every sub-chart also installs on its own if you’d rather compose the stack yourself.

### [Sending data to the collector](#sending-data-to-the-collector)

Your apps point at one endpoint, the OTel Collector, using standard OTLP. Nothing app-side is QuenchWorks-specific, which is the point.

OpenTelemetry env vars for any workload

```
1env:2  - name: OTEL_EXPORTER_OTLP_ENDPOINT3    value: 'http://lgtm-otel-collector.observability.svc:4317'4  - name: OTEL_EXPORTER_OTLP_PROTOCOL5    value: 'grpc'6  - name: OTEL_SERVICE_NAME7    value: 'acme-web'8  - name: OTEL_RESOURCE_ATTRIBUTES9    value: 'deployment.environment=production'
```

And here’s a minimal collector config that fans OTLP out to all three backends:

OTel Collector pipeline (conceptual)

```
1receivers:2  otlp:3    protocols:4      grpc: {endpoint: 0.0.0.0:4317}5      http: {endpoint: 0.0.0.0:4318}6
7processors:8  batch: {}9  memory_limiter:10    check_interval: 5s11    limit_percentage: 8012
13exporters:14  otlphttp/metrics:15    endpoint: http://lgtm-victoriametrics.observability.svc:8428/opentelemetry16  loki:17    endpoint: http://lgtm-loki.observability.svc:3100/loki/api/v1/push18  otlp/tempo:19    endpoint: lgtm-tempo.observability.svc:431720    tls: {insecure: true}21
22service:23  pipelines:24    metrics:25      {26        receivers: [otlp],27        processors: [memory_limiter, batch],28        exporters: [otlphttp/metrics],29      }30    logs:31      {32        receivers: [otlp],33        processors: [memory_limiter, batch],34        exporters: [loki],35      }36    traces:37      {38        receivers: [otlp],39        processors: [memory_limiter, batch],40        exporters: [otlp/tempo],41      }
```

### [Common questions](#common-questions)

Prometheus is a great scraper and a mediocre long-term store. VictoriaMetrics speaks the same PromQL and remote-write protocol, uses a fraction of the memory, and handles months of retention without sharding gymnastics. You keep the Prometheus ecosystem and lose the operational pain. If you specifically want the Prometheus server, that chart is in the catalog too.

In PersistentVolumes, sized by the `persistence.size` values you set. VictoriaMetrics, Loki, and Tempo each get their own PVC. Grafana is deliberately stateless: its dashboards are provisioned from JSON in your Git repo, so a Grafana pod can be deleted and recreated with zero data loss. That’s a feature, not a limitation.

Drop the dashboard JSON into `stacks/observability/dashboards/` in your repo and reference it through the `dashboards` values. Because Grafana is stateless and provisioned, your dashboards are code, reviewed in pull requests like everything else, and they survive pod restarts.

## [Example 2: CI/CD and GitOps that deploys itself](#example-2-cicd-and-gitops-that-deploys-itself)

A stack you install by hand is a stack that drifts. The fix is GitOps: your Git repo is the single source of truth, and a controller in the cluster continuously reconciles reality against what’s committed. You don’t run `helm install` from your laptop anymore. You open a pull request.

CI verifies signatures and scans before promoting a pinned digest into Git. The GitOps controller (itself a zero-CVE image) reconciles that state onto the cluster, and digest-bump automation opens a PR whenever a new signed image ships.

The difference from traditional push-based CI/CD is worth stating plainly:

```
1  PUSH model (traditional)                 PULL model (GitOps)2  ┌──────────┐   kubectl/helm              ┌──────────┐   commit3  │  CI job   │ ───────────────▶ cluster   │  Git repo │ ◀─────────── you4  └──────────┘   (credentials                └──────────┘5                  live in CI)                     ▲ watches + reconciles6                                                  │7                                             ┌──────────────┐8                                             │ Argo CD/Flux  │ (in-cluster)9                                             └──────────────┘
```

In the pull model your CI never needs cluster credentials. The in-cluster controller pulls. That’s a real security win, and it pairs perfectly with digest-pinned, signed images.

### [Bootstrap the controller](#bootstrap-the-controller)

-   [Argo CD](#tab-panel-51)
-   [Flux](#tab-panel-52)

Argo CD watches your Git repo and applies what’s there. Install it, then point it at the repo with an `Application` per stack.

Install Argo CD

```
kubectl create namespace argocdkubectl apply -n argocd \  -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
```

An `Application` that deploys our observability stack straight from the QuenchWorks OCI registry, with values from Git:

clusters/production/observability.yaml

```
1apiVersion: argoproj.io/v1alpha12kind: Application3metadata:4  name: observability5  namespace: argocd6spec:7  project: default8  sources:9    # The hardened chart, pinned to an exact version10    - repoURL: ghcr.io/quenchworks/charts11      chart: lgtm-stack12      targetRevision: 0.1.013      helm:14        valueFiles:15          - $values/stacks/observability/values.yaml16    # Your Git repo, providing the values file above17    - repoURL: https://github.com/your-org/platform-gitops18      targetRevision: main19      ref: values20  destination:21    server: https://kubernetes.default.svc22    namespace: observability23  syncPolicy:24    automated:25      prune: true26      selfHeal: true27    syncOptions:28      - CreateNamespace=true
```

Flux does the same job with a set of custom resources. You declare where the chart lives and where the values come from, and Flux reconciles.

Bootstrap Flux

```
flux bootstrap github \  --owner=your-org \  --repository=platform-gitops \  --branch=main \  --path=clusters/production
```

clusters/production/observability.yaml (Flux)

```
1apiVersion: source.toolkit.fluxcd.io/v1beta22kind: OCIRepository3metadata:4  name: lgtm-stack5  namespace: flux-system6spec:7  interval: 10m8  url: oci://ghcr.io/quenchworks/charts/lgtm-stack9  ref:10    tag: 0.1.011---12apiVersion: helm.toolkit.fluxcd.io/v2beta213kind: HelmRelease14metadata:15  name: observability16  namespace: observability17spec:18  interval: 10m19  chartRef:20    kind: OCIRepository21    name: lgtm-stack22    namespace: flux-system23  valuesFrom:24    - kind: ConfigMap25      name: observability-values26  install:27    createNamespace: true
```

### [The CI half: verify before you promote](#the-ci-half-verify-before-you-promote)

GitOps handles deployment. CI’s job shrinks to something healthier: lint the manifests, render them, and above all verify the image signature before letting a digest into the repo. Here’s a GitHub Actions workflow that does exactly that.

.github/workflows/verify-and-render.yml

```
1name: verify-and-render2on:3  pull_request:4    paths: ['stacks/**', 'apps/**', 'clusters/**']5
6jobs:7  verify:8    runs-on: ubuntu-latest9    permissions:10      contents: read11      id-token: write # for keyless verification12    steps:13      - uses: actions/checkout@v414
15      - name: Install tooling16        run: |17          curl -sSfL https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64 \18            -o /usr/local/bin/cosign && chmod +x /usr/local/bin/cosign19          curl -sL https://get.helm.sh/helm-v3.16.0-linux-amd64.tar.gz | tar xz20          sudo mv linux-amd64/helm /usr/local/bin/21
22      - name: Verify every QuenchWorks image referenced in this PR23        run: |24          # pull the digests out of the values and prove each one is signed25          grep -rhoE 'ghcr.io/quenchworks/images/[a-z0-9-]+@sha256:[a-f0-9]+' stacks apps \26            | sort -u \27            | while read -r ref; do28                echo "Verifying $ref"29                gh attestation verify "oci://$ref" --owner quenchworks30              done31        env:32          GH_TOKEN: ${{ github.token }}33
34      - name: Render the charts (catch template errors before merge)35        run: |36          helm template observability \37            oci://ghcr.io/quenchworks/charts/lgtm-stack --version 0.1.0 \38            --values stacks/observability/values.yaml > /dev/null
```

Tip

The verify step is the one that earns its keep. It makes an unsigned or tampered image a merge blocker, not a production incident. The whole supply chain QuenchWorks builds is only worth anything if something actually checks it, and this is where you check it.

### [Keeping digests fresh automatically](#keeping-digests-fresh-automatically)

Pinning by digest is great for reproducibility and terrible for staying current, unless you automate the bumps. Renovate does this. It watches the registry, opens a pull request when a new digest ships, and your CI verifies the signature on that new digest before you merge. QuenchWorks has a hardened Renovate image in the catalog, so you can even run the bot on your own hardened base.

renovate.json

```
1{2  "$schema": "https://docs.renovatebot.com/renovate-schema.json",3  "extends": ["config:recommended"],4  "helm-values": {"managerFilePatterns": ["/values\\.yaml$/"]},5  "packageRules": [6    {7      "matchDatasources": ["docker"],8      "matchPackagePatterns": ["^ghcr.io/quenchworks/"],9      "pinDigests": true,10      "groupName": "quenchworks images"11    }12  ]13}
```

### [GitOps questions people actually ask](#gitops-questions-people-actually-ask)

You revert the commit. That’s the whole answer, and it’s why GitOps is worth the setup. The controller sees the repo change back to the previous digest and reconciles the cluster to match. No frantic `helm rollback` from memory, no wondering what state the cluster is in. Git history is your deploy history.

Only if you don’t automate it. With Renovate opening digest-bump PRs and CI verifying each new digest’s signature, you get the best of both: every deploy is reproducible and provable, and you’re never more than a merged PR behind the latest hardened build. Reproducibility and freshness stop being a trade-off.

Because CI is the most attacked part of most pipelines. In the push model, a compromised CI job has the keys to your cluster. In the pull model, the in-cluster controller reaches out to Git and the registry, and CI only ever touches your repo. There’s no cluster credential to steal from CI because there isn’t one there.

## [Example 3: Shipping a real application end to end](#example-3-shipping-a-real-application-end-to-end)

Enough infrastructure. Let’s deploy an actual app with a database, a cache, TLS, and network isolation, using QuenchWorks charts for every piece. Call it `acme-web`.

The moving parts:

-   Directoryapps/acme-web/
    
    -   **values.yaml** the app itself
    -   **database.yaml** postgresql, digest-pinned
    -   **cache.yaml** redis
    -   **secrets.yaml** external-secrets pulling from your vault
    -   **ingress.yaml** ingress-nginx route + cert-manager TLS
    -   **networkpolicy.yaml** default-deny + explicit allows
    

1.  Create the namespace and a default-deny NetworkPolicy. Start locked down, then open only what’s needed. Every QuenchWorks image already runs nonroot with a read-only root filesystem, so the network is the next layer to harden.

apps/acme-web/networkpolicy.yaml

```
1apiVersion: networking.k8s.io/v12kind: NetworkPolicy3metadata:4  name: default-deny5  namespace: acme6spec:7  podSelector: {}8  policyTypes: [Ingress, Egress]9---10apiVersion: networking.k8s.io/v111kind: NetworkPolicy12metadata:13  name: web-to-db14  namespace: acme15spec:16  podSelector:17    matchLabels: {app.kubernetes.io/name: postgresql}18  ingress:19    - from:20        - podSelector:21            matchLabels: {app.kubernetes.io/name: acme-web}22      ports:23        - port: 5432
```

2.  Pull secrets from your vault with external-secrets instead of committing them. The database password never touches Git.

apps/acme-web/secrets.yaml

```
1apiVersion: external-secrets.io/v1beta12kind: ExternalSecret3metadata:4  name: acme-db5  namespace: acme6spec:7  refreshInterval: 1h8  secretStoreRef:9    name: vault-backend10    kind: ClusterSecretStore11  target:12    name: acme-db-credentials13  data:14    - secretKey: password15      remoteRef:16        key: secret/acme/db17        property: password
```

3.  Deploy PostgreSQL, pinned by digest, reading its password from that secret.

PostgreSQL

```
helm install acme-db \  oci://ghcr.io/quenchworks/charts/postgresql \  --namespace acme --create-namespace \  --set auth.existingSecret=acme-db-credentials \  --set primary.persistence.size=20Gi \  --wait
```

4.  Deploy Redis for caching and sessions.

Redis

```
helm install acme-cache \  oci://ghcr.io/quenchworks/charts/redis \  --namespace acme \  --set auth.enabled=true \  --set master.persistence.size=8Gi \  --wait
```

5.  Deploy the app, wiring it to both. This is where you’d point at your own application image; the pattern is identical.

apps/acme-web/values.yaml

```
1image:2  repository: ghcr.io/your-org/acme-web3  # your app's own digest; the QuenchWorks charts pin theirs the same way4  digest: 'sha256:...'5
6replicaCount: 37
8extraEnvVars:9  - name: DATABASE_HOST10    value: acme-db-postgresql.acme.svc11  - name: DATABASE_PASSWORD12    valueFrom:13      secretKeyRef:14        name: acme-db-credentials15        key: password16  - name: REDIS_HOST17    value: acme-cache-redis-master.acme.svc18
19# QuenchWorks charts default to a locked-down security context.20# You inherit nonroot + read-only rootfs; declare writable scratch explicitly.21extraVolumes:22  - name: tmp23    emptyDir: {}24extraVolumeMounts:25  - name: tmp26    mountPath: /tmp
```

6.  Expose it with ingress-nginx and get an automatic TLS cert from cert-manager.

apps/acme-web/ingress.yaml

```
1apiVersion: networking.k8s.io/v12kind: Ingress3metadata:4  name: acme-web5  namespace: acme6  annotations:7    cert-manager.io/cluster-issuer: letsencrypt-prod8spec:9  ingressClassName: nginx10  tls:11    - hosts: [app.example.com]12      secretName: acme-web-tls13  rules:14    - host: app.example.com15      http:16        paths:17          - path: /18            pathType: Prefix19            backend:20              service:21                name: acme-web22                port: {number: 8080}
```

Note

Notice what you did not have to do: no `runAsUser`, no `readOnlyRootFilesystem`, no dropping capabilities by hand. The QuenchWorks charts set all of that through the shared `quench-common` library, so every workload starts hardened and you only declare the exceptions, like the writable `/tmp` above. Secure by default, not secure if you remember.

## [Example 4: Staying zero-CVE after day one](#example-4-staying-zero-cve-after-day-one)

The hard part of a secure catalog isn’t launch day. It’s day ninety, when new CVEs have been found in software you already deployed. Here’s how the whole system stays honest without you babysitting it.

1.  QuenchWorks rebuilds every image on a nightly schedule against a fresh vulnerability database. When upstream ships a fix, the next rebuild picks it up and the image’s fixable-CVE count stays at zero on its own.
    
2.  New digests land in GHCR, signed and attested exactly like the originals.
    
3.  Renovate (from example 2) opens a pull request bumping the digest in your Git repo.
    
4.  Your CI verifies the new digest’s signature and provenance before the PR can merge.
    
5.  Argo CD or Flux reconciles the merged change into the cluster. You went from an upstream patch to a verified, deployed fix without a human touching a cluster.
    

That loop is the actual product. Not any single image, but a pipeline where a security fix flows from upstream to your running pods through a chain that’s signed and checked at every hop.

[

![Browse the full QuenchWorks catalog](https://raw.githubusercontent.com/quenchworks/.github/main/profile/assets/chart-icon.png)

#### Browse the full QuenchWorks catalog

150+ images · 120+ charts · verified publisher

Every image with its live CVE count, plus install commands for each chart.



](https://quench-works.com)

## [Where it stands, honestly](#where-it-stands-honestly)

I’ll close the way I started, without the marketing gloss.

QuenchWorks is real and it’s running. The daily cadence keeps adding apps, the nightly rebuild keeps the CVE counts honest, and the whole supply chain is signed and verifiable today. It is not a company and it is not trying to sell you a subscription. It exists because a useful free thing got taken away and rebuilding it turned out to be a genuinely interesting engineering problem in supply-chain security.

A few things are still upstream-blocked, where a project ships a dependency that nobody has patched yet. Those show up in the catalog with their real numbers rather than hidden. When upstream moves, the rebuild catches up on its own.

If you lost your Bitnami images and you’ve been putting off dealing with it, this is a drop-in place to land. And if you just care about how you build a container catalog that can prove what’s inside it, that’s the part I’d love to talk about.

You can find everything at [quench-works.com](https://quench-works.com).

Was this useful?

## Tags

[#Wolfi](/blog/tags/wolfi)[#Apko](/blog/tags/apko)[#Melange](/blog/tags/melange)[#Bitnami](/blog/tags/bitnami)[#CVE](/blog/tags/cve)[#Trivy](/blog/tags/trivy)[#Cosign](/blog/tags/cosign)[#SBOM](/blog/tags/sbom)[#SLSA](/blog/tags/slsa)[#Helm](/blog/tags/helm)[#ArtifactHub](/blog/tags/artifacthub)[#DevSecOps](/blog/tags/devsecops)

## Share

[Facebook](https://facebook.com/sharer/sharer.php?u=https%3A%2F%2Fmkabumattar.com%2Fblog%2Fpost%2Fquenchworks-zero-cve-bitnami-alternative-wolfi "Share on Facebook")[Twitter](https://twitter.com/intent/tweet/?text=QuenchWorks%3A%20A%20Zero-CVE%2C%20Built-From-Source%20Replacement%20for%20the%20Bitnami%20Catalog&url=https%3A%2F%2Fmkabumattar.com%2Fblog%2Fpost%2Fquenchworks-zero-cve-bitnami-alternative-wolfi "Share on Twitter")[LinkedIn](https://www.linkedin.com/shareArticle?mini=true&url=https%3A%2F%2Fmkabumattar.com%2Fblog%2Fpost%2Fquenchworks-zero-cve-bitnami-alternative-wolfi&title=QuenchWorks%3A%20A%20Zero-CVE%2C%20Built-From-Source%20Replacement%20for%20the%20Bitnami%20Catalog&summary=When%20Broadcom%20moved%20the%20free%20Bitnami%20catalog%20to%20a%20legacy%20tier%2C%20thousands%20of%20teams%20lost%20their%20supply%20of%20maintained%2C%20hardened%20container%20images%20overnight.%20QuenchWorks%20is%20my%20answer%3A%20over%20150%20container%20images%20and%20120%20Helm%20charts%2C%20rebuilt%20from%20source%20on%20Wolfi%2C%20scanned%20to%20zero%20fixable%20CVEs%2C%20cosign-signed%2C%20and%20pinned%20by%20digest.%20Here%20is%20why%20I%20built%20it%20and%20how%20it%20actually%20works.&source=https://mkabumattar.com "Share on LinkedIn")[WhatsApp](https://wa.me/?text=QuenchWorks%3A%20A%20Zero-CVE%2C%20Built-From-Source%20Replacement%20for%20the%20Bitnami%20Catalog%20https%3A%2F%2Fmkabumattar.com%2Fblog%2Fpost%2Fquenchworks-zero-cve-bitnami-alternative-wolfi "Share on WhatsApp")[Telegram](https://t.me/share/url?url=https%3A%2F%2Fmkabumattar.com%2Fblog%2Fpost%2Fquenchworks-zero-cve-bitnami-alternative-wolfi&text=QuenchWorks%3A%20A%20Zero-CVE%2C%20Built-From-Source%20Replacement%20for%20the%20Bitnami%20Catalog "Share on Telegram")[Reddit](https://www.reddit.com/submit?url=https%3A%2F%2Fmkabumattar.com%2Fblog%2Fpost%2Fquenchworks-zero-cve-bitnami-alternative-wolfi&title=QuenchWorks%3A%20A%20Zero-CVE%2C%20Built-From-Source%20Replacement%20for%20the%20Bitnami%20Catalog "Share on Reddit")[Hacker News](http://news.ycombinator.com/submitlink?u=https%3A%2F%2Fmkabumattar.com%2Fblog%2Fpost%2Fquenchworks-zero-cve-bitnami-alternative-wolfi&t=QuenchWorks%3A%20A%20Zero-CVE%2C%20Built-From-Source%20Replacement%20for%20the%20Bitnami%20Catalog "Share on Hacker News")[Pinterest](https://pinterest.com/pin/create/button/?url=https%3A%2F%2Fmkabumattar.com%2Fblog%2Fpost%2Fquenchworks-zero-cve-bitnami-alternative-wolfi&media=&description=When%20Broadcom%20moved%20the%20free%20Bitnami%20catalog%20to%20a%20legacy%20tier%2C%20thousands%20of%20teams%20lost%20their%20supply%20of%20maintained%2C%20hardened%20container%20images%20overnight.%20QuenchWorks%20is%20my%20answer%3A%20over%20150%20container%20images%20and%20120%20Helm%20charts%2C%20rebuilt%20from%20source%20on%20Wolfi%2C%20scanned%20to%20zero%20fixable%20CVEs%2C%20cosign-signed%2C%20and%20pinned%20by%20digest.%20Here%20is%20why%20I%20built%20it%20and%20how%20it%20actually%20works. "Share on Pinterest")[Email](<mailto:?subject=QuenchWorks%3A%20A%20Zero-CVE%2C%20Built-From-Source%20Replacement%20for%20the%20Bitnami%20Catalog&body=Check out this article: https%3A%2F%2Fmkabumattar.com%2Fblog%2Fpost%2Fquenchworks-zero-cve-bitnami-alternative-wolfi>)

## Comments

## You might also enjoy

More posts on similar topics

[![Container Image Signing with Cosign: A Hands-On Guide to Secure Your Supply Chain](/_astro/hero.eolo66hS_Z1Eh5SE.webp)](/blog/post/container-image-signing-cosign-guide)

## [Container Image Signing with Cosign: A Hands-On Guide to Secure Your Supply Chain](/blog/post/container-image-signing-cosign-guide)

-   [Mohammad Abu Mattar](/authors/mohammad-abu-mattar)
-   [Security](/blog/categories/security)
-   [Supply Chain Security](/blog/categories/supply-chain-security)
-   [Containers](/blog/categories/containers)
-   [DevSecOps](/blog/categories/devsecops)

Container images make packaging and running apps consistent across environments, which is genuinely useful. But that consistency also brings new security questions. We need to make sure the images run

[#Cosign](/blog/tags/cosign)[#Sigstore](/blog/tags/sigstore)[#Container Signing](/blog/tags/container-signing)+4 tags

[read more](/blog/post/container-image-signing-cosign-guide)

[![The Democratization of Container Security: Docker Hardened Images](/_astro/hero.CPmb6ixV_1X6DeU.webp)](/blog/post/democratization-docker-hardened-images-container-security)

## [The Democratization of Container Security: Docker Hardened Images](/blog/post/democratization-docker-hardened-images-container-security)

-   [Mohammad Abu Mattar](/authors/mohammad-abu-mattar)
-   [Docker](/blog/categories/docker)
-   [Container Security](/blog/categories/container-security)
-   [DevSecOps](/blog/categories/devsecops)
-   [Supply Chain Security](/blog/categories/supply-chain-security)

On December 17, 2025, the world of container security changed in a big way. Docker decided to open up its entire catalog of over 1,000 Docker Hardened Images (DHI) to everyone under the Apache 2.0 lic

[#Docker Hardened Images](/blog/tags/docker-hardened-images)[#Distroless](/blog/tags/distroless)[#SBOM](/blog/tags/sbom)+5 tags

[read more](/blog/post/democratization-docker-hardened-images-container-security)

[![HashiCorp Vault vs. AWS Secrets Manager vs. SOPS: Which One Fits Your Setup](/_astro/hero.BxpvzHJN_Z2hLAEl.webp)](/blog/post/secrets-management-vault-secrets-manager-sops)

## [HashiCorp Vault vs. AWS Secrets Manager vs. SOPS: Which One Fits Your Setup](/blog/post/secrets-management-vault-secrets-manager-sops)

-   [Mohammad Abu Mattar](/authors/mohammad-abu-mattar)
-   [DevOps](/blog/categories/devops)
-   [Security](/blog/categories/security)
-   [Cloud Computing](/blog/categories/cloud-computing)

Let's face it, keeping sensitive info safe, we're talking about those digital keys like API keys and passwords, is a big deal. They're what let you into important systems and data. But with everything

[#Secrets Management](/blog/tags/secrets-management)[#HashiCorp Vault](/blog/tags/hashicorp-vault)[#AWS Secrets Manager](/blog/tags/aws-secrets-manager)+4 tags

[read more](/blog/post/secrets-management-vault-secrets-manager-sops)

[![Policy as Code with Open Policy Agent: A Technical and Governance Perspective](/_astro/hero.FMyPom55_ZBRCR.webp)](/blog/post/policy-as-code-opa-guide)

## [Policy as Code with Open Policy Agent: A Technical and Governance Perspective](/blog/post/policy-as-code-opa-guide)

-   [Mohammad Abu Mattar](/authors/mohammad-abu-mattar)
-   [DevOps](/blog/categories/devops)
-   [Security](/blog/categories/security)
-   [Cloud Governance](/blog/categories/cloud-governance)
-   [Policy as Code](/blog/categories/policy-as-code)

Think about how much stuff modern organizations have running in the cloud these days. It's a lot, right? All those servers, applications, and connections can get pretty complicated to manage. Just cli

[#Open Policy Agent](/blog/tags/open-policy-agent)[#OPA](/blog/tags/opa)[#Policy as Code](/blog/tags/policy-as-code)+9 tags

[read more](/blog/post/policy-as-code-opa-guide)

[![What's the Deal with Shift-Left Security, and Why Should You Care?](/_astro/hero.xI_32nOg_3BHjK.webp)](/blog/post/shift-left-security-sast-dast-sca-cicd)

## [What's the Deal with Shift-Left Security, and Why Should You Care?](/blog/post/shift-left-security-sast-dast-sca-cicd)

-   [Mohammad Abu Mattar](/authors/mohammad-abu-mattar)
-   [CI/CD](/blog/categories/cicd)
-   [DevSecOps](/blog/categories/devsecops)
-   [Security](/blog/categories/security)
-   [Automation](/blog/categories/automation)
-   [Application Security](/blog/categories/application-security)

Let's be honest: security can't be an afterthought. If you're still waiting until the end of your development cycle to think about vulnerabilities, you're doing it wrong. That's where \*\*shift-left sec

[#Shift Left Security](/blog/tags/shift-left-security)[#SAST](/blog/tags/sast)[#DAST](/blog/tags/dast)+7 tags

[read more](/blog/post/shift-left-security-sast-dast-sca-cicd)

[![GitOps vs. Traditional IaC for Kubernetes: A Comparative Analysis](/_astro/hero.B-RmFsqr_57hBt.webp)](/blog/post/gitops-vs-traditional-iac-kubernetes-deployment)

## [GitOps vs. Traditional IaC for Kubernetes: A Comparative Analysis](/blog/post/gitops-vs-traditional-iac-kubernetes-deployment)

-   [Mohammad Abu Mattar](/authors/mohammad-abu-mattar)
-   [DevOps](/blog/categories/devops)
-   [Infrastructure as Code](/blog/categories/infrastructure-as-code)
-   [GitOps](/blog/categories/gitops)
-   [Kubernetes](/blog/categories/kubernetes)
-   [Cloud Native](/blog/categories/cloud-native)

If you're managing modern cloud-native applications, especially with Kubernetes, you know it can be a real puzzle. Getting containers to work together, handling all those configurations, and scaling t

[#GitOps](/blog/tags/gitops)[#Infrastructure as Code](/blog/tags/infrastructure-as-code)[#IaC](/blog/tags/iac)+9 tags

[read more](/blog/post/gitops-vs-traditional-iac-kubernetes-deployment)

6 related posts
