┌─────────────────────────────────────────────────────┐
│ Docker Container │
│ │
│ ┌─────────┐ │
│ │ tini │ (PID 1 - Init system) │
│ └────┬────┘ │
│ │ │
│ ├──> entrypoint.sh (Setup logic) │
│ │ │
│ └──> ┌──────────────────────────────────┐ │
│ │ Supervisor (PID 2) │ │
│ │ │ │
│ │ ┌────────────────────────────┐ │ │
│ │ │ corosync-qnetd (Priority 10)│ │ │
│ │ │ - Port 5403 │ │ │
│ │ │ - User: coroqnetd │ │ │
│ │ │ - Autorestart: true │ │ │
│ │ └────────────────────────────┘ │ │
│ │ │ │
│ │ ┌────────────────────────────┐ │ │
│ │ │ sshd (Priority 20) [*] │ │ │
│ │ │ - Port 22 │ │ │
│ │ │ - Setup mode only │ │ │
│ │ │ - Autorestart: true │ │ │
│ │ └────────────────────────────┘ │ │
│ │ │ │
│ │ ┌────────────────────────────┐ │ │
│ │ │ auto_lock.sh (Priority 30) [*]│ │
│ │ │ - Monitors cluster │ │ │
│ │ │ - Setup mode only │ │ │
│ │ │ - Autorestart: false │ │ │
│ │ └────────────────────────────┘ │ │
│ └──────────────────────────────────┘ │
│ │
│ [*] Only loaded when /data/SSH_DISABLED absent │
└─────────────────────────────────────────────────────┘
tinistarts as PID 1entrypoint.shdetects no/data/SSH_DISABLED- Generates/restores SSH host keys
- Sets root password from
ROOT_PASSWORD - Enables
sshd.confandautolock.conf - Launches Supervisor with 3 processes
- Waits for Proxmox connection...
- Proxmox connects via SSH (port 22)
- Exchanges certificates with QNetd
- Cluster appears in
corosync-qnetd-tool -l auto_lock.shdetects cluster (threshold check)- Waits
AUTO_LOCK_WAITseconds - Creates
/data/SSH_DISABLEDlock file - Stops SSH via supervisorctl
- Removes
sshd.conf
tinistarts as PID 1entrypoint.shdetects/data/SSH_DISABLED- Skips SSH setup entirely
- Only enables
qnetd.conf - Launches Supervisor with 1 process (qnetd only)
| Layer | Protection | Details |
|---|---|---|
| Temporal | Auto-disable SSH | Active max 2-5 min |
| Persistent | Lock file | Survives reboots |
| Process | Minimal attack surface | Only QNetd runs after setup |
| Network | Port isolation | 5403 only after setup |
| Init | Proper signal handling | Tini prevents zombie processes |
persistent_data/
├── SSH_DISABLED # Lock file (empty, presence-based)
├── ssh/ # SSH host keys
│ ├── ssh_host_rsa_key
│ ├── ssh_host_rsa_key.pub
│ ├── ssh_host_ecdsa_key
│ ├── ssh_host_ecdsa_key.pub
│ ├── ssh_host_ed25519_key
│ └── ssh_host_ed25519_key.pub
└── corosync/ # QNetd database
├── nssdb/
│ ├── cert9.db
│ ├── key4.db
│ └── pkcs11.txt
└── qnetd-cluster-*.crt
DETECTION_COUNT = 0
THRESHOLD = 3 # Configurable via AUTO_LOCK_THRESHOLD
while True:
if cluster_detected():
DETECTION_COUNT += 1
if DETECTION_COUNT >= THRESHOLD:
wait(WAIT_SECONDS)
if cluster_still_detected():
create_lock()
stop_ssh()
exit(0)
else:
# Cluster lost during wait
DETECTION_COUNT = 0
else:
# Reset counter on disconnection
if DETECTION_COUNT > 0:
DETECTION_COUNT = 0
sleep(10)Why threshold-based detection?
This prevents false positives during:
- Initial certificate exchange (transient cluster states)
- Network hiccups
- Corosync service restarts on Proxmox nodes
The default threshold of 3 detections = 30 seconds of stable cluster presence (3 × 10s check interval).
SIGTERM/SIGINT → tini → Supervisor → Graceful shutdown
├─> corosync-qnetd (SIGTERM)
├─> sshd (SIGTERM)
└─> auto_lock.sh (SIGTERM)
Tini ensures:
- Proper signal forwarding to child processes
- Zombie process reaping
- Clean container stops
| Port | Protocol | Service | Availability |
|---|---|---|---|
| 22 | TCP | SSH | Setup mode only (auto-disabled) |
| 5403 | TCP | QNetd | Always (persistent) |
/
├── data/ # Persistent volume mount
│ ├── SSH_DISABLED # Lock file
│ ├── ssh/ # SSH keys backup
│ └── corosync/ # QNetd data (symlinked)
│
├── etc/
│ ├── corosync/
│ │ └── qnetd/ # Symlink → /data/corosync
│ │
│ ├── ssh/ # SSH host keys (restored from /data)
│ │
│ └── supervisor/
│ ├── supervisord.conf
│ └── conf.d/
│ ├── qnetd.conf # Always loaded
│ ├── sshd.conf.disabled # Template
│ ├── sshd.conf # Created in setup mode
│ ├── autolock.conf.disabled # Template
│ └── autolock.conf # Created in setup mode
│
├── auto_lock.sh # Auto-lock monitor script
├── entrypoint.sh # Container init script
├── healthcheck.sh # Docker healthcheck
│
└── var/
└── run/
├── corosync-qnetd/ # QNetd runtime
└── sshd/ # SSH runtime (setup only)
| Variable | Default | Description |
|---|---|---|
ROOT_PASSWORD |
- | SSH root password (setup only) |
AUTO_LOCK_WAIT |
120 | Seconds to wait before locking after detection |
AUTO_LOCK_THRESHOLD |
3 | Number of successive cluster detections required |
TZ |
Europe/Paris | Timezone for logs |
# Every 30 seconds:
1. Check if QNetd responds
├─> FAIL → Container unhealthy
└─> OK → Continue
2. If locked mode: warn if no cluster
└─> WARN (but don't fail)
3. Check if qnetd process exists
├─> FAIL → Container unhealthy
└─> OK → Container healthyThe healthcheck allows Docker to automatically restart the container if QNetd crashes.
# Container remains secure - SSH already stopped
# Lock file only checked on container startrm persistent_data/SSH_DISABLED
docker restart proxmox-qdevice
# Container enters setup mode again# QNetd continues running (no dependency on cluster)
# Nodes reconnect automatically
# No intervention needed# entrypoint.sh detects lock file
# Skips SSH entirely
# Only starts QNetd
# Full security maintainedAll services log to Docker stdout/stderr:
[INFO] entrypoint.sh logs (container startup)
[AUTOLOCK] auto_lock.sh logs (cluster monitoring)
[qnetd] corosync-qnetd logs (voting activity)
[sshd] SSH connection logs (setup only)
View logs:
docker logs proxmox-qdevice -f
docker logs proxmox-qdevice | grep AUTOLOCK| Aspect | Setup Mode | Secure Mode |
|---|---|---|
| SSH Port 22 | ✅ Open | 🔒 Closed |
| QNetd Port 5403 | ✅ Open | ✅ Open |
| Running processes | 3 | 1 |
| Attack surface | Temporary (2-5 min) | Minimal |
| Auto-lock monitor | ✅ Active | ❌ Disabled |
| Lock file | ❌ Absent | ✅ Present |