This guide provides security hardening recommendations for production Vigil deployments. Topics include container security, network isolation, TLS configuration, authentication, credential management, audit logging, and compliance considerations.
Latest Version: v1.28.6 (November 2025)
Vigil handles sensitive operational data including service health status, incident history, and notification credentials. A secure deployment requires defense-in-depth across multiple layers:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Security Layers β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Network Security β β
β β β’ Firewall rules (UFW/firewalld) β β
β β β’ TLS 1.3 encryption β β
β β β’ Network isolation (Docker networks) β β
β β β’ Rate limiting β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Container Security β β
β β β’ Read-only filesystem β β
β β β’ Capability dropping β β
β β β’ Non-root user β β
β β β’ Resource limits β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Application Security β β
β β β’ Token-based authentication β β
β β β’ Secure configuration management β β
β β β’ Input validation β β
β β β’ Security headers β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Data Security β β
β β β’ Encrypted backups β β
β β β’ Secret management (Docker secrets, Vault) β β
β β β’ Secure log handling β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Apply these security settings to your Docker Compose configuration:
services:
vigil:
image: valeriansaliou/vigil:v1.28.6
container_name: vigil
# Run as non-root user
user: "1000:1000"
# Read-only root filesystem
read_only: true
# Temporary filesystems for writable directories
tmpfs:
- /tmp:rw,noexec,nosuid,size=64m,mode=1777
# Drop all capabilities, add only required
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
- NET_RAW # Only if using ICMP probes
# Resource limits
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
reservations:
cpus: '0.25'
memory: 128M
# Process limits
pids_limit: 100
# Stop grace period
stop_grace_period: 30s
# Logging configuration
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
compress: "true"
| Option | Purpose | Risk if Omitted |
|---|---|---|
read_only: true |
Prevents filesystem modifications | Attackers could modify binaries or config |
tmpfs |
Provides writable temp storage | Application may fail without writable dirs |
no-new-privileges:true |
Blocks setuid/setgid exploits | Privilege escalation possible |
cap_drop: ALL |
Removes all Linux capabilities | Excessive kernel access possible |
cap_add |
Adds back only required capabilities | Application may fail without needed caps |
user |
Runs as non-root | Container compromise = root access |
deploy.resources |
Limits CPU/memory usage | DoS attacks could exhaust resources |
pids_limit |
Prevents fork bombs | Process exhaustion attacks possible |
Configure /etc/docker/daemon.json:
{
"userns-remap": "default",
"live-restore": true,
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"no-new-privileges": true,
"userland-proxy": false,
"iptables": true,
"icc": false
}
Settings explained:
userns-remap: Maps container users to non-root host userslive-restore: Keeps containers running during Docker daemon restartsno-new-privileges: Default security setting for all containersuserland-proxy: false: Uses iptables directly (better performance)icc: false: Disables inter-container communication by defaultRestart Docker after changes:
sudo systemctl restart docker
Create /etc/apparmor.d/vigil:
#include <tunables/global>
profile vigil flags=(attach_disconnected,mediate_deleted) {
#include <abstractions/base>
network inet tcp,
network inet udp,
network inet icmp,
/bin/vigil mr,
/etc/vigil.cfg r,
/var/lib/vigil/ rw,
/var/lib/vigil/** rw,
deny /tmp/** w,
capability net_bind_service,
capability net_raw,
deny @{PROC}/* w,
deny /sys/* w,
}
Apply to Docker Compose:
security_opt:
- apparmor=vigil
For RHEL/CentOS/Fedora with SELinux:
security_opt:
- label=type:vigil_t
- label=level:s0:c100,c200
# Enable UFW with default deny
sudo ufw default deny incoming
sudo ufw default allow outgoing
# Allow SSH (use non-standard port if possible)
sudo ufw allow 22/tcp comment "SSH"
# Allow HTTP/HTTPS for status page
sudo ufw allow 80/tcp comment "HTTP redirect"
sudo ufw allow 443/tcp comment "HTTPS status page"
# Deny direct access to Vigil port (only via Nginx)
sudo ufw deny 8080/tcp
# Enable UFW
sudo ufw enable
# Verify rules
sudo ufw status verbose
# 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
# Block Vigil direct access
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" port port="8080" protocol="tcp" reject'
# Reload firewall
sudo firewall-cmd --reload
# Verify configuration
sudo firewall-cmd --list-all
Use Docker networks to isolate Vigil from other containers:
networks:
vigil-internal:
driver: bridge
ipam:
config:
- subnet: 172.28.0.0/16
gateway: 172.28.0.1
internal: false # Set true for complete isolation
# Separate network for database (if applicable)
vigil-db:
driver: bridge
internal: true # No external access
Never bind Vigil directly to public interface. Always use localhost or internal Docker network:
# β
Secure: Internal network only
expose:
- "8080"
# β
Secure: Localhost only
ports:
- "127.0.0.1:8080:8080"
# β Insecure: Public exposure
ports:
- "8080:8080"
Create /opt/vigil/nginx/nginx.conf:
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
events {
worker_connections 1024;
use epoll;
multi_accept on;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# TLS 1.3 only configuration
ssl_protocols TLSv1.3;
ssl_ciphers TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
# Security headers (applied globally)
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" 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: https:; font-src 'self' data:; connect-src 'self';" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
# Rate limiting
limit_req_zone $binary_remote_addr zone=vigil_limit:10m rate=10r/s;
upstream vigil_backend {
server vigil:8080;
keepalive 32;
}
# HTTP to HTTPS redirect
server {
listen 80;
listen [::]:80;
server_name status.example.com;
location /.well-known/acme-challenge/ {
root /usr/share/nginx/html;
}
location / {
return 301 https://$server_name$request_uri;
}
}
# HTTPS server
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name status.example.com;
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
ssl_trusted_certificate /etc/nginx/ssl/fullchain.pem;
resolver 1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout 5s;
# Rate limiting
limit_req zone=vigil_limit burst=20 nodelay;
location / {
proxy_pass http://vigil_backend;
proxy_http_version 1.1;
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;
proxy_set_header Connection "";
proxy_buffering off;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Deny access to sensitive paths
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
}
}
# Install Certbot
sudo apt-get install -y certbot
# Obtain certificate
sudo certbot certonly --standalone \
-d status.example.com \
--email admin@example.com \
--agree-tos \
--non-interactive
# Auto-renewal cron
echo "0 3 * * * certbot renew --quiet && docker compose restart nginx" | sudo crontab -
# Set proper permissions
sudo chmod 644 /opt/vigil/nginx/ssl/fullchain.pem
sudo chmod 600 /opt/vigil/nginx/ssl/privkey.pem
sudo chown root:root /opt/vigil/nginx/ssl/*.pem
# Verify certificate
openssl x509 -in /opt/vigil/nginx/ssl/fullchain.pem -text -noout
# Check expiry
openssl x509 -enddate -noout -in /opt/vigil/nginx/ssl/fullchain.pem
Vigil uses token-based authentication for Manager and Reporter APIs.
Generate secure tokens:
# 32-character hex token (minimum recommended)
openssl rand -hex 32
# 64-character for high-security environments
openssl rand -hex 64
Store tokens securely:
# Use Docker secrets (Swarm mode)
echo "your-token" | docker secret create vigil_manager_token -
# Or use environment file with restricted permissions
chmod 600 /opt/vigil/.env
chown root:root /opt/vigil/.env
Manager API endpoints (require manager_token):
POST /manager/announce - Publish announcementsPOST /manager/node/:id/force - Force node statusPUT /manager/config - Update configurationReporter API endpoints (require reporter_token):
POST /reporter/push - Submit health reportsPOST /reporter/system - Submit system metricsPublic endpoints (no authentication):
GET / - Status pageGET /status/report - JSON status reportGET /health - Health checkRestrict API access by IP:
# In Nginx configuration
location /manager/ {
allow 10.0.0.0/8; # Internal network
allow 192.168.1.100; # Specific admin IP
deny all;
proxy_pass http://vigil_backend;
}
location /reporter/ {
allow 10.0.0.0/8; # Internal services
deny all;
proxy_pass http://vigil_backend;
}
services:
vigil:
image: valeriansaliou/vigil:v1.28.6
secrets:
- vigil_manager_token
- vigil_reporter_token
- smtp_password
secrets:
vigil_manager_token:
external: true
vigil_reporter_token:
external: true
smtp_password:
external: true
Create secrets:
echo "your-manager-token" | docker secret create vigil_manager_token -
echo "your-reporter-token" | docker secret create vigil_reporter_token -
echo "your-smtp-password" | docker secret create smtp_password -
Reference in config:
[server]
manager_token = "${SECRET:vigil_manager_token}"
reporter_token = "${SECRET:vigil_reporter_token}"
[notify.email]
smtp_password = "${SECRET:smtp_password}"
apiVersion: v1
kind: Secret
metadata:
name: vigil-secrets
type: Opaque
stringData:
VIGIL_MANAGER_TOKEN: "your-manager-token"
VIGIL_REPORTER_TOKEN: "your-reporter-token"
SMTP_PASSWORD: "your-smtp-password"
Mount in pod:
envFrom:
- secretRef:
name: vigil-secrets
# Store secrets in Vault
vault kv put secret/vigil \
manager_token="your-manager-token" \
reporter_token="your-reporter-token" \
smtp_password="your-smtp-password"
Retrieve in deployment script:
export VIGIL_MANAGER_TOKEN=$(vault kv get -field=manager_token secret/vigil)
export VIGIL_REPORTER_TOKEN=$(vault kv get -field=reporter_token secret/vigil)
The Nginx configuration above includes these headers:
| Header | Value | Purpose |
|---|---|---|
Strict-Transport-Security |
max-age=63072000; includeSubDomains; preload |
Force HTTPS for 2 years |
X-Frame-Options |
SAMEORIGIN |
Prevent clickjacking |
X-Content-Type-Options |
nosniff |
Prevent MIME sniffing |
X-XSS-Protection |
1; mode=block |
Enable XSS filter |
Referrer-Policy |
strict-origin-when-cross-origin |
Control referrer leakage |
Content-Security-Policy |
Custom policy | Prevent XSS, injection attacks |
Permissions-Policy |
Feature restrictions | Disable unnecessary browser features |
curl -I https://status.example.com
# Expected output:
# strict-transport-security: max-age=63072000; includeSubDomains; preload
# x-frame-options: SAMEORIGIN
# x-content-type-options: nosniff
# x-xss-protection: 1; mode=block
# content-security-policy: default-src 'self'; ...
# Define rate limit zone (10 requests/second per IP)
limit_req_zone $binary_remote_addr zone=vigil_limit:10m rate=10r/s;
# Apply in server block
location / {
limit_req zone=vigil_limit burst=20 nodelay;
proxy_pass http://vigil_backend;
}
# Stricter limits for API endpoints
location /manager/ {
limit_req zone=vigil_limit burst=5 nodelay;
proxy_pass http://vigil_backend;
}
# Limit concurrent connections per IP
limit_conn_zone $binary_remote_addr zone=vigil_conn:10m;
server {
limit_conn vigil_conn 10; # Max 10 concurrent connections per IP
}
# Block suspicious patterns
if ($http_user_agent ~* (curl|wget|scanner|nikto)) {
return 403;
}
# Limit request body size
client_max_body_size 1M;
# Timeout settings
client_body_timeout 10s;
client_header_timeout 10s;
send_timeout 10s;
[server]
log_level = "info" # Use "warn" or "error" in production
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
compress: "true"
logging:
driver: syslog
options:
syslog-address: "udp://127.0.0.1:514"
tag: "vigil"
syslog-facility: "local0"
Create /etc/logrotate.d/vigil:
/opt/vigil/logs/*.log {
daily
rotate 30
compress
delaycompress
missingok
notifempty
create 0640 vigil vigil
sharedscripts
postrotate
docker compose -f /opt/vigil/docker-compose.yml kill -s HUP nginx
endscript
}
Monitor for these events:
# Search for failed auth in logs
docker compose logs vigil | grep -i "unauthorized\|forbidden\|invalid token"
# Monitor container restarts
docker inspect vigil --format='{{.State.RestartCount}}'
#!/bin/bash
# /opt/vigil/backup.sh
BACKUP_DIR="/backup/vigil"
DATE=$(date +%Y%m%d_%H%M%S)
GPG_RECIPIENT="admin@example.com"
mkdir -p "$BACKUP_DIR"
# Create backup
tar -czf "$BACKUP_DIR/config-$DATE.tar.gz" \
/opt/vigil/config \
/opt/vigil/data
# Encrypt backup
gpg --encrypt --recipient "$GPG_RECIPIENT" \
--output "$BACKUP_DIR/config-$DATE.tar.gz.gpg" \
"$BACKUP_DIR/config-$DATE.tar.gz"
# Securely delete unencrypted backup
shred -u "$BACKUP_DIR/config-$DATE.tar.gz"
# Retention: keep 30 days
find "$BACKUP_DIR" -name "*.tar.gz.gpg" -mtime +30 -delete
# Restrict backup directory access
sudo chmod 700 /backup/vigil
sudo chown root:root /backup/vigil
# Restrict GPG key access
chmod 600 ~/.gnupg/*.gpg
# Test backup integrity
gpg --decrypt /backup/vigil/config-20260216.tar.gz.gpg | tar -tzf -
# Test restore procedure
gpg --decrypt /backup/vigil/config-20260216.tar.gz.gpg | \
tar -xzf - -C /tmp/vigil-restore-test
Create /etc/sysctl.d/99-vigil-security.conf:
# Network security
net.ipv4.tcp_syncookies = 1
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 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.log_martians = 1
net.ipv4.conf.default.log_martians = 1
# IPv6 security
net.ipv6.conf.all.accept_redirects = 0
net.ipv6.conf.default.accept_redirects = 0
net.ipv6.conf.all.accept_ra = 0
net.ipv6.conf.default.accept_ra = 0
# Apply settings
sudo sysctl -p /etc/sysctl.d/99-vigil-security.conf
Install and configure Fail2ban:
sudo apt-get install -y fail2ban
Create /etc/fail2ban/jail.d/vigil.conf:
[vigil-nginx]
enabled = true
port = http,https
filter = nginx-limit-req
logpath = /opt/vigil/logs/nginx/error.log
maxretry = 5
bantime = 3600
findtime = 600
# Debian/Ubuntu
sudo apt-get install -y unattended-upgrades apt-listchanges
# Enable automatic updates
sudo dpkg-reconfigure --priority=low unattended-upgrades
# Vigil configuration
chmod 640 /opt/vigil/config/config.cfg
chown root:vigil /opt/vigil/config/config.cfg
# Environment file
chmod 600 /opt/vigil/.env
chown root:root /opt/vigil/.env
# SSL certificates
chmod 644 /opt/vigil/nginx/ssl/fullchain.pem
chmod 600 /opt/vigil/nginx/ssl/privkey.pem
chown root:root /opt/vigil/nginx/ssl/*.pem
Monitor these endpoints externally:
| Endpoint | Purpose | Expected Response |
|---|---|---|
GET /health |
Basic health check | 200 OK with βhealthyβ |
GET /status/report |
Full status report | 200 OK with JSON |
GET / |
Status page | 200 OK with HTML |
Configure external monitoring (Uptime Kuma, Pingdom, etc.):
# Uptime Kuma configuration example
monitor:
type: http
url: https://status.example.com/health
interval: 60
timeout: 10
expectedStatus: 200
notification:
- slack-critical
- email-ops
1. Detect:
2. Triage:
# Check container status
docker compose ps
# Check recent logs
docker compose logs --tail=100 vigil
# Check resource usage
docker stats vigil
3. Respond:
# Restart if needed
docker compose restart vigil
# Rollback if recent deployment
docker compose down
docker tag valeriansaliou/vigil:v1.28.6 valeriansaliou/vigil:latest
docker compose up -d
4. Document:
Any questions?
Feel free to contact us. Find all contact information on our contact page.