trilium/docs/Security/Security-Best-Practices-Comprehensive.md
2025-08-21 15:55:44 +00:00

61 KiB
Vendored

Trilium Security Best Practices - Comprehensive Guide

This comprehensive guide provides detailed security recommendations for deploying, configuring, and maintaining a secure Trilium installation across all environments from personal desktop use to enterprise deployments.

Table of Contents

  1. Deployment Security
  2. Infrastructure Hardening
  3. Application Security Configuration
  4. Data Protection
  5. Operational Security
  6. Compliance and Governance
  7. Incident Response
  8. Security Assessment

Deployment Security

Production Environment Setup

HTTPS Configuration

Mandatory for Production: Always use HTTPS in production environments to protect data in transit.

# Nginx configuration for Trilium
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name trilium.yourdomain.com;
    
    # SSL Configuration
    ssl_certificate /etc/letsencrypt/live/trilium.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/trilium.yourdomain.com/privkey.pem;
    ssl_session_timeout 1d;
    ssl_session_cache shared:MozTLS:10m;
    ssl_session_tickets off;
    
    # Modern SSL configuration
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;
    
    # HSTS (63072000 seconds = 2 years)
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
    
    # Security headers
    add_header X-Frame-Options DENY 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' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; media-src 'self'; object-src 'none'; child-src 'none'; frame-src 'none'; worker-src 'none'; frame-ancestors 'none'; form-action 'self'; upgrade-insecure-requests;" always;
    
    # OCSP stapling
    ssl_stapling on;
    ssl_stapling_verify on;
    
    # Proxy configuration
    location / {
        proxy_pass http://localhost:8080;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        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_cache_bypass $http_upgrade;
        
        # Security headers for proxy
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Server $host;
        
        # Timeouts
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }
    
    # WebSocket support
    location /api/sync {
        proxy_pass http://localhost:8080;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        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 to HTTPS redirect
server {
    listen 80;
    listen [::]:80;
    server_name trilium.yourdomain.com;
    return 301 https://$host$request_uri;
}

Docker Security Configuration

# Secure Dockerfile for Trilium
FROM node:18-alpine AS builder

# Create non-root user
RUN addgroup -g 1001 -S trilium && \
    adduser -S trilium -u 1001

# Security updates
RUN apk update && apk upgrade && \
    apk add --no-cache dumb-init

# Application setup
WORKDIR /opt/trilium
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force

COPY . .
RUN chown -R trilium:trilium /opt/trilium

# Runtime image
FROM node:18-alpine

# Install security updates
RUN apk update && apk upgrade && \
    apk add --no-cache dumb-init

# Create user and directories
RUN addgroup -g 1001 -S trilium && \
    adduser -S trilium -u 1001 && \
    mkdir -p /opt/trilium /home/trilium/data && \
    chown trilium:trilium /opt/trilium /home/trilium/data

# Copy application
COPY --from=builder --chown=trilium:trilium /opt/trilium /opt/trilium

# Switch to non-root user
USER trilium

# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
    CMD node docker_healthcheck.js

WORKDIR /opt/trilium
EXPOSE 8080

# Use dumb-init for proper signal handling
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "src/www.js"]
# Docker Compose with security hardening
version: '3.8'

