This document outlines security considerations and best practices for deploying Snipo.
Snipo is designed as a local-first, self-hosted application. The security model assumes:
- Single-user deployment - One master password protects all data
- Local network or VPN access - Not exposed directly to the internet
- No CDN dependencies - All assets served locally to prevent supply chain attacks
- Master password hashed at startup with Argon2id (OWASP recommended parameters)
- Progressive login delays - exponential backoff after failed attempts (1s, 2s, 4s, 8s, 16s, 30s max)
- Session tokens hashed with SHA256 before database storage
- Secure cookies:
HttpOnly,Secure,SameSite=Strict - Session expiration with automatic cleanup
- API tokens with SHA256 hashing and optional expiration
- Rate limiting on authentication endpoints (configurable)
- Session secret warning - logs warning if
SNIPO_SESSION_SECRETnot explicitly set
Content-Security-Policy- Restricts resource loading to same-originX-Content-Type-Options: nosniff- Prevents MIME sniffingX-Frame-Options: DENY- Prevents clickjackingX-XSS-Protection: 1; mode=block- Legacy XSS protectionStrict-Transport-Security- Enforces HTTPSReferrer-Policy: strict-origin-when-cross-originPermissions-Policy- Disables camera, microphone, geolocation
- JSON request body size limits (2MB max)
- Content size limits (1MB per file)
- Tag name validation (alphanumeric, underscores, hyphens)
- Language allowlist validation
- SQLite with foreign key constraints enabled
- Parameterized queries (SQL injection protection)
- WAL mode for crash recovery
# OPTION 1 (Recommended): Use pre-hashed password
# Generate hash with: ./snipo hash-password your-password
SNIPO_MASTER_PASSWORD_HASH=$argon2id$base64salt$base64hash
# OPTION 2: Plain text password (backward compatible, less secure)
SNIPO_MASTER_PASSWORD=your-very-secure-password-here
# REQUIRED: Random session secret (generate with: openssl rand -hex 32)
SNIPO_SESSION_SECRET=$(openssl rand -hex 32)
# Rate limiting (adjust based on expected usage)
SNIPO_RATE_LIMIT=100
SNIPO_RATE_WINDOW=1m
# Only enable if behind a trusted reverse proxy (nginx, traefik, etc.)
SNIPO_TRUST_PROXY=falseDocker Compose Warning: When using
SNIPO_MASTER_PASSWORD_HASHin docker-compose.yml, the$characters in Argon2id hashes will be interpreted as variable substitution. Either:
- Use double dollar signs:
$$argon2id$$base64salt$$base64hash- Quote the value:
"SNIPO_MASTER_PASSWORD_HASH=$argon2id$base64salt$base64hash"- Use a
.envfile and reference it:SNIPO_MASTER_PASSWORD_HASH=${SNIPO_MASTER_PASSWORD_HASH}
Password Security Best Practices:
- Use hashed passwords - Generate with
./snipo hash-passwordand setSNIPO_MASTER_PASSWORD_HASH - Strong passwords - Minimum 12 characters with mixed case, numbers, and symbols
- Never commit plain passwords - If using plain text, use environment variables or secrets management
- Rotate regularly - Change passwords periodically, especially after potential exposure
Generating Password Hashes:
# Using binary
./snipo hash-password
# Using Docker
docker run --rm ghcr.io/mohamedelashri/snipo:latest hash-password
# With password as argument (less secure, visible in shell history)
./snipo hash-password "your-password"The hash uses Argon2id with OWASP-recommended parameters (memory: 64MB, iterations: 1, parallelism: 4).
Hash Format: $argon2id$base64salt$base64hash
Migration from Plain Text:
# 1. Generate hash for current password
./snipo hash-password "current-password"
# 2. Update environment variable
SNIPO_MASTER_PASSWORD_HASH=$argon2id$...
# 3. Remove plain password (optional, hash takes precedence)
# unset SNIPO_MASTER_PASSWORD
# 4. Restart serviceWhy Use Hashed Passwords:
- Passwords never visible in config files, logs, or process listings
- Safer for version control (even with encrypted hashes)
- Reduces exposure if configs are accidentally leaked
- Backward compatible (plain passwords still supported)
Important Notes:
- Either
SNIPO_MASTER_PASSWORDorSNIPO_MASTER_PASSWORD_HASHis required - If both are set, the hash takes precedence
- Hash format is validated at startup (must start with
$argon2id$) - Use secrets management (Docker Secrets, Vault) in production
- Protect config files with appropriate permissions (e.g.,
chmod 600 .env)
Snipo offers three authentication modes to balance security and usability for different deployment scenarios:
Standard mode with login page and password protection.
SNIPO_MASTER_PASSWORD=your-secure-password
# or (recommended)
SNIPO_MASTER_PASSWORD_HASH=$argon2id$base64salt$base64hash
SNIPO_SESSION_SECRET=$(openssl rand -hex 32)Features:
- Login page required to access web UI
- Session-based authentication for all operations
- API token operations always require password verification (additional security layer)
- All admin operations require authentication
Security Model:
- Login required to access UI and API
- Session authentication for read/write operations
- Master password required for creating API tokens (even with valid session)
- Master password required for deleting API tokens (even with valid session)
Use when:
- Default deployment scenario
- Public internet exposure (behind HTTPS reverse proxy)
- Standard security requirements
- Maximum security needed
Hide login page while maintaining API password protection for admin operations.
Configuration:
- Log in to your Snipo instance
- Navigate to Settings → General
- Enable "Disable Login Page"
How it works:
- Web UI is accessible without login page
- Login page redirects to home
- All standard operations work without authentication
- API token operations always require password verification
Security characteristics:
- Web UI access: No session required
- API read/write operations: No session required
- API token creation: ❌ Always requires password (security best practice)
- API token deletion: ❌ Always requires password (security best practice)
- Settings changes: No session required
- Backup/restore: No session required
Enhanced Security: Even in this mode, API token management operations (create/delete) always require the master password. This provides:
- Protection against session hijacking
- Defense against XSS attacks
- Additional security layer for sensitive operations
- Prevention of unauthorized token management
Use when:
- Deployed on private networks (Tailscale, WireGuard, VPN)
- Trusted local environments
- You want easy access but protected admin features
- Primary API token usage with controlled token creation
Example workflow:
# 1. Create API token (password required for security)
curl -X POST http://localhost:8080/api/v1/tokens \
-H "Content-Type: application/json" \
-d '{
"name":"My Token",
"permissions":"admin",
"password":"your-secure-password"
}'
# Response includes the token (save it securely)
# {"token":"snipo_...",...}
# 2. Use API token for all operations (no password needed)
curl http://localhost:8080/api/v1/snippets \
-H "Authorization: Bearer <api-token>"
# 3. Delete token (password required for security)
curl -X DELETE http://localhost:8080/api/v1/tokens/1 \
-H "Content-Type: application/json" \
-d '{"password":"your-secure-password"}'Benefits:
- No login friction for trusted environments
- Easy access for reading and managing snippets
- Strong security for API token operations (password always required)
- Protection against unauthorized token creation/deletion
- Defense-in-depth: even if session is compromised, tokens are protected
- Suitable for personal networks, VPNs, and auth proxy deployments
- Balances convenience with security
Disable all authentication via environment variable.
SNIPO_DISABLE_AUTH=true
# Password variables not required when auth is disabledSecurity characteristics:
- Web UI access: No authentication
- All API operations: No authentication
- Token creation: No password verification (
⚠️ bypassed) - Token deletion: No password verification (
⚠️ bypassed) - Settings changes: No authentication
- Admin operations: No authentication
Critical Difference from Mode 2:
- Mode 2: Password still required for token operations
- Mode 3: Password requirement completely bypassed
- Use Mode 3 only when external authentication handles all access control Use ONLY when:
-
Behind Authentication Proxy - When using external authentication layers:
- Authelia
- OAuth2 Proxy
- Cloudflare Access
- NGINX with
auth_request - Traefik with ForwardAuth
- etc.
-
Isolated Local Environment - Completely offline, no network access
-
Development/Testing - Local development only, never in production
Configuration:
# In .env or environment
SNIPO_DISABLE_AUTH=true
# Password variables are ignored when auth is disabled
# SNIPO_MASTER_PASSWORD not required
# SNIPO_MASTER_PASSWORD_HASH not requiredSecurity Implications:
- All authentication checks are bypassed
- No login required
- All API endpoints are accessible without credentials
- No rate limiting on authentication
- Complete trust in external authentication layer
- Direct exposure to internet is extremely dangerous
Example 1: Docker with Authelia
services:
authelia:
image: authelia/authelia:latest
volumes:
- ./authelia-config:/config
networks:
- auth-network
snipo:
image: ghcr.io/mohamedelashri/snipo:latest
environment:
- SNIPO_DISABLE_AUTH=true # Auth handled by Authelia
networks:
- auth-network
# Snipo is NOT exposed directly to internet
nginx:
image: nginx:alpine
ports:
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
networks:
- auth-networkExample 2: Nginx with auth_request
location /auth {
internal;
proxy_pass http://authelia:9091/api/verify;
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
}
location / {
auth_request /auth;
proxy_pass http://snipo:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}When using SNIPO_DISABLE_AUTH=true, verify:
- External authentication layer is properly configured
- Snipo is NOT directly accessible from the internet
- All traffic passes through authentication proxy
- Authentication proxy is configured with strict policies
- Network isolation is in place (Docker networks, VPNs)
- Logs are monitored for unauthorized access attempts
- Backup authentication layer exists (defense in depth)
- Regular security audits of authentication setup
To re-enable authentication:
# 1. Remove or set to false
SNIPO_DISABLE_AUTH=false
# or just remove the variable entirely
# 2. Set password
SNIPO_MASTER_PASSWORD_HASH=$argon2id$...
# or
SNIPO_MASTER_PASSWORD=your-secure-password
# 3. Restart
docker compose restart snipoIf deploying behind a reverse proxy:
- Set
SNIPO_TRUST_PROXY=trueto trustX-Forwarded-Forheaders - Configure your proxy to set proper headers:
Nginx example:
location / {
proxy_pass http://localhost:8080;
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;
}Traefik example:
http:
middlewares:
secure-headers:
headers:
forceSTSHeader: true
stsSeconds: 31536000The Docker image runs as non-root user (snipo, UID 1000):
services:
snipo:
image: ghcr.io/mohamedelashri/snipo:latest
security_opt:
- no-new-privileges:true
read_only: true
tmpfs:
- /tmp
volumes:
- snipo_data:/dataThe Content Security Policy includes:
'unsafe-inline'for styles (required for dynamic styling)'unsafe-eval'for scripts (required for Alpine.js)
These are necessary for the current frontend stack but reduce XSS protection. The plan is to migrate to a more secure frontend stack in the future.
- No role-based access control
- All authenticated users have full access
- Password changes are in-memory only (reset on restart)
All dependencies are vendored and version-pinned in go.mod:
| Package | Version | Purpose |
|---|---|---|
go-chi/chi |
v5.1.0 | HTTP router |
golang.org/x/crypto |
v0.28.0 | Argon2id password hashing |
modernc.org/sqlite |
v1.33.1 | Pure-Go SQLite driver |
aws-sdk-go-v2 |
v1.40.1 | S3 backup support |
All frontend assets are served locally from /static/vendor/:
| Library | Version | File |
|---|---|---|
| Alpine.js | 3.x | alpine.min.js |
| htmx | 2.x | htmx.min.js |
| Ace Editor | 5.x | ace.js |
| Prism.js | 1.x | prism.min.js |
| Pico CSS | 2.x | pico.min.css |
| Fira Code | - | FiraCode-*.woff2 |
Go dependencies:
# Check for updates
go list -u -m all
# Update all dependencies
go get -u ./...
go mod tidy
# Update specific package
go get -u golang.org/x/crypto@latestFrontend dependencies:
Download new versions and replace files in internal/web/static/vendor/:
# Example: Update Alpine.js
curl -o internal/web/static/vendor/js/alpine.min.js \
https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js- Use hashed password (
SNIPO_MASTER_PASSWORD_HASH) instead of plain text - Set strong master password (12+ characters, mixed case, numbers, symbols)
- Generate random session secret with
openssl rand -hex 32 - Configure rate limiting appropriately
- Use HTTPS in production (via reverse proxy)
- Set
SNIPO_TRUST_PROXY=falseunless behind trusted proxy - Restrict network access (firewall/VPN)
- Protect config files with proper permissions (
chmod 600 .env) - Use secrets management in production (Docker Secrets, Vault, etc.)
- Regular backups with encryption enabled
- Keep dependencies updated
- Authentication proxy is properly configured and tested
- Snipo is NOT directly accessible from internet
- Network isolation is enforced (Docker networks, firewalls)
- All traffic must pass through authentication layer
- Monitoring and logging are in place
- Regular security audits of authentication setup
- Understand and accept the security implications
If you discover a security vulnerability, please report it privately via GitHub Security Advisories or email. Do not create public issues for security vulnerabilities.