Ultimate passbolt guide: from Docker to Podman, security, and remote management on Rocky Linux
Back to blog

Ultimate passbolt guide: from Docker to Podman, security, and remote management on Rocky Linux

6/7/2026 · 5 min · Infrastructure

In today's infrastructure ecosystem, credential security is non-negotiable. Recently, I dove into the challenge of implementing and migrating robust password managers (Passbolt and Vaultwarden) in Rocky Linux environments, transitioning from the traditional Docker model to the sovereignty of Podman.

If you are looking for a resilient, protected implementation that is easy to manage remotely, this guide gathers lessons learned "in the trenches" of both worlds.


⚠️ Security warnings before you start#

About the GPG private key#

The GPG private key is the sole artifact that decrypts every password stored in Passbolt.

What "ultimate trust" means in GPG#

GPG has 6 trust levels for keys:

LevelCodeMeaning
Unknown2Trust unknown or undefined
Not Trusted3Key explicitly untrusted
Marginally Trusted4Marginal trust (requires multiple validations)
Fully Trusted5Fully trusted, can validate third-party signatures
Ultimate Trust6Absolute trust - required for Passbolt to function
Undefined-No validation (default for imported keys)

Passbolt requires level 6 (Ultimate Trust) in the www-data user's keyring. If the level is incorrect, the container will not boot and the healthcheck returns FAIL. The value 6 in the format FINGERPRINT:6: is what the --import-ownertrust command expects.

# Check the current trust level of the key
docker exec passbolt su -s /bin/bash -c \
  "gpg --homedir /var/lib/passbolt/.gnupg --list-ownertrust" www-data

# Expected output with Ultimate Trust configured:
# 3AA5C34371567BD2:6:
#                 ^ this "6" is Ultimate Trust

1) The initial clash: passbolt vs. vaultwarden#

Before "getting your hands dirty" with commands, you must choose the right tool for your scenario:

CriterionPassboltVaultwarden
EncryptionIndividual OpenPGP (Military Standard)AES-256 (Market Standard)
Footprint (RAM)Moderate (~512MB+)Ultra Light (<100MB)
ComplexityHigh (GPG key management)Low (Plug and Play)
ArchitecturePHP/Go/GPGRust (Bitwarden API-compatible)
Target AudienceDevOps and Teams with AuditingGeneral Use and Small Projects
RecoveryImpossible without private keyVia database backup + master key

Verdict: Passbolt is the logical choice for those requiring granular auditing and native PGP end-to-end encryption. Vaultwarden shines for its extreme resource efficiency and operational simplicity.


2) Backup before any operation#

# ---- Docker ----

# MariaDB database backup
docker exec passbolt-db mysqldump \
  -u root -p'MARIADB_ROOT_PASSWORD' passbolt \
  > /root/passbolt-db-backup-$(date +%Y%m%d-%H%M).sql

# GPG keyring backup (CRITICAL - no recovery without this)
docker cp passbolt:/var/lib/passbolt/.gnupg /root/passbolt-gnupg-backup-$(date +%Y%m%d-%H%M)

# Full data volume backup
docker run --rm \
  -v passbolt_data:/data \
  -v /root:/backup \
  alpine tar czf /backup/passbolt-volume-$(date +%Y%m%d-%H%M).tar.gz /data

# ---- Podman ----

# MariaDB database backup
podman exec passbolt-db mysqldump \
  -u root -p'MARIADB_ROOT_PASSWORD' passbolt \
  > /root/passbolt-db-backup-$(date +%Y%m%d-%H%M).sql

# GPG keyring backup
podman cp passbolt:/var/lib/passbolt/.gnupg /root/passbolt-gnupg-backup-$(date +%Y%m%d-%H%M)

# Verify backup integrity
ls -lh /root/passbolt-*
# Expected: .sql file at minimum a few KB (empty DB) to MB (with data)
# .gnupg backup: a few KB (contains public key + ownertrust metadata)

3) Docker implementation: the GPG challenge#

When spinning up Passbolt via Docker, the most common issue is a Healthcheck FAIL related to OpenPGP.

The investigation (the 500 error)#