services:
  trilium:
    image: triliumnext/trilium:latest
    container_name: trilium
    restart: unless-stopped
    
    # Security configurations
    read_only: true
    user: "1001:1001"
    cap_drop:
      - ALL
    cap_add:
      - CHOWN
      - DAC_OVERRIDE
      - SETGID
      - SETUID
    security_opt:
      - no-new-privileges:true
      - apparmor:docker-default
    
    # Resource limits
    deploy:
      resources:
        limits:
          cpus: '2.0'
          memory: 2G
        reservations:
          cpus: '0.5'
          memory: 512M
    
    # Tmpfs for temporary files
    tmpfs:
      - /tmp:noexec,nosuid,size=100m
      - /var/tmp:noexec,nosuid,size=50m
    
    environment:
      - TRILIUM_DATA_DIR=/home/trilium/data
      - NODE_ENV=production
      - TRILIUM_PORT=8080
    
    volumes:
      - trilium_data:/home/trilium/data:rw
      - /etc/localtime:/etc/localtime:ro
    
    ports:
      - "127.0.0.1:8080:8080"
    
    # Health check
    healthcheck:
      test: ["CMD", "node", "docker_healthcheck.js"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

volumes:
  trilium_data:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: /opt/trilium/data

Network Security

Firewall Configuration

#!/bin/bash
# Comprehensive firewall setup for Trilium server

# Flush existing rules
iptables -F
iptables -X
iptables -t nat -F
iptables -t nat -X
iptables -t mangle -F
iptables -t mangle -X

# Set default policies
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT

# Allow loopback
iptables -A INPUT -i lo -j ACCEPT
iptables -A OUTPUT -o lo -j ACCEPT

# Allow established connections
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

# SSH access (change port as needed)
iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT

# HTTPS only for Trilium (block direct HTTP access)
iptables -A INPUT -p tcp --dport 443 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT

# Rate limiting for SSH
iptables -A INPUT -p tcp --dport 22 -m recent --set --name SSH
iptables -A INPUT -p tcp --dport 22 -m recent --update --seconds 60 --hitcount 4 --name SSH -j DROP

# Rate limiting for HTTPS
iptables -A INPUT -p tcp --dport 443 -m recent --set --name HTTPS
iptables -A INPUT -p tcp --dport 443 -m recent --update --seconds 1 --hitcount 20 --name HTTPS -j DROP

# ICMP (ping) - limited
iptables -A INPUT -p icmp --icmp-type echo-request -m limit --limit 1/second -j ACCEPT

# Block common attack patterns
iptables -A INPUT -p tcp --tcp-flags ALL NONE -j DROP
iptables -A INPUT -p tcp --tcp-flags ALL ALL -j DROP
iptables -A INPUT -p tcp --tcp-flags ALL FIN,URG,PSH -j DROP
iptables -A INPUT -p tcp --tcp-flags ALL SYN,RST,ACK,FIN,URG -j DROP

# Log dropped packets
iptables -A INPUT -j LOG --log-prefix "IPTABLES-DROPPED: " --log-level 4
iptables -A INPUT -j DROP

# Save rules (Ubuntu/Debian)
iptables-save > /etc/iptables/rules.v4

# Install UFW for easier management
ufw --force reset
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp
ufw allow 443/tcp
ufw limit ssh
ufw --force enable

VPN Access Configuration

# WireGuard VPN setup for secure remote access
# Server configuration
cat > /etc/wireguard/wg0.conf << EOF
[Interface]
PrivateKey = $(wg genkey)
Address = 10.100.0.1/24
ListenPort = 51820
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE

# Client configuration template
[Peer]
PublicKey = CLIENT_PUBLIC_KEY
AllowedIPs = 10.100.0.2/32
EOF

# Enable IP forwarding
echo 'net.ipv4.ip_forward = 1' >> /etc/sysctl.conf
sysctl -p

# Start WireGuard
systemctl enable wg-quick@wg0
systemctl start wg-quick@wg0

# Firewall rule for VPN
ufw allow 51820/udp

Infrastructure Hardening

Operating System Security

System Hardening

#!/bin/bash
# Comprehensive OS hardening script

# Update system
apt update && apt upgrade -y

# Install security tools
apt install -y fail2ban unattended-upgrades apt-listchanges

# Automatic security updates
cat > /etc/apt/apt.conf.d/20auto-upgrades << EOF
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Download-Upgradeable-Packages "1";
APT::Periodic::AutocleanInterval "7";
APT::Periodic::Unattended-Upgrade "1";
EOF

# Configure unattended upgrades for security only
sed -i 's/\/\/\s*"\${distro_id}:\${distro_codename}-security";/"\${distro_id}:\${distro_codename}-security";/' /etc/apt/apt.conf.d/50unattended-upgrades

# Secure kernel parameters
cat > /etc/sysctl.d/99-security.conf << EOF
# IP Spoofing protection
net.ipv4.conf.default.rp_filter = 1
net.ipv4.conf.all.rp_filter = 1

# Ignore ICMP redirects
net.ipv4.conf.all.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv6.conf.default.accept_redirects = 0

# Ignore send redirects
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0

# Disable source packet routing
net.ipv4.conf.all.accept_source_route = 0
net.ipv6.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
net.ipv6.conf.default.accept_source_route = 0

# Log Martians
net.ipv4.conf.all.log_martians = 1
net.ipv4.conf.default.log_martians = 1

# Ignore ICMP ping requests
net.ipv4.icmp_echo_ignore_all = 0

# Ignore Directed pings
net.ipv4.icmp_echo_ignore_broadcasts = 1

# Disable IPv6 if not used
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1

# Enable TCP SYN Cookies
net.ipv4.tcp_syncookies = 1

# Disable core dumps
fs.suid_dumpable = 0

# Hide kernel pointers
kernel.kptr_restrict = 2

# Restrict dmesg
kernel.dmesg_restrict = 1

# Restrict perf events
kernel.perf_event_paranoid = 2
EOF

sysctl -p /etc/sysctl.d/99-security.conf

# Secure shared memory
echo "tmpfs /run/shm tmpfs defaults,noexec,nosuid 0 0" >> /etc/fstab

# Configure fail2ban
cat > /etc/fail2ban/jail.local << EOF
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 3
backend = systemd

[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 3

[nginx-http-auth]
enabled = true
filter = nginx-http-auth
port = http,https
logpath = /var/log/nginx/error.log

[nginx-limit-req]
enabled = true
filter = nginx-limit-req
port = http,https
logpath = /var/log/nginx/error.log
maxretry = 10
EOF

systemctl enable fail2ban
systemctl start fail2ban

# Setup log rotation
cat > /etc/logrotate.d/trilium << EOF
/opt/trilium/logs/*.log {
    daily
    missingok
    rotate 52
    compress
    delaycompress
    notifempty
    create 644 trilium trilium
    postrotate
        systemctl reload trilium
    endscript
}
EOF

# Secure file permissions
chmod 600 /etc/ssh/sshd_config
chmod 700 /root
chmod 644 /etc/passwd
chmod 644 /etc/group
chmod 600 /etc/shadow
chmod 600 /etc/gshadow

# Remove unnecessary packages
apt autoremove -y
apt autoclean

SSH Hardening

# SSH configuration hardening
cat > /etc/ssh/sshd_config << EOF
# Protocol and encryption
Protocol 2
HostKey /etc/ssh/ssh_host_rsa_key
HostKey /etc/ssh/ssh_host_ecdsa_key
HostKey /etc/ssh/ssh_host_ed25519_key

# Key exchange, cipher, and MAC algorithms
KexAlgorithms curve25519-sha256@libssh.org,ecdh-sha2-nistp521,ecdh-sha2-nistp384,ecdh-sha2-nistp256,diffie-hellman-group-exchange-sha256
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr
MACs hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha2-256,hmac-sha2-512

# Authentication
LoginGraceTime 30
PermitRootLogin no
StrictModes yes
MaxAuthTries 3
MaxSessions 2
PubkeyAuthentication yes
PasswordAuthentication no
PermitEmptyPasswords no
ChallengeResponseAuthentication no
UsePAM yes

# Network settings
Port 22
AddressFamily inet
ListenAddress 0.0.0.0
TCPKeepAlive yes
ClientAliveInterval 300
ClientAliveCountMax 2

# Logging
SyslogFacility AUTH
LogLevel VERBOSE

# File transfer
AllowAgentForwarding no
AllowTcpForwarding no
GatewayPorts no
X11Forwarding no
PrintMotd no
PrintLastLog yes
UsePrivilegeSeparation sandbox

# User restrictions
DenyUsers root
AllowUsers trilium

# Miscellaneous
Compression delayed
UseDNS no
PermitUserEnvironment no
Banner /etc/ssh/banner
EOF

# Create SSH banner
cat > /etc/ssh/banner << EOF
***************************************************************************
                            AUTHORIZED ACCESS ONLY
***************************************************************************
This system is for authorized users only. All activities are monitored
and recorded. Unauthorized access is strictly prohibited and will be
prosecuted to the full extent of the law.
***************************************************************************
EOF

# Restart SSH service
systemctl restart sshd

Database Security

SQLite Security Configuration

#!/bin/bash
# Secure SQLite database configuration

# Set proper file permissions
TRILIUM_DATA_DIR="/opt/trilium/data"
DATABASE_FILE="$TRILIUM_DATA_DIR/document.db"

# Create trilium user if not exists
if ! id -u trilium >/dev/null 2>&1; then
    useradd -r -s /bin/false -M trilium
fi

# Set ownership and permissions
chown -R trilium:trilium "$TRILIUM_DATA_DIR"
chmod 700 "$TRILIUM_DATA_DIR"
chmod 600 "$DATABASE_FILE"
chmod 600 "$TRILIUM_DATA_DIR"/*.txt
chmod 600 "$TRILIUM_DATA_DIR"/*.ini

# Configure database security settings
sqlite3 "$DATABASE_FILE" << EOF
-- Enable WAL mode for better concurrency
PRAGMA journal_mode=WAL;

-- Secure delete
PRAGMA secure_delete=ON;

-- Auto vacuum for better space management
PRAGMA auto_vacuum=INCREMENTAL;

-- Foreign key constraints
PRAGMA foreign_keys=ON;

-- Integrity check
PRAGMA integrity_check;
EOF

# Set up database backup with encryption
cat > /usr/local/bin/trilium-backup.sh << 'EOF'
#!/bin/bash
BACKUP_DIR="/opt/trilium/backups"
DATE=$(date +%Y%m%d_%H%M%S)
GPG_RECIPIENT="trilium-backup@yourdomain.com"

mkdir -p "$BACKUP_DIR"

# Create backup
sqlite3 /opt/trilium/data/document.db ".backup /tmp/trilium_backup_$DATE.db"

# Encrypt backup
tar czf - -C /opt/trilium/data . | gpg --trust-model always --encrypt -r "$GPG_RECIPIENT" > "$BACKUP_DIR/trilium_backup_$DATE.tar.gz.gpg"

# Clean up
rm -f "/tmp/trilium_backup_$DATE.db"

# Remove old backups (keep 30 days)
find "$BACKUP_DIR" -name "*.gpg" -mtime +30 -delete

# Set permissions
chown trilium:trilium "$BACKUP_DIR"/*.gpg
chmod 600 "$BACKUP_DIR"/*.gpg
EOF

chmod +x /usr/local/bin/trilium-backup.sh

# Add to crontab for daily backups
echo "0 2 * * * /usr/local/bin/trilium-backup.sh" | crontab -u trilium -

Monitoring and Logging

Comprehensive Logging Setup

#!/bin/bash
# Setup comprehensive logging for Trilium

# Install log monitoring tools
apt install -y rsyslog logwatch logrotate auditd

# Configure auditd for file system monitoring
cat > /etc/audit/rules.d/trilium.rules << EOF
# Monitor Trilium directory
-w /opt/trilium/data/ -p wa -k trilium_data
-w /opt/trilium/data/document.db -p wa -k trilium_database
-w /etc/systemd/system/trilium.service -p wa -k trilium_config

# Monitor authentication
-w /var/log/auth.log -p wa -k auth_log
-w /etc/passwd -p wa -k passwd_changes
-w /etc/group -p wa -k group_changes
-w /etc/shadow -p wa -k shadow_changes

# Monitor network configuration
-w /etc/hosts -p wa -k network_config
-w /etc/network/ -p wa -k network_config

# Monitor sudoers
-w /etc/sudoers -p wa -k sudoers_changes
-w /etc/sudoers.d/ -p wa -k sudoers_changes
EOF

# Configure rsyslog for Trilium
cat > /etc/rsyslog.d/trilium.conf << EOF
# Trilium application logs
if $programname == 'trilium' then /var/log/trilium/trilium.log
& stop

# Trilium security events
:msg, contains, "trilium" /var/log/trilium/security.log
& stop

# Rotate logs
\$WorkDirectory /var/spool/rsyslog
\$ActionFileDefaultTemplate RSYSLOG_TraditionalFileFormat
\$RepeatedMsgReduction on
\$FileOwner trilium
\$FileGroup adm
\$FileCreateMode 0640
\$DirCreateMode 0755
\$Umask 0022
EOF

# Create log directories
mkdir -p /var/log/trilium
chown trilium:adm /var/log/trilium
chmod 750 /var/log/trilium

# Configure log rotation for Trilium
cat > /etc/logrotate.d/trilium << EOF
/var/log/trilium/*.log {
    daily
    missingok
    rotate 90
    compress
    delaycompress
    notifempty
    create 640 trilium adm
    sharedscripts
    postrotate
        systemctl reload rsyslog
    endscript
}
EOF

# Setup log monitoring script
cat > /usr/local/bin/trilium-log-monitor.sh << 'EOF'
#!/bin/bash
# Monitor Trilium logs for security events

LOG_FILE="/var/log/trilium/security.log"
ALERT_EMAIL="admin@yourdomain.com"

# Check for failed login attempts
FAILED_LOGINS=$(grep -c "Failed login" "$LOG_FILE" 2>/dev/null || echo 0)
if [ "$FAILED_LOGINS" -gt 5 ]; then
    echo "Alert: $FAILED_LOGINS failed login attempts detected in Trilium" | \
    mail -s "Trilium Security Alert" "$ALERT_EMAIL"
fi

# Check for CSRF violations
CSRF_VIOLATIONS=$(grep -c "CSRF violation" "$LOG_FILE" 2>/dev/null || echo 0)
if [ "$CSRF_VIOLATIONS" -gt 0 ]; then
    echo "Alert: $CSRF_VIOLATIONS CSRF violations detected in Trilium" | \
    mail -s "Trilium Security Alert" "$ALERT_EMAIL"
fi

# Check for unusual access patterns
UNUSUAL_ACCESS=$(grep -c "Unusual access" "$LOG_FILE" 2>/dev/null || echo 0)
if [ "$UNUSUAL_ACCESS" -gt 0 ]; then
    echo "Alert: $UNUSUAL_ACCESS unusual access patterns detected in Trilium" | \
    mail -s "Trilium Security Alert" "$ALERT_EMAIL"
fi
EOF

chmod +x /usr/local/bin/trilium-log-monitor.sh

# Add to crontab for monitoring
echo "*/15 * * * * /usr/local/bin/trilium-log-monitor.sh" | crontab -u root -

# Restart services
systemctl restart auditd
systemctl restart rsyslog

Application Security Configuration

Trilium Configuration Hardening

Security Configuration

# config.ini - Production security configuration
[General]
# Disable authentication only for localhost/VPN access
noAuthentication=false

# Enable additional security logging
securityLogging=true

# Set minimum password length
passwordMinLength=12

# Session security
[Session]
# Shorter session timeout for security
cookieMaxAge=3600

# Secure cookie settings (HTTPS required)
cookieSecure=true
cookieSameSite=strict
cookieHttpOnly=true

# Session secret rotation
sessionSecretRotationDays=30

# Database settings
[Database]
# Enable database encryption at rest
encryptionEnabled=true

# Regular integrity checks
integrityCheckInterval=86400

# Backup encryption
backupEncryption=true

# API Security
[ETAPI]
# Rate limiting
rateLimitRequests=100
rateLimitWindow=60

# Token expiration
tokenExpirationDays=90

# Require API token for all operations
requireTokenForRead=true

# Protected Session
[ProtectedSession]
# Shorter timeout for protected sessions
timeout=600

# Warning before timeout
timeoutWarning=true

# Auto-logout on browser close
autoLogoutOnClose=true

# Content Security Policy
[CSP]
# Strict CSP for XSS protection
enabled=true
scriptSrc='self' 'unsafe-inline'
styleSrc='self' 'unsafe-inline'
imgSrc='self' data: https:
connectSrc='self'
fontSrc='self' data:
objectSrc='none'
mediaSrc='self'
frameSrc='none'

Environment Variables for Security

# Trilium security environment variables
export TRILIUM_ENV=production
export TRILIUM_PORT=8080
export TRILIUM_HOST=127.0.0.1

# Security settings
export TRILIUM_PASSWORD_MIN_LENGTH=12
export TRILIUM_SESSION_TIMEOUT=3600
export TRILIUM_PROTECTED_SESSION_TIMEOUT=600
export TRILIUM_MFA_REQUIRED=true

# Database security
export TRILIUM_DB_ENCRYPTION=true
export TRILIUM_BACKUP_ENCRYPTION=true

# Logging
export TRILIUM_LOG_LEVEL=info
export TRILIUM_SECURITY_LOGGING=true
export TRILIUM_AUDIT_LOGGING=true

# API security
export TRILIUM_ETAPI_RATE_LIMIT=100
export TRILIUM_ETAPI_TOKEN_EXPIRATION=90

# Content Security Policy
export TRILIUM_CSP_ENABLED=true
export TRILIUM_HSTS_ENABLED=true

Security Headers Configuration

// Enhanced security headers for Trilium
const securityHeaders = {
    // Prevent clickjacking
    'X-Frame-Options': 'DENY',
    
    // Prevent MIME sniffing
    'X-Content-Type-Options': 'nosniff',
    
    // XSS protection
    'X-XSS-Protection': '1; mode=block',
    
    // Referrer policy
    'Referrer-Policy': 'strict-origin-when-cross-origin',
    
    // HSTS (only over HTTPS)
    'Strict-Transport-Security': 'max-age=31536000; includeSubDomains; preload',
    
    // Permissions policy
    'Permissions-Policy': 'geolocation=(), microphone=(), camera=(), payment=(), usb=(), magnetometer=(), gyroscope=(), speaker=()',
    
    // Content Security Policy
    'Content-Security-Policy': [
        "default-src 'self'",
        "script-src 'self' 'unsafe-inline' 'unsafe-eval'",
        "style-src 'self' 'unsafe-inline'",
        "img-src 'self' data: https:",
        "font-src 'self' data:",
        "connect-src 'self'",
        "media-src 'self'",
        "object-src 'none'",
        "child-src 'none'",
        "frame-src 'none'",
        "worker-src 'none'",
        "frame-ancestors 'none'",
        "form-action 'self'",
        "upgrade-insecure-requests",
        "block-all-mixed-content"
    ].join('; ')
};

Data Protection

Backup Security Strategy

Encrypted Backup Implementation

#!/bin/bash
# Comprehensive encrypted backup strategy

BACKUP_CONFIG_FILE="/etc/trilium/backup.conf"
GPG_KEY_ID="trilium-backup@yourdomain.com"
BACKUP_BASE_DIR="/opt/trilium/backups"
RETENTION_DAYS=90

# Load configuration
source "$BACKUP_CONFIG_FILE"

backup_trilium() {
    local timestamp=$(date +%Y%m%d_%H%M%S)
    local backup_name="trilium_backup_$timestamp"
    local temp_dir="/tmp/$backup_name"
    
    echo "Starting Trilium backup: $backup_name"
    
    # Create temporary directory
    mkdir -p "$temp_dir"
    
    # Stop Trilium for consistent backup
    systemctl stop trilium
    
    # Copy data files
    cp -r /opt/trilium/data/* "$temp_dir/"
    
    # Copy configuration
    cp /opt/trilium/config.ini "$temp_dir/"
    
    # Database integrity check
    sqlite3 "$temp_dir/document.db" "PRAGMA integrity_check;" > "$temp_dir/integrity_check.log"
    
    # Create backup manifest
    cat > "$temp_dir/backup_manifest.txt" << EOF
Backup Date: $(date -Iseconds)
Trilium Version: $(cat /opt/trilium/package.json | jq -r .version)
Database Size: $(stat -c%s "$temp_dir/document.db" | numfmt --to=iec)
Files Included:
$(find "$temp_dir" -type f -exec basename {} \;)
Checksum:
$(find "$temp_dir" -type f -exec sha256sum {} \;)
EOF
    
    # Create encrypted archive
    tar czf - -C /tmp "$backup_name" | \
    gpg --trust-model always --cipher-algo AES256 --compress-algo 2 \
        --symmetric --passphrase-file /etc/trilium/backup_passphrase \
        --output "$BACKUP_BASE_DIR/${backup_name}.tar.gz.gpg"
    
    # Verify backup
    if gpg --quiet --batch --passphrase-file /etc/trilium/backup_passphrase \
           --decrypt "$BACKUP_BASE_DIR/${backup_name}.tar.gz.gpg" | tar tz > /dev/null; then
        echo "Backup verification successful"
    else
        echo "Backup verification failed!" >&2
        exit 1
    fi
    
    # Clean up
    rm -rf "$temp_dir"
    
    # Start Trilium
    systemctl start trilium
    
    # Set permissions
    chown trilium:trilium "$BACKUP_BASE_DIR/${backup_name}.tar.gz.gpg"
    chmod 600 "$BACKUP_BASE_DIR/${backup_name}.tar.gz.gpg"
    
    echo "Backup completed: $BACKUP_BASE_DIR/${backup_name}.tar.gz.gpg"
}

restore_trilium() {
    local backup_file="$1"
    
    if [ ! -f "$backup_file" ]; then
        echo "Backup file not found: $backup_file" >&2
        exit 1
    fi
    
    echo "Restoring from backup: $backup_file"
    
    # Stop Trilium
    systemctl stop trilium
    
    # Backup current data
    local current_backup="/opt/trilium/data.backup.$(date +%Y%m%d_%H%M%S)"
    mv /opt/trilium/data "$current_backup"
    
    # Extract backup
    mkdir -p /opt/trilium/data
    gpg --quiet --batch --passphrase-file /etc/trilium/backup_passphrase \
        --decrypt "$backup_file" | tar xzf - -C /opt/trilium/data --strip-components=1
    
    # Verify database integrity
    if sqlite3 /opt/trilium/data/document.db "PRAGMA integrity_check;" | grep -q "ok"; then
        echo "Database integrity check passed"
    else
        echo "Database integrity check failed!" >&2
        echo "Restoring original data..."
        rm -rf /opt/trilium/data
        mv "$current_backup" /opt/trilium/data
        exit 1
    fi
    
    # Set permissions
    chown -R trilium:trilium /opt/trilium/data
    chmod 700 /opt/trilium/data
    chmod 600 /opt/trilium/data/*
    
    # Start Trilium
    systemctl start trilium
    
    echo "Restore completed successfully"
}

cleanup_old_backups() {
    echo "Cleaning up backups older than $RETENTION_DAYS days"
    find "$BACKUP_BASE_DIR" -name "*.tar.gz.gpg" -mtime +$RETENTION_DAYS -delete
}

# Main execution
case "$1" in
    backup)
        backup_trilium
        cleanup_old_backups
        ;;
    restore)
        restore_trilium "$2"
        ;;
    cleanup)
        cleanup_old_backups
        ;;
    *)
        echo "Usage: $0 {backup|restore <file>|cleanup}"
        exit 1
        ;;
esac

Backup Configuration

# /etc/trilium/backup.conf
BACKUP_FREQUENCY="daily"
BACKUP_TIME="02:00"
BACKUP_RETENTION_DAYS=90
BACKUP_LOCATION="/opt/trilium/backups"
REMOTE_BACKUP_ENABLED=true
REMOTE_BACKUP_HOST="backup.yourdomain.com"
REMOTE_BACKUP_USER="trilium-backup"
VERIFICATION_ENABLED=true
COMPRESSION_LEVEL=6

Remote Backup Synchronization

#!/bin/bash
# Secure remote backup synchronization

REMOTE_HOST="backup.yourdomain.com"
REMOTE_USER="trilium-backup"
REMOTE_PATH="/backups/trilium"
LOCAL_BACKUP_DIR="/opt/trilium/backups"

# Sync backups to remote location
sync_remote_backups() {
    echo "Syncing backups to remote location..."
    
    rsync -avz --progress --delete \
        -e "ssh -i /home/trilium/.ssh/backup_key -o StrictHostKeyChecking=yes" \
        "$LOCAL_BACKUP_DIR/" \
        "$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/"
    
    if [ $? -eq 0 ]; then
        echo "Remote backup sync completed successfully"
    else
        echo "Remote backup sync failed" >&2
        exit 1
    fi
}

# Verify remote backups
verify_remote_backups() {
    echo "Verifying remote backups..."
    
    ssh -i /home/trilium/.ssh/backup_key \
        "$REMOTE_USER@$REMOTE_HOST" \
        "find $REMOTE_PATH -name '*.tar.gz.gpg' -mtime -1 | wc -l"
}

# Setup SSH key for backup user
setup_backup_ssh() {
    # Generate SSH key for backup user
    sudo -u trilium ssh-keygen -t ed25519 -f /home/trilium/.ssh/backup_key -N ""
    
    # Set proper permissions
    chown trilium:trilium /home/trilium/.ssh/backup_key*
    chmod 600 /home/trilium/.ssh/backup_key
    chmod 644 /home/trilium/.ssh/backup_key.pub
    
    echo "SSH key generated. Add the following public key to the remote backup server:"
    cat /home/trilium/.ssh/backup_key.pub
}

case "$1" in
    sync)
        sync_remote_backups
        ;;
    verify)
        verify_remote_backups
        ;;
    setup-ssh)
        setup_backup_ssh
        ;;
    *)
        echo "Usage: $0 {sync|verify|setup-ssh}"
        exit 1
        ;;
esac

Data Loss Prevention

File Integrity Monitoring

#!/bin/bash
# File integrity monitoring for Trilium

TRILIUM_DATA_DIR="/opt/trilium/data"
CHECKSUM_FILE="/var/lib/trilium/checksums.db"
ALERT_EMAIL="admin@yourdomain.com"

# Initialize checksum database
init_checksums() {
    echo "Initializing file integrity monitoring..."
    
    sqlite3 "$CHECKSUM_FILE" << EOF
CREATE TABLE IF NOT EXISTS file_checksums (
    path TEXT PRIMARY KEY,
    checksum TEXT NOT NULL,
    size INTEGER NOT NULL,
    mtime INTEGER NOT NULL,
    last_check INTEGER NOT NULL
);
EOF
    
    update_checksums
}

# Update checksums for all files
update_checksums() {
    echo "Updating file checksums..."
    
    while IFS= read -r -d '' file; do
        if [ -f "$file" ]; then
            local checksum=$(sha256sum "$file" | cut -d' ' -f1)
            local size=$(stat -c%s "$file")
            local mtime=$(stat -c%Y "$file")
            local now=$(date +%s)
            
            sqlite3 "$CHECKSUM_FILE" << EOF
INSERT OR REPLACE INTO file_checksums 
(path, checksum, size, mtime, last_check)
VALUES ('$file', '$checksum', $size, $mtime, $now);
EOF
        fi
    done < <(find "$TRILIUM_DATA_DIR" -type f -print0)
}

# Check file integrity
check_integrity() {
    echo "Checking file integrity..."
    
    local violations=0
    
    while IFS='|' read -r path stored_checksum stored_size stored_mtime; do
        if [ -f "$path" ]; then
            local current_checksum=$(sha256sum "$path" | cut -d' ' -f1)
            local current_size=$(stat -c%s "$path")
            local current_mtime=$(stat -c%Y "$path")
            
            if [ "$current_checksum" != "$stored_checksum" ] || 
               [ "$current_size" != "$stored_size" ] ||
               [ "$current_mtime" != "$stored_mtime" ]; then
                
                echo "INTEGRITY VIOLATION: $path"
                echo "  Expected checksum: $stored_checksum"
                echo "  Current checksum:  $current_checksum"
                echo "  Expected size: $stored_size"
                echo "  Current size:  $current_size"
                
                violations=$((violations + 1))
            fi
        else
            echo "FILE MISSING: $path"
            violations=$((violations + 1))
        fi
    done < <(sqlite3 "$CHECKSUM_FILE" "SELECT path, checksum, size, mtime FROM file_checksums;" | tr ' ' '|')
    
    if [ $violations -gt 0 ]; then
        echo "ALERT: $violations integrity violations detected!" >&2
        echo "File integrity violations detected in Trilium data directory" | \
        mail -s "Trilium Integrity Alert" "$ALERT_EMAIL"
        return 1
    else
        echo "File integrity check passed"
        return 0
    fi
}

# Generate integrity report
generate_report() {
    local report_file="/var/log/trilium/integrity_report_$(date +%Y%m%d_%H%M%S).txt"
    
    cat > "$report_file" << EOF
Trilium File Integrity Report
Generated: $(date -Iseconds)

File Count: $(sqlite3 "$CHECKSUM_FILE" "SELECT COUNT(*) FROM file_checksums;")
Last Check: $(date -d "@$(sqlite3 "$CHECKSUM_FILE" "SELECT MAX(last_check) FROM file_checksums;")")

Files by Type:
$(sqlite3 "$CHECKSUM_FILE" "
SELECT 
    CASE 
        WHEN path LIKE '%.db' THEN 'Database'
        WHEN path LIKE '%.log' THEN 'Log'
        WHEN path LIKE '%.ini' THEN 'Config'
        WHEN path LIKE '%.txt' THEN 'Text'
        ELSE 'Other'
    END as type,
    COUNT(*) as count
FROM file_checksums 
GROUP BY type;
")

Total Data Size: $(sqlite3 "$CHECKSUM_FILE" "SELECT SUM(size) FROM file_checksums;" | numfmt --to=iec)
EOF
    
    echo "Integrity report generated: $report_file"
}

case "$1" in
    init)
        init_checksums
        ;;
    update)
        update_checksums
        ;;
    check)
        check_integrity
        ;;
    report)
        generate_report
        ;;
    *)
        echo "Usage: $0 {init|update|check|report}"
        exit 1
        ;;
esac

Operational Security

Security Monitoring and Alerting

Real-time Security Monitoring

#!/usr/bin/env python3
"""
Trilium Security Monitoring System
Real-time monitoring and alerting for security events
"""

import sqlite3
import time
import smtplib
import json
import logging
from datetime import datetime, timedelta
from email.mime.text import MimeText
from email.mime.multipart import MimeMultipart
from typing import Dict, List, Any
import configparser

class TriliumSecurityMonitor:
    def __init__(self, config_file: str):
        self.config = configparser.ConfigParser()
        self.config.read(config_file)
        
        self.db_path = self.config.get('database', 'path')
        self.log_path = self.config.get('logging', 'path')
        self.alert_email = self.config.get('alerts', 'email')
        self.smtp_server = self.config.get('smtp', 'server')
        self.smtp_port = self.config.getint('smtp', 'port')
        self.smtp_user = self.config.get('smtp', 'user')
        self.smtp_password = self.config.get('smtp', 'password')
        
        # Thresholds
        self.failed_login_threshold = self.config.getint('thresholds', 'failed_logins')
        self.time_window_minutes = self.config.getint('thresholds', 'time_window')
        
        # Setup logging
        logging.basicConfig(
            filename=self.log_path,
            level=logging.INFO,
            format='%(asctime)s - %(levelname)s - %(message)s'
        )
        self.logger = logging.getLogger(__name__)
    
    def connect_db(self) -> sqlite3.Connection:
        """Connect to Trilium database"""
        return sqlite3.connect(self.db_path)
    
    def check_failed_logins(self) -> Dict[str, Any]:
        """Check for excessive failed login attempts"""
        with self.connect_db() as conn:
            cursor = conn.cursor()
            
            # Check for failed logins in the last time window
            time_threshold = datetime.now() - timedelta(minutes=self.time_window_minutes)
            
            cursor.execute("""
                SELECT data, COUNT(*) as count
                FROM security_events 
                WHERE type = 'login_failure' 
                AND timestamp > ?
                GROUP BY JSON_EXTRACT(data, '$.ip')
                HAVING count > ?
            """, (time_threshold.isoformat(), self.failed_login_threshold))
            
            results = cursor.fetchall()
            
            if results:
                return {
                    'alert_type': 'failed_logins',
                    'severity': 'HIGH',
                    'count': len(results),
                    'details': results
                }
        
        return None
    
    def check_csrf_violations(self) -> Dict[str, Any]:
        """Check for CSRF violations"""
        with self.connect_db() as conn:
            cursor = conn.cursor()
            
            time_threshold = datetime.now() - timedelta(minutes=self.time_window_minutes)
            
            cursor.execute("""
                SELECT COUNT(*) as count, data
                FROM security_events 
                WHERE type = 'csrf_violation' 
                AND timestamp > ?
            """, (time_threshold.isoformat(),))
            
            result = cursor.fetchone()
            
            if result and result[0] > 0:
                return {
                    'alert_type': 'csrf_violations',
                    'severity': 'HIGH',
                    'count': result[0],
                    'details': result[1]
                }
        
        return None
    
    def check_database_integrity(self) -> Dict[str, Any]:
        """Check database integrity"""
        try:
            with self.connect_db() as conn:
                cursor = conn.cursor()
                cursor.execute("PRAGMA integrity_check;")
                result = cursor.fetchone()
                
                if result[0] != 'ok':
                    return {
                        'alert_type': 'database_integrity',
                        'severity': 'CRITICAL',
                        'result': result[0]
                    }
        except Exception as e:
            return {
                'alert_type': 'database_error',
                'severity': 'CRITICAL',
                'error': str(e)
            }
        
        return None
    
    def check_unusual_activity(self) -> Dict[str, Any]:
        """Check for unusual activity patterns"""
        with self.connect_db() as conn:
            cursor = conn.cursor()
            
            # Check for sessions from new IPs
            cursor.execute("""
                SELECT JSON_EXTRACT(data, '$.ip') as ip, COUNT(*) as count
                FROM security_events 
                WHERE type = 'session_create'
                AND timestamp > datetime('now', '-1 hour')
                GROUP BY ip
                HAVING count > 10
            """)
            
            unusual_ips = cursor.fetchall()
            
            if unusual_ips:
                return {
                    'alert_type': 'unusual_activity',
                    'severity': 'MEDIUM',
                    'ips': unusual_ips
                }
        
        return None
    
    def send_alert(self, alert: Dict[str, Any]):
        """Send security alert via email"""
        try:
            msg = MimeMultipart()
            msg['From'] = self.smtp_user
            msg['To'] = self.alert_email
            msg['Subject'] = f"Trilium Security Alert - {alert['alert_type'].upper()}"
            
            body = f"""
Security Alert Detected

Alert Type: {alert['alert_type']}
Severity: {alert['severity']}
Time: {datetime.now().isoformat()}

Details:
{json.dumps(alert, indent=2)}

Please investigate immediately.
            """
            
            msg.attach(MimeText(body, 'plain'))
            
            server = smtplib.SMTP(self.smtp_server, self.smtp_port)
            server.starttls()
            server.login(self.smtp_user, self.smtp_password)
            server.send_message(msg)
            server.quit()
            
            self.logger.info(f"Alert sent: {alert['alert_type']}")
            
        except Exception as e:
            self.logger.error(f"Failed to send alert: {e}")
    
    def run_monitoring_cycle(self):
        """Run one monitoring cycle"""
        self.logger.info("Starting monitoring cycle")
        
        checks = [
            self.check_failed_logins,
            self.check_csrf_violations,
            self.check_database_integrity,
            self.check_unusual_activity
        ]
        
        for check in checks:
            try:
                alert = check()
                if alert:
                    self.logger.warning(f"Security alert: {alert['alert_type']}")
                    self.send_alert(alert)
            except Exception as e:
                self.logger.error(f"Error in check {check.__name__}: {e}")
        
        self.logger.info("Monitoring cycle completed")
    
    def run_continuous(self, interval_seconds: int = 300):
        """Run continuous monitoring"""
        self.logger.info("Starting continuous security monitoring")
        
        while True:
            try:
                self.run_monitoring_cycle()
                time.sleep(interval_seconds)
            except KeyboardInterrupt:
                self.logger.info("Monitoring stopped by user")
                break
            except Exception as e:
                self.logger.error(f"Unexpected error: {e}")
                time.sleep(60)  # Wait before retrying

if __name__ == "__main__":
    import sys
    
    if len(sys.argv) != 2:
        print("Usage: python3 trilium_security_monitor.py <config_file>")
        sys.exit(1)
    
    monitor = TriliumSecurityMonitor(sys.argv[1])
    monitor.run_continuous()

Configuration for Security Monitor

# /etc/trilium/security_monitor.conf
[database]
path = /opt/trilium/data/document.db

[logging]
path = /var/log/trilium/security_monitor.log

[alerts]
email = admin@yourdomain.com

[smtp]
server = smtp.gmail.com
port = 587
user = trilium-alerts@yourdomain.com
password = your-app-password

[thresholds]
failed_logins = 5
time_window = 15

Incident Response Procedures

Automated Incident Response

#!/bin/bash
# Automated incident response for Trilium

INCIDENT_LOG="/var/log/trilium/incidents.log"
BACKUP_DIR="/opt/trilium/incident_backups"
ADMIN_EMAIL="admin@yourdomain.com"

log_incident() {
    local incident_type="$1"
    local severity="$2"
    local description="$3"
    
    local timestamp=$(date -Iseconds)
    local incident_id="INC-$(date +%Y%m%d-%H%M%S)"
    
    echo "[$timestamp] [$incident_id] [$severity] [$incident_type] $description" >> "$INCIDENT_LOG"
    
    # Send immediate notification for high severity
    if [ "$severity" = "HIGH" ] || [ "$severity" = "CRITICAL" ]; then
        echo "INCIDENT ALERT: $incident_id - $incident_type - $description" | \
        mail -s "CRITICAL: Trilium Security Incident $incident_id" "$ADMIN_EMAIL"
    fi
}

isolate_system() {
    log_incident "ISOLATION" "HIGH" "System isolation initiated"
    
    # Block all incoming connections except admin SSH
    iptables -P INPUT DROP
    iptables -A INPUT -i lo -j ACCEPT
    iptables -A INPUT -p tcp --dport 22 -s ADMIN_IP_RANGE -j ACCEPT
    iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
    
    # Stop Trilium service
    systemctl stop trilium
    
    # Create forensic backup
    create_forensic_backup
}

create_forensic_backup() {
    local backup_name="forensic_$(date +%Y%m%d_%H%M%S)"
    local backup_path="$BACKUP_DIR/$backup_name"
    
    mkdir -p "$backup_path"
    
    # Copy database
    cp /opt/trilium/data/document.db "$backup_path/"
    
    # Copy logs
    cp -r /var/log/trilium "$backup_path/"
    
    # Copy configuration
    cp /opt/trilium/config.ini "$backup_path/"
    
    # System information
    uname -a > "$backup_path/system_info.txt"
    ps aux > "$backup_path/processes.txt"
    netstat -tulpn > "$backup_path/network.txt"
    
    # Calculate checksums
    find "$backup_path" -type f -exec sha256sum {} \; > "$backup_path/checksums.txt"
    
    # Encrypt backup
    tar czf - -C "$BACKUP_DIR" "$backup_name" | \
    gpg --symmetric --cipher-algo AES256 --output "$backup_path.tar.gz.gpg"
    
    rm -rf "$backup_path"
    
    log_incident "FORENSICS" "MEDIUM" "Forensic backup created: $backup_path.tar.gz.gpg"
}

investigate_failed_logins() {
    local threshold="$1"
    local time_window="$2"
    
    # Check for failed login patterns
    local failed_attempts=$(sqlite3 /opt/trilium/data/document.db "
        SELECT COUNT(*) FROM security_events 
        WHERE type = 'login_failure' 
        AND timestamp > datetime('now', '-$time_window minutes')
    ")
    
    if [ "$failed_attempts" -gt "$threshold" ]; then
        log_incident "BRUTE_FORCE" "HIGH" "Excessive failed login attempts detected: $failed_attempts"
        
        # Extract attacking IPs
        sqlite3 /opt/trilium/data/document.db "
            SELECT JSON_EXTRACT(data, '$.ip') as ip, COUNT(*) as count
            FROM security_events 
            WHERE type = 'login_failure'
            AND timestamp > datetime('now', '-$time_window minutes')
            GROUP BY ip
            ORDER BY count DESC
        " > /tmp/attack_ips.txt
        
        # Block attacking IPs
        while read ip count; do
            if [ "$count" -gt 3 ]; then
                iptables -A INPUT -s "$ip" -j DROP
                log_incident "IP_BLOCK" "MEDIUM" "Blocked attacking IP: $ip ($count attempts)"
            fi
        done < /tmp/attack_ips.txt
        
        rm /tmp/attack_ips.txt
    fi
}

check_data_integrity() {
    # Database integrity check
    local integrity_result=$(sqlite3 /opt/trilium/data/document.db "PRAGMA integrity_check;")
    
    if [ "$integrity_result" != "ok" ]; then
        log_incident "DATA_CORRUPTION" "CRITICAL" "Database integrity check failed: $integrity_result"
        isolate_system
        return 1
    fi
    
    # File system integrity check
    if ! /usr/local/bin/trilium-integrity-check.sh check; then
        log_incident "FILE_CORRUPTION" "HIGH" "File integrity check failed"
        create_forensic_backup
    fi
}

restore_from_backup() {
    local backup_file="$1"
    
    if [ ! -f "$backup_file" ]; then
        log_incident "RESTORE_ERROR" "HIGH" "Backup file not found: $backup_file"
        return 1
    fi
    
    log_incident "RESTORE_START" "HIGH" "Starting restore from backup: $backup_file"
    
    # Stop services
    systemctl stop trilium
    
    # Backup current state
    mv /opt/trilium/data /opt/trilium/data.corrupt.$(date +%Y%m%d_%H%M%S)
    
    # Restore from backup
    if /usr/local/bin/trilium-backup.sh restore "$backup_file"; then
        log_incident "RESTORE_SUCCESS" "MEDIUM" "Restore completed successfully"
        systemctl start trilium
    else
        log_incident "RESTORE_FAILED" "CRITICAL" "Restore failed"
        return 1
    fi
}

generate_incident_report() {
    local start_time="$1"
    local end_time="$2"
    local report_file="/tmp/incident_report_$(date +%Y%m%d_%H%M%S).txt"
    
    cat > "$report_file" << EOF
TRILIUM SECURITY INCIDENT REPORT
Generated: $(date -Iseconds)
Period: $start_time to $end_time

INCIDENTS:
$(grep -A 5 -B 5 "\[$start_time\].*\[$end_time\]" "$INCIDENT_LOG")

SECURITY EVENTS:
$(sqlite3 /opt/trilium/data/document.db "
    SELECT timestamp, type, data 
    FROM security_events 
    WHERE timestamp BETWEEN '$start_time' AND '$end_time'
    ORDER BY timestamp DESC
")

SYSTEM STATUS:
Service Status: $(systemctl is-active trilium)
Database Integrity: $(sqlite3 /opt/trilium/data/document.db "PRAGMA integrity_check;")
Disk Usage: $(df -h /opt/trilium)
Memory Usage: $(free -h)

NETWORK STATUS:
$(netstat -tulpn | grep :8080)

EOF
    
    echo "Incident report generated: $report_file"
    
    # Email report
    mail -s "Trilium Incident Report" -a "$report_file" "$ADMIN_EMAIL" < "$report_file"
}

# Main incident response dispatcher
case "$1" in
    isolate)
        isolate_system
        ;;
    investigate-logins)
        investigate_failed_logins "${2:-5}" "${3:-15}"
        ;;
    check-integrity)
        check_data_integrity
        ;;
    restore)
        restore_from_backup "$2"
        ;;
    report)
        generate_incident_report "$2" "$3"
        ;;
    log)
        log_incident "$2" "$3" "$4"
        ;;
    *)
        echo "Usage: $0 {isolate|investigate-logins [threshold] [window]|check-integrity|restore <backup>|report <start> <end>|log <type> <severity> <description>}"
        exit 1
        ;;
esac

Security Assessment

Automated Security Assessment

#!/bin/bash
# Comprehensive Trilium security assessment

ASSESSMENT_DATE=$(date +%Y%m%d_%H%M%S)
REPORT_FILE="/tmp/trilium_security_assessment_$ASSESSMENT_DATE.txt"
SCORE=0
MAX_SCORE=0

# Colors for output
RED='\033[0;31m'
YELLOW='\033[1;33m'
GREEN='\033[0;32m'
NC='\033[0m' # No Color

print_status() {
    local status="$1"
    local message="$2"
    local points="$3"
    
    case "$status" in
        "PASS")
            echo -e "${GREEN}[PASS]${NC} $message (+$points points)"
            SCORE=$((SCORE + points))
            ;;
        "FAIL")
            echo -e "${RED}[FAIL]${NC} $message"
            ;;
        "WARN")
            echo -e "${YELLOW}[WARN]${NC} $message"
            ;;
    esac
    
    MAX_SCORE=$((MAX_SCORE + points))
}

check_https_configuration() {
    echo "=== HTTPS Configuration ==="
    
    # Check if HTTPS is enabled
    if curl -s -I https://localhost:8080 2>/dev/null | grep -q "HTTP/"; then
        print_status "PASS" "HTTPS is configured" 20
    else
        print_status "FAIL" "HTTPS is not configured" 20
    fi
    
    # Check SSL certificate validity
    if openssl s_client -connect localhost:443 -servername localhost </dev/null 2>/dev/null | grep -q "Verification: OK"; then
        print_status "PASS" "SSL certificate is valid" 10
    else
        print_status "WARN" "SSL certificate validation failed" 10
    fi
    
    # Check for secure ciphers
    local ciphers=$(nmap --script ssl-enum-ciphers -p 443 localhost 2>/dev/null | grep -c "TLS")
    if [ "$ciphers" -gt 0 ]; then
        print_status "PASS" "Secure TLS ciphers available" 10
    else
        print_status "WARN" "TLS cipher check inconclusive" 10
    fi
    
    echo
}

check_authentication_security() {
    echo "=== Authentication Security ==="
    
    # Check if password is set
    local password_set=$(sqlite3 /opt/trilium/data/document.db "SELECT value FROM options WHERE name = 'passwordVerificationHash';" 2>/dev/null)
    if [ -n "$password_set" ]; then
        print_status "PASS" "Password authentication is configured" 15
    else
        print_status "FAIL" "Password authentication is not configured" 15
    fi
    
    # Check MFA status
    local mfa_enabled=$(sqlite3 /opt/trilium/data/document.db "SELECT value FROM options WHERE name = 'mfaEnabled';" 2>/dev/null)
    if [ "$mfa_enabled" = "true" ]; then
        print_status "PASS" "Multi-factor authentication is enabled" 20
    else
        print_status "WARN" "Multi-factor authentication is not enabled" 20
    fi
    
    # Check session timeout
    local session_timeout=$(grep -i "cookieMaxAge" /opt/trilium/config.ini 2>/dev/null | cut -d'=' -f2)
    if [ "$session_timeout" -le 3600 ]; then
        print_status "PASS" "Session timeout is appropriately configured" 10
    else
        print_status "WARN" "Session timeout may be too long" 10
    fi
    
    echo
}

check_file_permissions() {
    echo "=== File Permissions ==="
    
    # Check database permissions
    local db_perms=$(stat -c "%a" /opt/trilium/data/document.db 2>/dev/null)
    if [ "$db_perms" = "600" ] || [ "$db_perms" = "640" ]; then
        print_status "PASS" "Database file permissions are secure" 10
    else
        print_status "FAIL" "Database file permissions are too permissive ($db_perms)" 10
    fi
    
    # Check data directory permissions
    local dir_perms=$(stat -c "%a" /opt/trilium/data 2>/dev/null)
    if [ "$dir_perms" = "700" ] || [ "$dir_perms" = "750" ]; then
        print_status "PASS" "Data directory permissions are secure" 10
    else
        print_status "FAIL" "Data directory permissions are too permissive ($dir_perms)" 10
    fi
    
    # Check config file permissions
    if [ -f /opt/trilium/config.ini ]; then
        local config_perms=$(stat -c "%a" /opt/trilium/config.ini)
        if [ "$config_perms" = "600" ] || [ "$config_perms" = "640" ]; then
            print_status "PASS" "Configuration file permissions are secure" 5
        else
            print_status "WARN" "Configuration file permissions could be more secure ($config_perms)" 5
        fi
    fi
    
    echo
}

check_network_security() {
    echo "=== Network Security ==="
    
    # Check firewall status
    if ufw status | grep -q "Status: active"; then
        print_status "PASS" "UFW firewall is active" 15
    elif iptables -L | grep -q "DROP"; then
        print_status "PASS" "Firewall rules are configured" 15
    else
        print_status "FAIL" "No firewall detected" 15
    fi
    
    # Check listening ports
    local listening_ports=$(netstat -tulpn | grep :8080 | wc -l)
    if [ "$listening_ports" -eq 1 ]; then
        print_status "PASS" "Trilium is listening on expected port only" 10
    else
        print_status "WARN" "Multiple services or unexpected ports detected" 10
    fi
    
    # Check for direct database access
    local db_ports=$(netstat -tulpn | grep -E ":1433|:3306|:5432" | wc -l)
    if [ "$db_ports" -eq 0 ]; then
        print_status "PASS" "No direct database ports exposed" 10
    else
        print_status "WARN" "Database ports may be exposed" 10
    fi
    
    echo
}

check_backup_security() {
    echo "=== Backup Security ==="
    
    # Check if backups exist
    if [ -d "/opt/trilium/backups" ] && [ "$(ls -A /opt/trilium/backups)" ]; then
        print_status "PASS" "Backup directory exists and contains files" 10
        
        # Check backup encryption
        local encrypted_backups=$(find /opt/trilium/backups -name "*.gpg" | wc -l)
        local total_backups=$(find /opt/trilium/backups -type f | wc -l)
        
        if [ "$encrypted_backups" -eq "$total_backups" ] && [ "$total_backups" -gt 0 ]; then
            print_status "PASS" "All backups are encrypted" 15
        elif [ "$encrypted_backups" -gt 0 ]; then
            print_status "WARN" "Some backups are encrypted ($encrypted_backups/$total_backups)" 15
        else
            print_status "FAIL" "No encrypted backups found" 15
        fi
    else
        print_status "FAIL" "No backup directory or backups found" 25
    fi
    
    echo
}

check_system_security() {
    echo "=== System Security ==="
    
    # Check system updates
    local updates_available=$(apt list --upgradable 2>/dev/null | grep -c "upgradable")
    if [ "$updates_available" -eq 0 ]; then
        print_status "PASS" "System is up to date" 10
    else
        print_status "WARN" "$updates_available updates available" 10
    fi
    
    # Check fail2ban
    if systemctl is-active --quiet fail2ban; then
        print_status "PASS" "Fail2ban is active" 10
    else
        print_status "WARN" "Fail2ban is not active" 10
    fi
    
    # Check SSH security
    if grep -q "PasswordAuthentication no" /etc/ssh/sshd_config 2>/dev/null; then
        print_status "PASS" "SSH password authentication disabled" 10
    else
        print_status "WARN" "SSH password authentication may be enabled" 10
    fi
    
    # Check for root login
    if grep -q "PermitRootLogin no" /etc/ssh/sshd_config 2>/dev/null; then
        print_status "PASS" "SSH root login disabled" 5
    else
        print_status "WARN" "SSH root login may be enabled" 5
    fi
    
    echo
}

check_database_security() {
    echo "=== Database Security ==="
    
    # Check database integrity
    local integrity_check=$(sqlite3 /opt/trilium/data/document.db "PRAGMA integrity_check;" 2>/dev/null)
    if [ "$integrity_check" = "ok" ]; then
        print_status "PASS" "Database integrity check passed" 15
    else
        print_status "FAIL" "Database integrity check failed" 15
    fi
    
    # Check for protected notes
    local protected_notes=$(sqlite3 /opt/trilium/data/document.db "SELECT COUNT(*) FROM notes WHERE isProtected = 1;" 2>/dev/null)
    if [ "$protected_notes" -gt 0 ]; then
        print_status "PASS" "Protected notes are configured" 10
    else
        print_status "WARN" "No protected notes found" 10
    fi
    
    # Check encryption settings
    local encryption_key=$(sqlite3 /opt/trilium/data/document.db "SELECT value FROM options WHERE name = 'encryptedDataKey';" 2>/dev/null)
    if [ -n "$encryption_key" ]; then
        print_status "PASS" "Encryption key is configured" 10
    else
        print_status "WARN" "No encryption key found" 10
    fi
    
    echo
}

check_log_security() {
    echo "=== Logging and Monitoring ==="
    
    # Check log directory
    if [ -d "/var/log/trilium" ]; then
        print_status "PASS" "Trilium log directory exists" 5
        
        # Check log permissions
        local log_perms=$(stat -c "%a" /var/log/trilium 2>/dev/null)
        if [ "$log_perms" = "750" ] || [ "$log_perms" = "755" ]; then
            print_status "PASS" "Log directory permissions are appropriate" 5
        else
            print_status "WARN" "Log directory permissions may be too restrictive or permissive" 5
        fi
    else
        print_status "WARN" "No dedicated Trilium log directory found" 10
    fi
    
    # Check for security event logging
    local security_events=$(sqlite3 /opt/trilium/data/document.db "SELECT COUNT(*) FROM security_events;" 2>/dev/null)
    if [ "$security_events" -gt 0 ]; then
        print_status "PASS" "Security events are being logged" 10
    else
        print_status "WARN" "No security events found in database" 10
    fi
    
    echo
}

generate_recommendations() {
    echo "=== Security Recommendations ==="
    
    if [ $SCORE -lt $((MAX_SCORE * 80 / 100)) ]; then
        echo "⚠️  Your Trilium installation has significant security issues that should be addressed immediately:"
        echo
        
        # Check specific issues and provide recommendations
        if ! curl -s -I https://localhost:8080 2>/dev/null | grep -q "HTTP/"; then
            echo "🔴 CRITICAL: Enable HTTPS immediately"
            echo "   - Configure SSL certificate"
            echo "   - Set up reverse proxy with HTTPS"
            echo "   - Redirect all HTTP traffic to HTTPS"
            echo
        fi
        
        local mfa_enabled=$(sqlite3 /opt/trilium/data/document.db "SELECT value FROM options WHERE name = 'mfaEnabled';" 2>/dev/null)
        if [ "$mfa_enabled" != "true" ]; then
            echo "🟡 HIGH: Enable Multi-Factor Authentication"
            echo "   - Go to Options → Security → Multi-Factor Authentication"
            echo "   - Generate TOTP secret and configure authenticator app"
            echo "   - Save recovery codes securely"
            echo
        fi
        
        if ! systemctl is-active --quiet fail2ban; then
            echo "🟡 MEDIUM: Install and configure fail2ban"
            echo "   - apt install fail2ban"
            echo "   - Configure jail rules for SSH and web services"
            echo "   - Monitor failed authentication attempts"
            echo
        fi
        
        if [ ! -d "/opt/trilium/backups" ] || [ ! "$(ls -A /opt/trilium/backups 2>/dev/null)" ]; then
            echo "🟡 HIGH: Set up encrypted backups"
            echo "   - Configure automated daily backups"
            echo "   - Encrypt backups with GPG"
            echo "   - Test backup restoration procedures"
            echo "   - Store backups in secure off-site location"
            echo
        fi
    fi
    
    echo "💡 General Security Best Practices:"
    echo "   - Keep system and Trilium updated"
    echo "   - Use strong, unique passwords"
    echo "   - Regularly review access logs"
    echo "   - Implement network segmentation"
    echo "   - Monitor for security events"
    echo "   - Maintain incident response procedures"
    echo
}

# Main assessment execution
{
    echo "TRILIUM SECURITY ASSESSMENT REPORT"
    echo "Generated: $(date -Iseconds)"
    echo "=========================================="
    echo
    
    check_https_configuration
    check_authentication_security
    check_file_permissions
    check_network_security
    check_backup_security
    check_system_security
    check_database_security
    check_log_security
    
    echo "=========================================="
    echo "ASSESSMENT SUMMARY"
    echo "=========================================="
    echo "Score: $SCORE / $MAX_SCORE ($(($SCORE * 100 / $MAX_SCORE))%)"
    echo
    
    if [ $SCORE -eq $MAX_SCORE ]; then
        echo "🎉 EXCELLENT: Your Trilium installation follows security best practices!"
    elif [ $SCORE -ge $((MAX_SCORE * 80 / 100)) ]; then
        echo "✅ GOOD: Your Trilium installation is well-secured with minor improvements needed."
    elif [ $SCORE -ge $((MAX_SCORE * 60 / 100)) ]; then
        echo "⚠️  FAIR: Your Trilium installation has some security issues that should be addressed."
    else
        echo "🚨 POOR: Your Trilium installation has significant security vulnerabilities."
    fi
    echo
    
    generate_recommendations
    
} | tee "$REPORT_FILE"

echo "Security assessment completed. Report saved to: $REPORT_FILE"

# Email report if configured
if [ -n "$ADMIN_EMAIL" ]; then
    mail -s "Trilium Security Assessment Report" "$ADMIN_EMAIL" < "$REPORT_FILE"
fi

Security Checklist

Pre-Deployment Checklist

# Trilium Security Pre-Deployment Checklist

## Infrastructure Security
- [ ] Server OS is updated and hardened
- [ ] Firewall rules are configured and tested
- [ ] SSH is properly secured (key-based auth, no root login)
- [ ] SSL certificates are valid and properly configured
- [ ] Reverse proxy is configured with security headers
- [ ] Intrusion detection system is installed and configured

## Application Security
- [ ] Trilium is updated to latest stable version
- [ ] Strong master password is set
- [ ] Multi-factor authentication is enabled
- [ ] Session timeouts are appropriately configured
- [ ] CSRF protection is enabled
- [ ] Content Security Policy headers are set
- [ ] API tokens are properly secured

## Data Protection
- [ ] Database file permissions are restrictive (600/640)
- [ ] Data directory permissions are secure (700/750)
- [ ] Encrypted backups are configured and tested
- [ ] Backup retention policy is defined
- [ ] File integrity monitoring is implemented
- [ ] Data loss prevention measures are in place

## Operational Security
- [ ] Security logging is enabled and configured
- [ ] Log rotation is properly set up
- [ ] Monitoring and alerting systems are operational
- [ ] Incident response procedures are documented
- [ ] Security assessment tools are installed
- [ ] Staff training is completed

## Compliance and Governance
- [ ] Security policies are documented
- [ ] Access control procedures are defined
- [ ] Audit requirements are identified
- [ ] Compliance standards are addressed
- [ ] Regular security reviews are scheduled
- [ ] Penetration testing is planned

This comprehensive security guide provides detailed procedures and best practices for securing Trilium installations across all environments. Regular review and updates of these procedures ensure ongoing security effectiveness as threats evolve.

Remember: Security is not a one-time configuration but an ongoing process requiring continuous monitoring, assessment, and improvement.