Skip to main content

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

ToolOpen SourceFree TierMonthly Cost
PastebinNoLimitedFree / From $4.99/mo
GitHub GistsNoYesFree; requires GitHub account

Self Hosted

ToolOpen SourceFull FeaturesNotes
HastebinYesYesNo encryption, plaintext only

Installation​

Architecture​

  • Deployment: Single privatebin deployment in the privatebin namespace
  • Image: privatebin/nginx-fpm-alpine, digest-pinned via Renovate
  • Storage: Longhorn PVC (privatebin-data) for paste storage; tmpfs volumes for tmp, 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-data is excluded with k8up.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 at s.web.kueber.eu matches only short codes (base62 regex); the REST API is only called cluster-internally by privatebin, authenticated with the INITIAL_API_KEY pinned in SOPS. Shlink has no admin web UI — manage it via kubectl exec (shlink CLI) when needed. conf.php lives in a SOPS secret instead of a ConfigMap because it carries the api key. Ingress labels are patched in network-policies/ (name-targeted, since a LabelTransformer would label every Deployment in the overlay).

  • Backups cover Shlink, not PrivateBin. shlink-data is annotated for k8up (short links are permanent); privatebin-data carries k8up.io/backup: "false" because pastes are ephemeral by design. The backups component is in the overlay only for Shlink.

Kubernetes Metadata
  • 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