Skip to content

Hardening Guide

This guide covers security hardening measures for production Pinchy deployments. Pinchy is self-hosted — infrastructure security is your responsibility. These recommendations assume a Linux server with Docker, but most apply to any OS.

Never expose the Pinchy Web service directly. Use a reverse proxy with TLS termination.

pinchy.example.com {
reverse_proxy localhost:7777
}

Caddy automatically provisions and renews Let’s Encrypt certificates.

server {
listen 443 ssl http2;
server_name pinchy.example.com;
ssl_certificate /etc/letsencrypt/live/pinchy.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/pinchy.example.com/privkey.pem;
# TLS hardening
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# Security headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options DENY always;
location / {
proxy_pass http://127.0.0.1:7777;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support (required for agent communication)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
server {
listen 80;
server_name pinchy.example.com;
return 301 https://$host$request_uri;
}

Only port 443 (HTTPS) needs to be exposed. Block everything else.

Terminal window
ufw default deny incoming
ufw default allow outgoing
ufw allow ssh
ufw allow 443/tcp
ufw enable
Terminal window
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -i lo -j ACCEPT
iptables -A INPUT -j DROP

PostgreSQL stores all Pinchy data on disk — user accounts, agent configurations, audit logs, and encrypted API keys. Encrypt the underlying volume.

Terminal window
# Encrypt the partition (destructive — do this before deploying)
cryptsetup luksFormat /dev/sdX
cryptsetup open /dev/sdX pinchy-data
mkfs.ext4 /dev/mapper/pinchy-data
mount /dev/mapper/pinchy-data /var/lib/docker/volumes

Enable FileVault in System Settings → Privacy & Security. This encrypts the entire startup disk, including Docker volumes.

The Pinchy Docker image runs as a non-root user by default. Verify this is not overridden in your docker-compose.yml:

services:
pinchy:
# Do NOT add: user: root
security_opt:
- no-new-privileges:true

Mount the container filesystem as read-only and whitelist only the directories that need writes:

services:
pinchy:
read_only: true
tmpfs:
- /tmp
volumes:
- pinchy-data:/data

Prevent a single container from consuming all host resources:

services:
pinchy:
deploy:
resources:
limits:
cpus: "2.0"
memory: 2G
reservations:
memory: 512M
db:
deploy:
resources:
limits:
cpus: "1.0"
memory: 1G

The .env file contains secrets like DATABASE_URL, BETTER_AUTH_SECRET, and AUDIT_HMAC_SECRET. Protect it.

Terminal window
chmod 600 .env
chown root:root .env

If you use Docker Swarm, use secrets instead of environment variables:

services:
pinchy:
secrets:
- db_password
- auth_secret
- audit_hmac_secret
secrets:
db_password:
file: ./secrets/db_password.txt
auth_secret:
file: ./secrets/auth_secret.txt
audit_hmac_secret:
file: ./secrets/audit_hmac_secret.txt

Pinchy’s Docker Compose setup uses an internal network. PostgreSQL and OpenClaw are not exposed to the host network.

Terminal window
# Check that only Pinchy Web publishes ports
docker compose ps --format "table {{.Name}}\t{{.Ports}}"

Only the pinchy service should show a published port (7777 by default). The db and openclaw services should show no published ports, or only 127.0.0.1:PORT bindings.

For additional isolation, define an explicit internal network:

networks:
pinchy-internal:
internal: true
pinchy-external:
services:
pinchy:
networks:
- pinchy-internal
- pinchy-external
ports:
- "127.0.0.1:7777:7777"
db:
networks:
- pinchy-internal
openclaw:
networks:
- pinchy-internal

The internal: true flag prevents containers on that network from reaching the internet. The pinchy service bridges both networks — it can reach the database and OpenClaw internally, while also serving HTTP traffic.

The audit trail uses HMAC-SHA256 to sign every log entry. By default, Pinchy auto-generates a secret at startup. For production, set a persistent secret.

Terminal window
openssl rand -hex 32
.env
AUDIT_HMAC_SECRET=your-generated-hex-string

Create regular backups with pg_dump:

Terminal window
# Backup
docker compose exec db pg_dump -U pinchy pinchy | gzip > "pinchy-backup-$(date +%Y%m%d).sql.gz"
# Restore
gunzip < pinchy-backup-20260222.sql.gz | docker compose exec -T db psql -U pinchy pinchy

Encrypt backups before storing them off-site:

Terminal window
# Backup + encrypt with GPG
docker compose exec db pg_dump -U pinchy pinchy | gzip | gpg --symmetric --cipher-algo AES256 > "pinchy-backup-$(date +%Y%m%d).sql.gz.gpg"
# Decrypt + restore
gpg --decrypt pinchy-backup-20260222.sql.gz.gpg | gunzip | docker compose exec -T db psql -U pinchy pinchy

Set up a cron job for daily backups:

/etc/cron.d/pinchy-backup
0 3 * * * root cd /opt/pinchy && docker compose exec -T db pg_dump -U pinchy pinchy | gzip | gpg --symmetric --batch --passphrase-file /root/.pinchy-backup-passphrase --cipher-algo AES256 > /backups/pinchy-$(date +\%Y\%m\%d).sql.gz.gpg

Check for new Pinchy releases and update:

Terminal window
docker compose pull
docker compose up -d

Always read the release notes before updating. Back up the database first.

Use tools like Trivy to scan your running images for known vulnerabilities:

Terminal window
trivy image ghcr.io/heypinchy/pinchy:latest

Consider integrating this into a CI pipeline or running it on a schedule. See the SBOM reference for information about Pinchy’s software bill of materials.

Monitor the Pinchy Web service by polling its health endpoint:

Terminal window
curl -f https://pinchy.example.com/api/health || echo "Pinchy is down"

Add health checks to your docker-compose.yml:

services:
pinchy:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:7777/api/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
db:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U pinchy"]
interval: 30s
timeout: 5s
retries: 3

Monitor Docker logs for errors and suspicious activity:

Terminal window
# Follow all service logs
docker compose logs -f
# Filter for errors
docker compose logs pinchy 2>&1 | grep -i error

For production, forward logs to a centralized logging system (e.g., Loki, Elasticsearch) and set up alerts for:

  • Authentication failures (auth.failed audit events)
  • Denied tool executions (tool.denied audit events)
  • Container restarts
  • High error rates

Use the audit trail API to monitor for security-relevant events programmatically:

Terminal window
# Check for failed logins in the last 24 hours
curl -b session_cookie "https://pinchy.example.com/api/audit?eventType=auth.failed&from=$(date -d '24 hours ago' -Iseconds)"

See Audit Trail for the full API reference.