mirror of
https://github.com/zadam/trilium.git
synced 2025-12-08 16:34:25 +01:00
2171 lines
61 KiB
Markdown
Vendored
2171 lines
61 KiB
Markdown
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](#deployment-security)
|
|
2. [Infrastructure Hardening](#infrastructure-hardening)
|
|
3. [Application Security Configuration](#application-security-configuration)
|
|
4. [Data Protection](#data-protection)
|
|
5. [Operational Security](#operational-security)
|
|
6. [Compliance and Governance](#compliance-and-governance)
|
|
7. [Incident Response](#incident-response)
|
|
8. [Security Assessment](#security-assessment)
|
|
|
|
## Deployment Security
|
|
|
|
### Production Environment Setup
|
|
|
|
#### HTTPS Configuration
|
|
|
|
**Mandatory for Production**: Always use HTTPS in production environments to protect data in transit.
|
|
|
|
```nginx
|
|
# 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
|
|
|
|
```dockerfile
|
|
# 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"]
|
|
```
|
|
|
|
```yaml
|
|
# 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
|
|
|
|
```bash
|
|
#!/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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
#!/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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
#!/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
|
|
|
|
```bash
|
|
#!/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
|
|
|
|
```ini
|
|
# 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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```bash
|
|
#!/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
|
|
|
|
```bash
|
|
# /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
|
|
|
|
```bash
|
|
#!/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
|
|
|
|
```bash
|
|
#!/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
|
|
|
|
```python
|
|
#!/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
|
|
|
|
```ini
|
|
# /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
|
|
|
|
```bash
|
|
#!/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
|
|
|
|
```bash
|
|
#!/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
|
|
|
|
```markdown
|
|
# 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. |