Passbolt depends on the Linux GPG engine having the key imported into the www-data user's keyring with Ultimate Trust. If the environment variables aren't perfectly aligned, the container will abort its boot sequence.

The Corrective "Ace Up Your Sleeve":

# 1. Clear residual configurations that might ignore new variables
rm /etc/passbolt/passbolt.php
su -s /bin/bash -c "bin/cake cache clear_all" www-data

# 2. Ensure 'Ultimate Trust' (Replace YOUR_FINGERPRINT)
echo "YOUR_FINGERPRINT:6:" | su -s /bin/bash -c \
  "gpg --homedir /var/lib/passbolt/.gnupg --import-ownertrust" www-data

# 3. Synchronize the keyring with the database
su -s /bin/bash -c "bin/cake passbolt keyring_init" www-data

GPG verification after configuration#

# Verify that the key was imported correctly
docker exec passbolt su -s /bin/bash -c \
  "gpg --homedir /var/lib/passbolt/.gnupg --list-keys" www-data

# Verify Ultimate Trust is configured (must show :6:)
docker exec passbolt su -s /bin/bash -c \
  "gpg --homedir /var/lib/passbolt/.gnupg --list-ownertrust" www-data

# Check keyring permissions (should be 700 for .gnupg directory)
docker exec passbolt ls -la /var/lib/passbolt/.gnupg/

# Check ownership (should be www-data:www-data)
docker exec passbolt stat /var/lib/passbolt/.gnupg/

# Verify www-data can access the keyring (real access test)
docker exec passbolt su -s /bin/bash -c \
  "ls -la /var/lib/passbolt/.gnupg/" www-data

MariaDB verification#

# Verify the database container is running
docker ps | grep passbolt-db

# Verify connection and list databases
docker exec passbolt-db mysql \
  -u root -p'MARIADB_ROOT_PASSWORD' \
  -e "SHOW DATABASES;"

# Confirm the Passbolt schema exists
docker exec passbolt-db mysql \
  -u root -p'MARIADB_ROOT_PASSWORD' \
  -e "SHOW TABLES FROM passbolt;" | head -20

4) The evolution: orchestrating passbolt with Podman on Rocky Linux#

Moving from Docker to Podman provides a daemonless model and rootless execution, significantly enhancing VPS security.

4.1 resolving privileged port conflicts#

In rootless mode, Podman cannot bind ports below 1024. The Error: rootlessport cannot expose privileged port 80 error is common. The Solution: Use high ports (e.g., 9001) and map them correctly in your podman-compose.yaml.

services:
  passbolt:
    image: docker.io/passbolt/passbolt:latest-ce
    ports:
      - "9001:80"
    environment:
      - APP_FULL_BASE_URL=https://mydomain.com:9001
    restart: always

4.2 clearing locks and ensuring persistence#

If a container crashes abruptly, Podman might lock the metadata files (acquiring lock: file exists). The command to clear the environment is:

podman system renumber

To ensure Passbolt restarts automatically after a VPS reboot:

sudo systemctl enable podman.socket
sudo systemctl enable podman-restart.service

5) Firewall configuration on Rocky Linux#

Rocky Linux uses firewalld by default. Without opening the port, Passbolt is inaccessible from outside the server.

# Check current firewall state and active rules
sudo firewall-cmd --list-all

# Permanently open port 9001
sudo firewall-cmd --permanent --add-port=9001/tcp
sudo firewall-cmd --reload

# Confirm port was added
sudo firewall-cmd --list-ports
# Expected output: 9001/tcp

# If using iptables directly (alternative)
sudo iptables -A INPUT -p tcp --dport 9001 -j ACCEPT
sudo iptables-save > /etc/sysconfig/iptables

6) Ssl/tls and connectivity verification#

SSL certificate verification#

# Inspect certificate: validity, issuer, and subject
echo | openssl s_client -connect mydomain.com:9001 2>/dev/null \
  | openssl x509 -noout -dates -issuer -subject

# Verify active TLS negotiation
curl -vI https://mydomain.com:9001/ 2>&1 | grep -iE "ssl|tls|cipher"

# Test returned HTTP status (should be 200 or 301/302 for login redirect)
curl -sk https://mydomain.com:9001/ -o /dev/null -w "HTTP Status: %{http_code}\n"

