Guide

Install with Helm

Run Penweir on Kubernetes with the Helm chart: secrets, values, install, first admin, upgrades and rollbacks.

The chart runs the same image as the Docker route, with the same hardening: non-root (65532), read-only root filesystem, all capabilities dropped, RuntimeDefault seccomp and no service account token. It runs one replica. High availability is an Enterprise feature.

You need Kubernetes 1.27 or later and Helm 3.

1. Get the chart

Download penweir-1.3.0.tgz from the download page. The chart version matches the image version.

2. Create the secrets

The chart never creates secrets itself, so no secret value ends up in Helm’s release history. Create the namespace, with the restricted Pod Security level, and two Secrets:

kubectl create namespace penweir
kubectl label namespace penweir pod-security.kubernetes.io/enforce=restricted
kubectl -n penweir create secret generic penweir-master \
  --from-literal=master-key="$(docker run --rm ghcr.io/robinhana04/penweir-app:1.3.0 keygen master)"
kubectl -n penweir create secret generic penweir-providers \
  --from-literal=openai="$OPENAI_API_KEY"

The master key encrypts stored provider keys and two-factor seeds. Keep a copy in your secret manager: a backup of the database is useless without it.

3. Write a values file

Save this as values.yaml. It points the chart at your Secrets and holds the Penweir configuration:

image:
  tag: "1.3.0"
masterKey:
  existingSecret: penweir-master
secretEnv:
  - {name: OPENAI_API_KEY, secretName: penweir-providers, key: openai}
config:
  version: 1
  providers:
    - name: openai
      type: openai_compatible
      base_url: https://api.openai.com/v1
      api_key: {env: OPENAI_API_KEY}
  models:
    - {name: cloud/default, targets: {openai: gpt-4.1-mini}}
  pipelines:
    - name: default
      default: true
      routing: {allow: [openai]}
      request_steps:
        - {detector: iban, action: tokenize}
        - {detector: email, action: tokenize}
        - {detector: bsn, action: block}

On Kubernetes the ConfigMap is the source of truth, so the dashboard shows the configuration read-only. Change config in your values and run helm upgrade. The pod restarts with the new configuration. To keep the configuration in a ConfigMap you manage yourself, set existingConfigMap.

Everything that lives in the database still works from the dashboard: applications, keys, admins, stored secrets and the audit log.

4. Install and create the first admin

helm install penweir ./penweir-1.3.0.tgz -n penweir -f values.yaml --wait
helm test penweir -n penweir
kubectl -n penweir exec -it deploy/penweir -- penweir admin create <your-name>

There is no default account. The command asks for a password of at least 12 characters.

Open the dashboard through a port-forward:

kubectl -n penweir port-forward svc/penweir-admin 8081:8081

Then go to http://127.0.0.1:8081 and send your first request. The gateway is svc/penweir on port 8080.

Values

Value Default Meaning
image.repository, image.tag the chart’s appVersion The image to run. Pin a digest in production.
config {version: 1} The contents of penweir.yaml.
existingConfigMap empty Use this ConfigMap (key penweir.yaml) instead of config.
masterKey.existingSecret, .key empty, master-key The master key for stored secrets and TOTP.
secretEnv [] Environment variables from Secrets, for api_key: {env: ...}.
database.existingSecret, .key empty, url A Secret with a Postgres URL. SQLite on the volume otherwise.
license.existingSecret, .key empty, license.json The licence file.
persistence.* enabled, 1Gi, ReadWriteOnce The data volume. Keep it even with Postgres: it holds the hash key.
ingress.*, adminIngress.* disabled Ingresses. Terminate TLS there.
networkPolicy.* disabled See below.
resources 100m / 256Mi request, 1Gi limit Container resources.
logLevel, auditRetentionDays info, 90 Log level and how long audit records are kept.

Exposing it

Expose the gateway port to applications only. Keep the admin port internal: use kubectl port-forward, or an adminIngress restricted to your VPN. Terminate TLS at the Ingress and send Strict-Transport-Security from it.

Network policy

With networkPolicy.enabled: true, the pod accepts gateway traffic only from the peers in ingressFrom (from anywhere if that is empty). The admin port is closed except to the peers in adminIngressFrom; kubectl port-forward keeps working. The pod sends traffic only to DNS and to the rules in egress. List your providers there to enforce it at the cluster level:

networkPolicy:
  enabled: true
  ingressFrom:
    - namespaceSelector: {matchLabels: {kubernetes.io/metadata.name: apps}}
  egress:
    - to: [{ipBlock: {cidr: 10.20.0.0/16}}]   # vLLM in the cluster
      ports: [{port: 8000}]

Your CNI must support NetworkPolicy for it to have any effect. With ingressFrom set, also allow the release’s test pod (app.kubernetes.io/name: penweir-test), or helm test cannot reach Penweir.

Postgres

kubectl -n penweir create secret generic penweir-db \
  --from-literal=url='postgresql+asyncpg://penweir:<password>@postgres:5432/penweir'

Set database.existingSecret: penweir-db. Penweir migrates the schema on startup. The database holds metadata and configuration only.

Upgrading

  1. Back up the data volume, and the database if you use Postgres.
  2. Read the release notes for changes to the values.
  3. Run helm upgrade penweir ./penweir-<new-version>.tgz -n penweir -f values.yaml --wait. The Recreate strategy stops the old pod before the new one starts, so two versions never share the database. Migrations run on startup.
  4. Run helm test penweir -n penweir.

To roll back, run helm rollback penweir -n penweir. A migration that has already run is not undone by a rollback. Restore the backup if the old version cannot read the new schema.

Removing it

helm uninstall penweir -n penweir removes the release. The data volume is kept by default; delete the PersistentVolumeClaim yourself to remove the data.