Version: 5.9.0 | Security Audits: Quarkslab (2025), Cure53 (2025), Johanson Group SOC 2 (2025)
Passbolt uses OpenPGP-based end-to-end encryption for password sharing and team collaboration. This guide provides security hardening measures covering cryptographic key management, access control, network security, compliance, and operational best practices.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Passbolt Security Model β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β βββββββββββββββ βββββββββββββββ βββββββββββββββ β
β β Client β β Server β β Database β β
β β (Browser) β β (PHP) β β (MariaDB) β β
β βββββββββββββββ€ βββββββββββββββ€ βββββββββββββββ€ β
β β β’ GPG Keys β β β’ No access β β β’ Encrypted β β
β β β’ EncryptionβββββΊβ to plain βββββΊβ passwords β β
β β β’ Decryptionβ β text β β β’ No keys β β
β β (local) β β β’ Relay β β stored β β
β βββββββββββββββ β only β βββββββββββββββ β
β βββββββββββββββ β
β β
β Key Points: β
β β’ Private keys NEVER leave client device β
β β’ Server cannot decrypt stored passwords β
β β’ Each password encrypted individually (1:1 encryption) β
β β’ Compromise of one credential doesn't affect others β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
| Layer | Technology | Protection |
|---|---|---|
| Client-side | OpenPGP (RSA 4096-bit) | Password encryption before transmission |
| Transport | TLS 1.3 | In-transit encryption |
| Database | AES-256 | Encrypted metadata (v5.6+) |
| Metadata | Shared key rotation | Forward secrecy |
Generate Strong Server Key:
# Generate 4096-bit RSA key (minimum for production)
docker compose exec passbolt su -m -c "
gpg --batch --gen-key <<EOF
Key-Type: RSA
Key-Length: 4096
Key-Usage: sign,encrypt
Subkey-Type: RSA
Subkey-Length: 4096
Subkey-Usage: sign,encrypt
Name-Real: Passbolt Server
Name-Email: passbolt@your-domain.com
Expire-Date: 0
%no-protection
%commit
EOF
" -s /bin/sh www-data
# Verify key strength
docker compose exec passbolt gpg --list-keys --with-colons passbolt@your-domain.com
Secure Key Storage:
# Set restrictive permissions
chmod 700 /etc/passbolt/gpg
chmod 600 /etc/passbolt/gpg/private-keys/*
chmod 644 /etc/passbolt/gpg/public-keys/*
# Backup private key to secure location
docker compose exec passbolt gpg --export-secret-keys \
--armor passbolt@your-domain.com > /secure/backup/passbolt-server-key.asc
chmod 600 /secure/backup/passbolt-server-key.asc
# Store backup offline or in HSM
Enforce Key Requirements:
// config/passbolt.php
'Passbolt' => [
'Security' => [
'key_policy' => [
// Minimum key length
'min_key_length' => 4096,
// Allowed algorithms
'allowed_algorithms' => ['RSA', 'EdDSA'],
// Key expiration (days, 0 = never)
'max_key_age_days' => 730,
// Require key verification on first login
'require_key_verification' => true,
// Prohibit weak keys
'prohibit_weak_keys' => true,
],
],
],
Encrypted Metadata Key Rotation (v5.6+):
# Pre-rotation checklist
# 1. Ensure all users updated to latest client (5.6+)
# 2. Verify backup is current
# 3. Schedule during maintenance window
# Initiate rotation via CLI
docker compose exec passbolt su -m -c "
/usr/share/php/passbolt/bin/cake passbolt rotate_metadata_key
" -s /bin/sh www-data
# Monitor progress
docker compose exec passbolt su -m -c "
/usr/share/php/passbolt/bin/cake passbolt metadata_rotation_status
" -s /bin/sh www-data
Key Rotation Best Practices:
| Action | Frequency | Notes |
|---|---|---|
| Server key backup | After changes | Store offline |
| Metadata key rotation | Quarterly or after staff changes | v5.6+ feature |
| User key re-verification | Annually | Enforce via policy |
| Audit key access logs | Monthly | Review anomalies |
Enforce MFA for All Users:
'Passbolt' => [
'Authentication' => [
'mfa' => [
// Global MFA enforcement
'enabled' => true,
'required_for_all_users' => true,
// Require for specific roles
'required_for_roles' => ['admin', 'manager', 'auditor'],
// TOTP configuration
'totp' => [
'issuer' => 'Passbolt',
'digits' => 6,
'period' => 30,
'algorithm' => 'SHA256', // Stronger than SHA1
],
// Backup codes
'backup_codes' => [
'enabled' => true,
'count' => 10,
'length' => 10,
],
],
],
],
Strong Password Requirements:
'Passbolt' => [
'Authentication' => [
'password_policy' => [
// Length requirements
'min_length' => 14,
'max_length' => 128,
// Complexity requirements
'require_uppercase' => true,
'require_lowercase' => true,
'require_numbers' => true,
'require_special_chars' => true,
'min_different_chars' => 8,
// Password history
'history_count' => 12, // Remember last 12 passwords
// Expiration
'max_age_days' => 90,
'warn_before_expiry_days' => 14,
// Breach detection (Have I Been Pwned)
'check_breach' => true,
],
],
],
'Passbolt' => [
'Authentication' => [
'lockout' => [
'enabled' => true,
// Lockout threshold
'max_attempts' => 5,
'window_minutes' => 15,
// Lockout duration
'lockout_duration_minutes' => 30,
// Progressive lockout
'progressive' => true,
'progressive_multiplier' => 2,
// Admin notification
'notify_admin_on_lockout' => true,
],
],
],
'Session' => [
// Session timeout
'timeout' => 3600, // 1 hour
// Idle timeout
'idle_timeout' => 900, // 15 minutes
// Cookie security
'cookie' => [
'secure' => true, // HTTPS only
'httpOnly' => true, // No JavaScript access
'sameSite' => 'Strict', // CSRF protection
'path' => '/',
'domain' => 'passbolt.your-domain.com',
],
// Session regeneration
'regenerate' => true,
'regenerate_interval' => 300,
],
Nginx Hardened TLS:
server {
listen 443 ssl http2;
server_name passbolt.your-domain.com;
# Modern TLS only
ssl_protocols TLSv1.3;
ssl_ciphers 'TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256';
ssl_prefer_server_ciphers off;
# Certificate
ssl_certificate /etc/letsencrypt/live/passbolt.your-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/passbolt.your-domain.com/privkey.pem;
# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/passbolt.your-domain.com/chain.pem;
# Security headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'none';" always;
# DH parameters
ssl_dhparam /etc/nginx/ssl/dhparam.pem;
# Session settings
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
}
Generate DH Parameters:
# Generate strong DH parameters (takes time)
openssl dhparam -out /etc/nginx/ssl/dhparam.pem 4096
chmod 644 /etc/nginx/ssl/dhparam.pem
UFW (Ubuntu/Debian):
# Default policies
ufw default deny incoming
ufw default allow outgoing
# Allow SSH (with rate limiting)
ufw limit 22/tcp
# Allow HTTPS only
ufw allow 443/tcp
# Deny all other incoming
ufw deny incoming
# Enable firewall
ufw enable
# Verify status
ufw status verbose
firewalld (RHEL/CentOS):
# Set default zone
firewall-cmd --set-default-zone=public
# Allow services
firewall-cmd --permanent --add-service=ssh
firewall-cmd --permanent --add-service=https
# Rate limit SSH
firewall-cmd --permanent --add-rich-rule='rule service name="ssh" limit value="3/m" accept'
# Block specific threats (optional)
# firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="192.168.1.0/24" reject'
# Reload firewall
firewall-cmd --reload
# Verify configuration
firewall-cmd --list-all
Install and Configure:
# Install fail2ban
apt-get install -y fail2ban
# Create Passbolt jail
cat > /etc/fail2ban/jail.d/passbolt.conf <<EOF
[passbolt]
enabled = true
port = https
filter = passbolt
logpath = /var/log/passbolt/error.log
maxretry = 5
bantime = 3600
findtime = 600
EOF
# Create filter
cat > /etc/fail2ban/filter.d/passbolt.conf <<EOF
[Definition]
failregex = ^.*Failed login attempt.*IP: <HOST>.*$
^.*Invalid MFA code.*IP: <HOST>.*$
^.*Account locked.*IP: <HOST>.*$
ignoreregex =
EOF
# Restart fail2ban
systemctl restart fail2ban
# Check status
fail2ban-client status passbolt
DNS Records for Email Authentication:
# SPF Record (allow only specified servers to send)
passbolt.your-domain.com. IN TXT "v=spf1 include:_spf.google.com -all"
# DKIM Record (sign emails cryptographically)
default._domainkey.passbolt.your-domain.com. IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQE..."
# DMARC Record (policy for failed authentication)
_dmarc.passbolt.your-domain.com. IN TXT "v=DMARC1; p=quarantine; rua=mailto:dmarc-reports@your-domain.com; ruf=mailto:dmarc-forensics@your-domain.com; fo=1"
Secure SMTP Configuration:
# Docker environment variables
EMAIL_TRANSPORT_DEFAULT_HOST: smtp.your-domain.com
EMAIL_TRANSPORT_DEFAULT_PORT: 587
EMAIL_TRANSPORT_DEFAULT_USERNAME: passbolt@your-domain.com
EMAIL_TRANSPORT_DEFAULT_PASSWORD: ${SMTP_PASSWORD}
EMAIL_TRANSPORT_DEFAULT_TLS: true
EMAIL_TRANSPORT_DEFAULT_TIMEOUT: 30
# Use Docker secrets for password
EMAIL_TRANSPORT_DEFAULT_PASSWORD_FILE: /run/secrets/smtp_password
SMTP Credential Rotation:
# Quarterly rotation reminder
# Add to calendar: Rotate SMTP credentials
# Update in Docker secrets
echo "new-smtp-password" | docker secret create smtp_password -
# Or update environment and restart
docker compose down
# Update .env file
docker compose up -d
Default Roles:
| Role | Permissions | Use Case |
|---|---|---|
| Admin | Full system access | System administrators |
| User | Standard user access | Regular team members |
Custom Roles (v5.8+, max 2):
'Passbolt' => [
'Roles' => [
'custom' => [
[
'name' => 'Manager',
'description' => 'Team manager with limited admin capabilities',
'permissions' => [
'groups.create' => true,
'groups.edit' => true,
'groups.delete' => false,
'users.view' => true,
'users.edit' => false,
'resources.create' => true,
'resources.edit' => true,
'reports.view' => true,
],
],
[
'name' => 'Auditor',
'description' => 'Read-only compliance and audit access',
'permissions' => [
'logs.view' => true,
'reports.view' => true,
'reports.export' => true,
'users.view' => true,
'resources.view' => true,
'groups.view' => true,
'admin.access' => false,
],
],
],
],
],
Access Review Checklist:
Admin-Approved Recovery:
'Passbolt' => [
'Recovery' => [
// Require admin approval for account recovery
'require_admin_approval' => true,
// Recovery request expiration (hours)
'request_expiry_hours' => 24,
// Notify admins of recovery requests
'notify_admins' => true,
// Audit all recovery operations
'audit_recovery' => true,
],
],
'Log' => [
// Audit log for security events
'audit' => [
'className' => 'Cake\Log\Engine\SyslogLog',
'prefix' => 'passbolt-audit-',
'facility' => LOG_AUTH,
'levels' => ['info', 'notice', 'warning', 'error'],
'scopes' => ['authentication', 'authorization', 'admin'],
],
// Security event log
'security' => [
'className' => 'Cake\Log\Engine\FileLog',
'path' => LOGS . 'security/',
'file' => 'security-events',
'levels' => ['warning', 'error', 'alert', 'emergency'],
],
],
| Event Category | Events | Alert Threshold |
|---|---|---|
| Authentication | Failed logins, MFA failures, Account lockouts | 5 failures in 15 min |
| Authorization | Permission denied, Role changes, Group modifications | Any admin change |
| Data Access | Bulk exports, API token usage, Unusual access patterns | >100 resources/hour |
| Administration | Config changes, User creation/deletion, Key rotations | Any change |
Forward Logs to SIEM:
# Configure rsyslog to forward Passbolt logs
cat > /etc/rsyslog.d/50-passbolt.conf <<EOF
# Forward Passbolt audit logs to SIEM
if \$programname == 'passbolt-audit' then {
action(type="omfwd"
target="siem.your-domain.com"
port="514"
protocol="tcp"
Template="RFC5424"
)
stop
}
EOF
# Restart rsyslog
systemctl restart rsyslog
Encrypted Backup Script:
#!/bin/bash
set -e
BACKUP_DIR="/secure-backup/passbolt/$(date +%F)"
ENCRYPTION_KEY="/etc/passbolt/backup-key.gpg"
mkdir -p $BACKUP_DIR
# Database backup
mysqldump -u passbolt -p${DB_PASSWORD} \
--single-transaction \
--routines \
--triggers \
passbolt | gpg --encrypt --recipient backup@your-domain.com \
> $BACKUP_DIR/database.sql.gpg
# GPG keys backup (already encrypted)
docker compose exec passbolt gpg --export-secret-keys \
--armor passbolt@your-domain.com > $BACKUP_DIR/gpg-keys.asc
# Configuration (redact sensitive values)
sed 's/password=.*/password=REDACTED/' \
/etc/passbolt/passbolt.php > $BACKUP_DIR/passbolt.php
# Create checksum
sha256sum $BACKUP_DIR/* > $BACKUP_DIR/checksums.txt
# Upload to secure storage
aws s3 sync $BACKUP_DIR s3://secure-backup-bucket/passbolt/ \
--sse-customer-algorithm AES256 \
--sse-customer-key file:///etc/backup-encryption-key
# Local retention (30 days)
find /secure-backup/passbolt -name "*.gpg" -mtime +30 -delete
echo "Secure backup completed: $BACKUP_DIR"
Passbolt 2025 SOC 2 Type II audit covers:
| Control Category | Implementation |
|---|---|
| Access Control | RBAC, MFA, session management |
| Encryption | OpenPGP end-to-end encryption |
| Audit Logging | Comprehensive event logging |
| Change Management | Version control, approval workflows |
| Incident Response | Security response policy, breach notification |
Data Protection Measures:
'Passbolt' => [
'Privacy' => [
// Data retention
'retention' => [
'deleted_users_days' => 30,
'audit_logs_days' => 365,
'session_logs_days' => 90,
],
// Data export (GDPR Article 15)
'export' => [
'enabled' => true,
'format' => 'json',
],
// Right to erasure (GDPR Article 17)
'erasure' => [
'enabled' => true,
'anonymize_instead_of_delete' => false,
],
],
],
Key Requirements Met:
Run Comprehensive Security Check:
# Full security health check
docker compose exec passbolt su -m -c "
/usr/share/php/passbolt/bin/cake passbolt healthcheck
" -s /bin/sh www-data
# Specific domain checks
docker compose exec passbolt su -m -c "
/usr/share/php/passbolt/bin/cake passbolt healthcheck --domain gpg
" -s /bin/sh www-data
docker compose exec passbolt su -m -c "
/usr/share/php/passbolt/bin/cake passbolt healthcheck --domain email
" -s /bin/sh www-data
docker compose exec passbolt su -m -c "
/usr/share/php/passbolt/bin/cake passbolt healthcheck --domain config
" -s /bin/sh www-data
#!/bin/bash
# Passbolt Security Scan
echo "=== Passbolt Security Scan ==="
echo ""
# Check TLS configuration
echo "[1/5] Checking TLS configuration..."
nmap --script ssl-enum-ciphers -p 443 passbolt.your-domain.com
# Check for security headers
echo ""
echo "[2/5] Checking security headers..."
curl -sI https://passbolt.your-domain.com | grep -E "(Strict-Transport|X-Frame|X-Content|Content-Security)"
# Check GPG key strength
echo ""
echo "[3/5] Checking GPG key strength..."
docker compose exec passbolt gpg --list-keys --with-colons passbolt@* | grep "^pub"
# Check for outdated packages
echo ""
echo "[4/5] Checking for security updates..."
docker compose exec passbolt apt list --upgradable 2>/dev/null | grep -i security
# Check failed login attempts
echo ""
echo "[5/5] Recent failed login attempts..."
docker compose exec passbolt grep "Failed login" /var/log/passbolt/error.log | tail -10
echo ""
echo "=== Scan Complete ==="
Immediate Actions:
| Role | Contact | Escalation |
|---|---|---|
| Security Team | security@your-domain.com | Immediate |
| Passbolt Security | security@passbolt.com | Critical only |
| Passbolt Support | https://www.passbolt.com/support | Business hours |
| Audit | Date | Firm | Report |
|---|---|---|---|
| SOC 2 Type II | 2025 | Johanson Group | Available on request |
| Cryptographic Review | 2025 | Cure53 | Public report |
| Penetration Test | 2025 | Quarkslab | Public report |
Any questions?
Feel free to contact us. Find all contact information on our contact page.