PrivateBin
A minimalist, zero-knowledge encrypted paste service.
PrivateBin is a zero-knowledge pastebin where the server has no knowledge of pasted content. All encryption and decryption happens client-side in the browser using AES-256. Self-hosting ensures pastes are not stored on third-party servers, making it suitable for sharing sensitive configuration or code snippets.
Alternatives considered
Cloud Hosted
| Tool | Open Source | Free Tier | Monthly Cost |
|---|---|---|---|
| Pastebin | No | Limited | Free / From $4.99/mo |
| GitHub Gists | No | Yes | Free; requires GitHub account |
Self Hosted
| Tool | Open Source | Full Features | Notes |
|---|---|---|---|
| Hastebin | Yes | Yes | No encryption, plaintext only |
Installation
Architecture
- Deployment: Single
privatebindeployment in theprivatebinnamespace - Image:
privatebin/nginx-fpm-alpine, digest-pinned via Renovate - Storage: Longhorn PVC (
privatebin-data) for paste storage; tmpfs volumes fortmp,var-run,var-tmp - Networking: ClusterIP service on port 80, HTTPRoute via public gateway
Security
- Runs as
runAsUser: 65534,runAsNonRoot: true,readOnlyRootFilesystem: true,allowPrivilegeEscalation: false, capabilities dropped - Zero-knowledge architecture — server never sees plaintext (client-side AES-256 encryption)
- Longhorn PVC encrypted at rest; tmpfs mounts enforce read-only container filesystem
Updates
Managed by Renovate. Image is digest-pinned.
Data Management
- PVC:
privatebin-data(Longhorn-encrypted) for paste storage - Backups: the namespace has the k8up schedule for Shlink's
shlink-data(short links are permanent);privatebin-datais excluded withk8up.io/backup: "false"because pastes are ephemeral by design
User Management
No user accounts or authentication. PrivateBin is anonymous by design.
Configuration Management
- App configuration (expiry, format options, etc.) from ConfigMap (
privatebin-config) - Longhorn encryption key from SOPS-encrypted secret
Usage
Paste text, code, or data into the web UI. Choose an expiration time and optional password. Share the generated link — only the URL fragment (containing the decryption key) allows decryption, and the server never sees it.
Cluster-specific deviations from the above live in the per-cluster README — see k8s/apps/privatebin/talos/README.md.
Cluster Deployment
PrivateBin — Talos cluster
Deviations from defaults
-
URL shortener: a Shlink instance (sqlite, single container) runs in this namespace under
shlink/, wired into PrivateBin via the server-side proxy (?shortenviashlink). The public route ats.web.kueber.eumatches only short codes (base62 regex); the REST API is only called cluster-internally by privatebin, authenticated with theINITIAL_API_KEYpinned in SOPS. Shlink has no admin web UI — manage it viakubectl exec(shlinkCLI) when needed.conf.phplives in a SOPS secret instead of a ConfigMap because it carries the api key. Ingress labels are patched innetwork-policies/(name-targeted, since a LabelTransformer would label every Deployment in the overlay). -
Backups cover Shlink, not PrivateBin.
shlink-datais annotated for k8up (short links are permanent);privatebin-datacarriesk8up.io/backup: "false"because pastes are ephemeral by design. The backups component is in the overlay only for Shlink.
- Image:
privatebin/nginx-fpm-alpine:2.0.6@sha256:13290e2f04bfd98cf8fc7e8d216fb76b2b2d12373d4923b859cd41c2d984fde8 - Image:
shlinkio/shlink:5.1.7@sha256:844e1f2b6455331c5b49207bad3a2455ccd65e62bb9daa09a6c57afff26bd9c8
Rendered manifests (kustomize build)
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
kustomize.toolkit.fluxcd.io/force: enabled
name: privatebin
namespace: privatebin
spec:
replicas: 1
revisionHistoryLimit: 2
selector:
matchLabels:
app.kubernetes.io/name: privatebin
strategy:
type: Recreate
template:
metadata:
labels:
app.kubernetes.io/name: privatebin
spec:
automountServiceAccountToken: false
containers:
- image: privatebin/nginx-fpm-alpine:2.0.6@sha256:13290e2f04bfd98cf8fc7e8d216fb76b2b2d12373d4923b859cd41c2d984fde8
livenessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 10
periodSeconds: 30
name: privatebin
ports:
- containerPort: 8080
name: http
readinessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
resources:
limits:
cpu: 500m
memory: 256Mi
requests:
cpu: 50m
memory: 64Mi
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
volumeMounts:
- mountPath: /srv/data
name: data
- mountPath: /srv/cfg/conf.php
name: config
readOnly: true
subPath: conf.php
- mountPath: /tmp
name: tmp
- mountPath: /var/run
name: var-run
- mountPath: /var/tmp
name: var-tmp
priorityClassName: tier-3
securityContext:
fsGroup: 65534
runAsGroup: 65534
runAsNonRoot: true
runAsUser: 65534
volumes:
- name: data
persistentVolumeClaim:
claimName: privatebin-data
- name: config
secret:
secretName: privatebin-config
- emptyDir: {}
name: tmp
- emptyDir: {}
name: var-run
- emptyDir: {}
name: var-tmp
---
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
kustomize.toolkit.fluxcd.io/force: enabled
name: shlink
namespace: privatebin
spec:
replicas: 1
revisionHistoryLimit: 2
selector:
matchLabels:
app.kubernetes.io/name: shlink
strategy:
type: Recreate
template:
metadata:
labels:
app.kubernetes.io/name: shlink
spec:
automountServiceAccountToken: false
containers:
- env:
- name: DEFAULT_DOMAIN
value: s.web.kueber.eu
- name: IS_HTTPS_ENABLED
value: 'true'
- name: WEB_WORKER_NUM
value: '2'
- name: TASK_WORKER_NUM
value: '2'
envFrom:
- secretRef:
name: shlink
image: shlinkio/shlink:5.1.7@sha256:844e1f2b6455331c5b49207bad3a2455ccd65e62bb9daa09a6c57afff26bd9c8
livenessProbe:
httpGet:
path: /rest/health
port: 8080
initialDelaySeconds: 30
periodSeconds: 30
name: shlink
ports:
- containerPort: 8080
name: http
readinessProbe:
httpGet:
path: /rest/health
port: 8080
initialDelaySeconds: 10
periodSeconds: 10
resources:
limits:
cpu: 500m
memory: 384Mi
requests:
cpu: 50m
memory: 128Mi
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
volumeMounts:
- mountPath: /etc/shlink/data
name: data
- mountPath: /tmp
name: tmp
priorityClassName: tier-3
securityContext:
fsGroup: 1001
runAsGroup: 1001
runAsNonRoot: true
runAsUser: 1001
seccompProfile:
type: RuntimeDefault
volumes:
- name: data
persistentVolumeClaim:
claimName: shlink-data
- emptyDir: {}
name: tmp