This guide provides security hardening for Statping-ng deployments. Statping-ng handles sensitive monitoring data, incident timelines, notification credentials, and subscriber information. Security hardening should focus on admin authentication, monitor credential safety, webhook/notification endpoint protection, and infrastructure security.
| Security Domain | Priority | Key Actions |
|---|---|---|
| Authentication | Critical | Strong passwords, MFA via reverse proxy, SSO integration |
| Container Security | High | Non-root user, capability drops, read-only filesystem |
| Network Security | High | TLS 1.3, network isolation, firewall rules |
| Data Protection | High | Encrypted backups, secret management, credential rotation |
| Monitoring & Audit | Medium | Audit logging, alerting, log aggregation |
# Generate strong admin password
openssl rand -base64 32
# Or use pwgen
pwgen -s 32 1
# Password requirements:
# - Minimum 16 characters
# - Mix of uppercase, lowercase, numbers, symbols
# - No dictionary words or common patterns
Add authentication layer before Statping-ng admin interface.
# /etc/nginx/conf.d/statping-auth.conf
location /admin {
auth_basic "Statping-ng Admin";
auth_basic_user_file /etc/nginx/.htpasswd;
proxy_pass http://127.0.0.1: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;
}
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# Create htpasswd file
htpasswd -c /etc/nginx/.htpasswd admin
# Enter strong password when prompted
# docker-compose.yml excerpt
services:
oauth2-proxy:
image: quay.io/oauth2-proxy/oauth2-proxy:latest
container_name: oauth2-proxy
restart: unless-stopped
ports:
- "4180:4180"
environment:
- OAUTH2_PROXY_PROVIDER=github
- OAUTH2_PROXY_CLIENT_ID=${GITHUB_CLIENT_ID}
- OAUTH2_PROXY_CLIENT_SECRET=${GITHUB_CLIENT_SECRET}
- OAUTH2_PROXY_COOKIE_SECRET=${COOKIE_SECRET}
- OAUTH2_PROXY_EMAIL_DOMAINS=example.com
- OAUTH2_PROXY_UPSTREAMS=http://statping-ng:8080
- OAUTH2_PROXY_HTTP_ADDRESS=0.0.0.0:4180
networks:
- frontend
location /admin {
allow 192.168.1.0/24; # Office network
allow 10.0.0.0/8; # Internal network
deny all; # Block everything else
proxy_pass http://127.0.0.1:8080;
}
# /etc/nginx/conf.d/rate-limit.conf
limit_req_zone $binary_remote_addr zone=admin_limit:10m rate=5r/m;
location /admin {
limit_req zone=admin_limit burst=3 nodelay;
proxy_pass http://127.0.0.1:8080;
}
# docker-compose.yml
services:
statping-ng:
image: adamboutcher/statping-ng:0.93.0
user: "1000:1000" # Run as non-root user
# ... other settings
services:
statping-ng:
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
cap_add:
- CHOWN
- SETUID
- SETGID
- NET_BIND_SERVICE # Only if needed for ICMP
services:
statping-ng:
read_only: true
tmpfs:
- /tmp
- /run
- /app/logs
volumes:
- statping_data:/app
β οΈ Note: Read-only filesystem may require additional tmpfs mounts for application functionality. Test thoroughly before production deployment.
services:
statping-ng:
deploy:
resources:
limits:
memory: 512M
cpus: '0.5'
reservations:
memory: 256M
cpus: '0.25'
version: '3.8'
services:
statping-ng:
image: adamboutcher/statping-ng:0.93.0
container_name: statping-ng
restart: unless-stopped
user: "1000:1000"
depends_on:
postgres:
condition: service_healthy
networks:
- frontend
- backend
volumes:
- statping_data:/app
environment:
- NAME=My Status Page
- DB_CONN=postgres
- DB_HOST=postgres
- DB_USER=statping
- DB_PASS_FILE=/run/secrets/db_password
secrets:
- db_password
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
# Security hardening
security_opt:
- no-new-privileges:true
read_only: true
tmpfs:
- /tmp:mode=1777,size=64M
- /run:mode=1777,size=32M
cap_drop:
- ALL
cap_add:
- CHOWN
- SETUID
- SETGID
deploy:
resources:
limits:
memory: 512M
cpus: '0.5'
postgres:
image: postgres:15-alpine
container_name: statping-postgres
restart: unless-stopped
user: "999:999" # postgres user
networks:
- backend
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_USER=statping
- POSTGRES_PASSWORD_FILE=/run/secrets/db_password
- POSTGRES_DB=statping
secrets:
- db_password
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
cap_add:
- CHOWN
- SETUID
- SETGID
- DAC_OVERRIDE
deploy:
resources:
limits:
memory: 1G
cpus: '1.0'
networks:
frontend:
driver: bridge
backend:
driver: bridge
internal: true # No external access
secrets:
db_password:
file: ./secrets/db_password.txt
volumes:
statping_data:
driver: local
postgres_data:
driver: local
# Create secrets directory
mkdir -p secrets
# Generate and store database password
openssl rand -base64 32 > secrets/db_password.txt
chmod 600 secrets/db_password.txt
# Set proper permissions
chown root:root secrets/db_password.txt
server {
listen 443 ssl http2;
server_name status.example.com;
# Certificate paths
ssl_certificate /etc/letsencrypt/live/status.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/status.example.com/privkey.pem;
# TLS 1.3 only (or 1.2 + 1.3 for compatibility)
ssl_protocols TLSv1.3;
# Strong cipher suites
ssl_ciphers 'TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256';
ssl_prefer_server_ciphers off;
# Session settings
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;
# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/status.example.com/chain.pem;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
# Security headers
add_header Strict-Transport-Security "max-age=63072000" always;
add_header X-Frame-Options "SAMEORIGIN" 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:; font-src 'self' data:; connect-src 'self';" always;
location / {
proxy_pass http://127.0.0.1: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;
}
}
# HTTP redirect
server {
listen 80;
server_name status.example.com;
return 301 https://$server_name$request_uri;
}
# Generate strong DH parameters (takes 5-10 minutes)
openssl dhparam -out /etc/nginx/dhparam.pem 4096
# Or use pre-generated parameters from Mozilla
curl -o /etc/nginx/dhparam.pem https://ssl-config.mozilla.org/ffdhe4096.txt
# Enable UFW
sudo ufw enable
# Set default policies
sudo ufw default deny incoming
sudo ufw default allow outgoing
# Allow SSH (rate limited)
sudo ufw limit 22/tcp
# Allow HTTP/HTTPS
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# Enable logging
sudo ufw logging on
# Check status
sudo ufw status verbose
# Enable firewalld
sudo systemctl enable firewalld
sudo systemctl start firewalld
# Set default zone
sudo firewall-cmd --set-default-zone=public
# Allow services
sudo firewall-cmd --permanent --add-service=ssh
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
# Enable rate limiting for SSH
sudo firewall-cmd --permanent --add-rich-rule='rule service name="ssh" limit value="3/m" accept'
# Reload configuration
sudo firewall-cmd --reload
# Check status
sudo firewall-cmd --list-all
networks:
# Public-facing network
frontend:
driver: bridge
ipam:
config:
- subnet: 172.28.0.0/16
# Internal database network (no external access)
backend:
driver: bridge
internal: true # Critical: prevents external access
ipam:
config:
- subnet: 172.29.0.0/16
# Verify backend network is isolated
docker network inspect statping-ng_backend | grep Internal
# Should show: "Internal": true
# Test isolation (should fail)
docker run --rm --network statping-ng_frontend alpine ping -c 3 statping-postgres
secrets:
db_password:
external: true
smtp_password:
external: true
webhook_token:
external: true
services:
statping-ng:
secrets:
- db_password
- smtp_password
- webhook_token
environment:
- DB_PASS_FILE=/run/secrets/db_password
# Create external secrets
echo "SecurePassword123!" | docker secret create db_password -
echo "SMTPPassword456!" | docker secret create smtp_password -
echo "WebhookToken789!" | docker secret create webhook_token -
# Store secrets in Vault
vault kv put secret/statping-ng \
db_password="SecurePassword123!" \
smtp_password="SMTPPassword456!" \
webhook_token="WebhookToken789!"
# Retrieve secrets
vault kv get -field=db_password secret/statping-ng
# 1. Generate new password
NEW_PASSWORD=$(openssl rand -base64 32)
# 2. Update PostgreSQL
docker exec -it statping-postgres psql -U postgres << EOF
ALTER USER statping WITH PASSWORD '$NEW_PASSWORD';
EOF
# 3. Update Docker secret
echo "$NEW_PASSWORD" | docker secret create db_password - --force
# 4. Restart Statping-ng
docker compose restart statping-ng
# 5. Verify connectivity
docker compose logs -f statping-ng
#!/bin/bash
# backup-encrypted.sh
BACKUP_DIR="/backup/statping-ng"
DATE=$(date +%Y%m%d_%H%M%S)
GPG_RECIPIENT="admin@example.com"
# Create backup
docker exec statping-postgres pg_dump -U statping -d statping | \
gzip > "$BACKUP_DIR/statping_db_$DATE.sql.gz"
# Encrypt backup
gpg --encrypt \
--recipient "$GPG_RECIPIENT" \
--trust-model always \
--output "$BACKUP_DIR/statping_db_$DATE.sql.gz.gpg" \
"$BACKUP_DIR/statping_db_$DATE.sql.gz"
# Remove unencrypted backup
shred -u "$BACKUP_DIR/statping_db_$DATE.sql.gz"
echo "Encrypted backup created: statping_db_$DATE.sql.gz.gpg"
# Decrypt backup
gpg --decrypt --output statping_db_20260216_020000.sql.gz \
statping_db_20260216_020000.sql.gz.gpg
# Restore database
gunzip -c statping_db_20260216_020000.sql.gz | \
docker exec -i statping-postgres psql -U statping -d statping
# docker-compose.yml
services:
statping-ng:
logging:
driver: "json-file"
options:
max-size: "100m"
max-file: "10"
compress: "true"
tag: "{{.ImageName}}"
services:
statping-ng:
logging:
driver: syslog
options:
syslog-address: "udp://192.168.1.100:514"
tag: "statping-ng"
syslog-facility: "local0"
services:
statping-ng:
logging:
driver: fluentd
options:
fluentd-address: "tcp://127.0.0.1:24224"
tag: "statping-ng"
Monitor these events for security incidents:
| Event Type | Description | Alert Threshold |
|---|---|---|
| Failed logins | Authentication failures | >5 per minute |
| Admin access | Admin panel access | Any from unknown IP |
| Config changes | Settings modifications | Any |
| Notification sends | Alert deliveries | Failure rate >10% |
| Database errors | Connection/query failures | Any |
# Failed login attempts
docker logs statping-ng 2>&1 | grep -i "failed\|invalid\|unauthorized"
# Admin access logs
docker logs statping-ng 2>&1 | grep -i "admin\|login"
# Database connection issues
docker logs statping-ng 2>&1 | grep -i "database\|connection\|postgres"
# /etc/sysctl.d/99-statping-security.conf
net.ipv4.ip_forward = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.all.secure_redirects = 0
net.ipv4.conf.default.secure_redirects = 0
net.ipv4.conf.all.log_martians = 1
net.ipv4.conf.default.log_martians = 1
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.icmp_ignore_bogus_error_responses = 1
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
net.ipv4.tcp_syncookies = 1
net.ipv4.conf.all.route_localnet = 0
net.ipv4.conf.default.route_localnet = 0
# Apply settings
sudo sysctl -p /etc/sysctl.d/99-statping-security.conf
# /etc/fail2ban/jail.d/statping.conf
[statping-nginx]
enabled = true
port = http,https
filter = nginx-http-auth
logpath = /var/log/nginx/*error.log
maxretry = 5
bantime = 3600
findtime = 600
[statping-admin]
enabled = true
port = http,https
filter = nginx-limit-req
logpath = /var/log/nginx/*access.log
maxretry = 3
bantime = 7200
findtime = 300
| Requirement | Implementation |
|---|---|
| Data minimization | Only collect necessary subscriber data |
| Consent management | Double opt-in for email subscriptions |
| Data access | Export subscriber data on request |
| Data deletion | Remove subscriber data on request |
| Breach notification | Document incident response procedure |
| Control | Implementation |
|---|---|
| Access control | MFA, IP restrictions, audit logging |
| Encryption | TLS 1.3, encrypted backups |
| Monitoring | Log aggregation, alerting |
| Change management | Version-controlled configurations |
| Backup/Recovery | Automated backups, tested restoration |
Any questions?
Feel free to contact us. Find all contact information on our contact page.