Full connectivity check#

# Verify port availability (TCP handshake)
nc -zv mydomain.com 9001

# Verify DNS resolution
dig mydomain.com +short

# Test HTTP service response (first lines of HTML)
curl -sk https://mydomain.com:9001/ | head -10

7) Post-configuration verification (full checklist)#

# --- Container Status ---
# Docker:
docker ps | grep passbolt
docker inspect --format='{{.State.Health.Status}}' passbolt
docker logs passbolt --tail 50

# Podman:
podman ps | grep passbolt
podman inspect --format='{{.State.Health.Status}}' passbolt
podman logs passbolt --tail 50

# --- Connectivity ---
curl -I https://mydomain.com:9001/

# Expected healthcheck output: "healthy"
# Expected curl output: HTTP/1.1 200 OK or 302 Found (redirect to login)

8) Elite management: connecting Podman desktop to your VPS#

A modern analyst doesn't want to rely solely on the terminal. Managing logs and status visually via Podman Desktop (on your local Linux machine) is a total game changer.

The "pro tip": root vs user in SSH#

If you deployed the service as root on the VPS, a standard unprivileged SSH user will not see the containers due to Podman's UID isolation.

Command to establish the correct connection:

# Cleaning old keys if there's a mismatch error
ssh-keygen -R mydomain.com

# Adding the system connection to the remote host
podman system connection add ovh-vps --identity ~/.ssh/id_rsa ssh://[email protected]
podman system connection default ovh-vps

9) Container updates#

# ---- Docker ----
# 1. Run backup FIRST (see Section 2)
# 2. Pull new images
docker pull passbolt/passbolt:latest-ce
docker pull mariadb:10.11

# 3. With docker-compose (recommended - preserves volumes automatically)
docker-compose pull
docker-compose up -d
# Compose recreates containers with the new image while keeping volumes

# ---- Podman ----
podman pull docker.io/passbolt/passbolt:latest-ce
podman-compose pull
podman-compose up -d

# ---- Post-update verification ----
docker inspect --format='{{.State.Health.Status}}' passbolt
# Wait 30-60s for healthcheck to return "healthy"

10) Automated monitoring#

# Create the health check script
cat > /usr/local/bin/check-passbolt.sh << 'EOF'
#!/bin/bash
# Passbolt health monitor (Docker or Podman)
# Scheduled via cron: */5 * * * * /usr/local/bin/check-passbolt.sh

RUNTIME="${1:-docker}"   # docker or podman
CONTAINER="passbolt"
ADMIN_EMAIL="[email protected]"

HEALTH=$($RUNTIME inspect --format='{{.State.Health.Status}}' "$CONTAINER" 2>/dev/null)
RUNNING=$($RUNTIME ps --filter "name=$CONTAINER" --format "{{.Status}}" 2>/dev/null)

if [ "$HEALTH" != "healthy" ] || [ -z "$RUNNING" ]; then
    MSG="ALERT: Passbolt status - Health: ${HEALTH:-N/A} | Running: ${RUNNING:-not found}"
    echo "$MSG"
    echo "$MSG" | mail -s "🚨 Passbolt Alert $(date +%Y-%m-%d\ %H:%M)" "$ADMIN_EMAIL"
fi
EOF

chmod +x /usr/local/bin/check-passbolt.sh

# Add to cron (check every 5 minutes)
(crontab -l 2>/dev/null; echo "*/5 * * * * /usr/local/bin/check-passbolt.sh docker") | crontab -

# Verify cron was added
crontab -l | grep passbolt

11) Production checklist (SRE insights)#

Production takeaways#

Implementing Passbolt requires understanding the subtle details of GPG keyring permissions and, when transitioning to Podman, mastering namespace names and rootless ports. Having this setup operating with visual management from your desktop transforms a password manager into a world-class infrastructure tool. And with the backup, verification, and monitoring procedures documented in this guide, you have a complete runbook - not just for installation day, but for months of secure production operation.

Was this article helpful?

Leave a quick reaction to help prioritize future technical guides:

CC BY-NC

This post is licensed under CC BY-NC.

Comments

Join the discussion below.

0 comments