Status is a self-hosted status page system for communicating service availability and incidents. As a public-facing application that may contain sensitive operational information, Status requires proper security configuration to protect administrative access while maintaining public availability. This guide covers security measures for production Status deployments.
Status architecture includes these security-sensitive components:
Key security concerns include admin panel protection, API security, database protection, webhook authentication, and preventing unauthorized status modifications.
Configure firewall rules for Status:
# Status web interface (public)
ufw allow from any to any port 80 proto tcp
ufw allow from any to any port 443 proto tcp
# Admin panel (restrict to management network)
# Configure via application settings or reverse proxy
# Database
ufw allow from 127.0.0.1 to any port 5432 proto tcp # PostgreSQL
ufw allow from 127.0.0.1 to any port 3306 proto tcp # MySQL
# Block direct database access
ufw deny from any to any port 5432 proto tcp
ufw deny from any to any port 3306 proto tcp
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: status-network-policy
spec:
podSelector:
matchLabels:
app: status
ingress:
- from: [] # Allow from anywhere for public page
ports:
- protocol: TCP
port: 80
- protocol: TCP
port: 443
Configure web server binding:
# /etc/nginx/sites-available/status
server {
listen 80;
server_name status.company.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name status.company.com;
root /var/www/html/status;
ssl_certificate /etc/nginx/certs/status.crt;
ssl_certificate_key /etc/nginx/certs/status.key;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
# Admin panel - restrict by IP
location /admin {
allow 10.0.0.0/8;
allow 192.168.0.0/16;
deny all;
try_files $uri $uri/ /admin/index.php?$query_string;
}
}
Configure admin authentication:
// config.php
// Use strong password
// Enable MFA if supported
$admin_password = '${HASHED_PASSWORD}';
$session_timeout = 3600;
The public status page should be accessible without authentication, but admin areas require protection:
# /etc/nginx/sites-available/status
# Public status page - no auth
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# Admin panel - require auth
location /admin {
allow 10.0.0.0/8;
allow 192.168.0.0/16;
deny all;
auth_basic "Status Admin";
auth_basic_user_file /etc/nginx/.htpasswd;
try_files $uri $uri/ /admin/index.php?$query_string;
}
Secure API access:
// config.php
$api_enabled = true;
$api_key_required = true;
$api_key = '${API_KEY}';
Generate secure API key:
# Generate secure API key
openssl rand -hex 32
Configure user roles:
Role Permissions:
- admin: Full access including configuration
- operator: Can update status and create incidents
- viewer: Read-only access (public)
Configure HTTPS:
# /etc/nginx/sites-available/status
server {
listen 443 ssl http2;
server_name status.company.com;
root /var/www/html/status;
ssl_certificate /etc/nginx/certs/status.crt;
ssl_certificate_key /etc/nginx/certs/status.key;
ssl_trusted_certificate /etc/nginx/certs/ca-bundle.crt;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" 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 Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src * data:; font-src 'self'; connect-src 'self'" always;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
}
Require HTTPS for admin panel:
# Force HTTPS for admin
location /admin {
if ($scheme = http) {
return 301 https://$server_name$request_uri;
}
# Additional security for admin
add_header X-Robots-Tag "noindex, nofollow" always;
}
Generate and manage certificates:
# Use Let's Encrypt for public status page
certbot --nginx -d status.company.com
# Generate internal certificate for admin
openssl req -new -x509 -days 365 -nodes \
-out /etc/nginx/certs/status-admin.crt \
-keyout /etc/nginx/certs/status-admin.key \
-subj "/CN=status-admin.company.com"
Secure PHP application:
# /etc/php/8.1/fpm/php.ini
expose_php = Off
display_errors = Off
log_errors = On
error_log = /var/log/php/error.log
session.cookie_secure = 1
session.cookie_httponly = 1
session.cookie_samesite = Strict
session.use_strict_mode = 1
# Disable dangerous functions
disable_functions = exec,passthru,shell_exec,system,proc_open,popen
Secure API endpoints:
| Endpoint | Risk Level | Access Control |
|---|---|---|
GET /api/v1/status |
Low | Public |
GET /api/v1/incidents |
Low | Public |
POST /api/v1/incidents |
High | API key required |
PUT /api/v1/services |
High | API key required |
DELETE /api/v1/incidents |
Critical | API key required |
Implement API rate limiting:
# Nginx rate limiting for Status API
limit_req_zone $binary_remote_addr zone=status_api:10m rate=30r/s;
location /api/ {
limit_req zone=status_api burst=50 nodelay;
proxy_pass http://localhost:8080;
}
Secure webhook integrations:
// config.php
// Verify webhook signatures
$webhook_secret = '${WEBHOOK_SECRET}';
// Validate incoming webhooks
function verifyWebhook($signature, $payload) {
$expected = hash_hmac('sha256', $payload, '${WEBHOOK_SECRET}');
return hash_equals($expected, $signature);
}
Additional admin panel security:
# /etc/nginx/sites-available/status
location /admin {
# IP restriction
allow 10.0.0.0/8;
allow 192.168.0.0/16;
deny all;
# Authentication
auth_basic "Status Admin";
auth_basic_user_file /etc/nginx/.htpasswd;
# Rate limiting
limit_req zone=admin burst=5 nodelay;
# Security headers
add_header X-Robots-Tag "noindex, nofollow" always;
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
}
Secure Status database:
-- Create dedicated database user
CREATE USER status WITH PASSWORD '${DB_PASSWORD}';
CREATE DATABASE status OWNER status;
GRANT ALL PRIVILEGES ON DATABASE status TO status;
-- Enable SSL requirement
ALTER USER status WITH PASSWORD '${DB_PASSWORD}';
Enable data encryption:
-- Enable TDE (MySQL Enterprise)
ALTER TABLE incidents ENCRYPTION='Y';
ALTER TABLE services ENCRYPTION='Y';
Secure SQLite database:
# Set restrictive permissions
chown www-data:www-data /var/www/html/status/data/status.db
chmod 640 /var/www/html/status/data/status.db
# Use encrypted filesystem
# Mount data directory on encrypted volume
Secure sensitive configuration:
// config.php
// Use environment variables
$db_password = getenv('STATUS_DB_PASSWORD');
$api_key = getenv('STATUS_API_KEY');
$webhook_secret = getenv('STATUS_WEBHOOK_SECRET');
// Or use external secrets file
if (file_exists('/etc/status/secrets.php')) {
include '/etc/status/secrets.php';
}
Protect secrets file:
# Set restrictive permissions
chown root:www-data /etc/status/secrets.php
chmod 640 /etc/status/secrets.php
Secure Status backups:
#!/bin/bash
# Secure backup script
BACKUP_DIR="/secure/backups/status"
DATE=$(date +%Y%m%d)
# Database backup
pg_dump status > ${BACKUP_DIR}/status-db-${DATE}.sql
# Encrypt backup
gpg -e --recipient security@company.com ${BACKUP_DIR}/status-db-${DATE}.sql
# Set restrictive permissions
chmod 600 ${BACKUP_DIR}/status-db-${DATE}.sql.gpg
Enable logging:
// config.php
$log_enabled = true;
$log_file = '/var/log/status/status.log';
$log_level = 'INFO';
// Audit logging
$audit_enabled = true;
$audit_file = '/var/log/status/audit.log';
Configure web server access logging:
# /etc/nginx/sites-available/status
access_log /var/log/nginx/status_access.log combined;
error_log /var/log/nginx/status_error.log warn;
# Separate admin logging
location /admin {
access_log /var/log/nginx/status_admin_access.log combined;
}
Monitor Status for security events:
#!/bin/bash
# /usr/local/bin/check-status-security.sh
# Check for failed admin login attempts
FAILED_ADMIN=$(grep -c "401" /var/log/nginx/status_admin_access.log 2>/dev/null || echo 0)
if [ "$FAILED_ADMIN" -gt 10 ]; then
echo "CRITICAL: Multiple failed admin login attempts"
exit 2
fi
# Check for unauthorized API access
API_ERRORS=$(grep -c "403" /var/log/nginx/status_access.log 2>/dev/null || echo 0)
if [ "$API_ERRORS" -gt 50 ]; then
echo "WARNING: Possible API abuse"
exit 1
fi
Forward logs to SIEM:
# /etc/rsyslog.d/status.conf
:programname, isequal, "status" /var/log/status/syslog.log
:programname, isequal, "status" @siem.company.com:514