Security Guide

Best practices for securing your Borg Web UI installation.


Quick Security Checklist

  • Change default admin password immediately
  • Use HTTPS with reverse proxy (production)
  • Restrict volume mounts to necessary directories only
  • Set appropriate PUID/PGID for file permissions
  • Use SSH keys (not passwords) for remote repositories
  • Enable firewall rules to limit access
  • Regularly update to latest version
  • Backup the /data volume (contains database and keys)
  • Review and rotate SSH keys periodically
  • Monitor application logs for suspicious activity

Authentication Security

Built-in Authentication (Default)

By default, Borg Web UI uses its own JWT-based authentication system.

Change Default Password

On first login, you’ll be prompted to change the default password (admin123).

To change later:

  1. Go to Settings > Profile
  2. Enter current password
  3. Enter new password (minimum 8 characters)
  4. Confirm new password

Strong Password Requirements

Use passwords with:

  • Minimum 12 characters
  • Mix of uppercase and lowercase
  • Numbers and special characters
  • Unique to this application (no reuse)

Example strong password: B0rg!Backup#2025$Secure

User Management

For multi-user setups:

  1. Create individual accounts for each user
  2. Assign appropriate permissions (admin vs. regular user)
  3. Disable or delete inactive accounts
  4. Review user access regularly

Emergency Password Reset (CLI)

Use this procedure when a user (including the admin) is locked out of the web UI and cannot change their password through Settings > Profile.

Security warning: This tool requires shell access to the container or host. Treat shell access as equivalent to full admin access — anyone who can run docker exec can reset any password.

Docker (primary method):

docker exec -it borg-ui python -m app.scripts.reset_password <username> <new_password>

Concrete example:

docker exec -it borg-ui python -m app.scripts.reset_password admin newpassword123

Non-Docker / custom database path:

Set the BORG_DB_PATH environment variable to override the default /data/borg.db:

BORG_DB_PATH=/custom/path/borg.db python -m app.scripts.reset_password admin newpassword123

Behaviour: The script sets must_change_password = 0, so the user is not forced to change the password again immediately after logging in.


Proxy/OIDC Authentication

New Feature: Proxy-based authentication for OIDC/SSO integration

Borg Web UI supports proxy-based authentication to integrate with external authentication providers like:

  • Authentik
  • Authelia
  • Keycloak
  • Authserv
  • Google Identity-Aware Proxy (IAP)
  • Azure AD Application Proxy
  • Cloudflare Access
  • Any reverse proxy that provides authenticated usernames in headers

How It Works

When enabled, Borg Web UI:

  1. Disables the login screen - No password prompts
  2. Trusts the reverse proxy - Reads username from HTTP headers
  3. Auto-creates users - Creates accounts on first access
  4. Maintains authorization - Still respects admin/user permissions

Security Model:

  • ✅ Authentication: Handled by your proxy/OIDC provider
  • ✅ Authorization: Managed by Borg Web UI (admin vs. regular user)
  • ✅ Session management: JWT tokens still used for API calls

Configuration

1. Enable proxy authentication:

environment:
  - DISABLE_AUTHENTICATION=true  # Disable built-in login screen
  - PROXY_AUTH_HEADER=X-Forwarded-User  # Default header name

2. Configure your reverse proxy to forward authenticated usernames:

The proxy must set the X-Forwarded-User header (or your custom header) with the authenticated username.

Supported Headers (checked in order):

  • X-Forwarded-User (default, configurable via PROXY_AUTH_HEADER)
  • X-Remote-User
  • Remote-User
  • X-authentik-username (Authentik)

Security Requirements

⚠️ CRITICAL: This feature requires proper security configuration

You MUST:

  1. Bind Borg UI to localhost only: ```yaml ports:
    • “127.0.0.1:8081:8081” # Only accessible via localhost ```
  2. Use firewall rules to block direct access:
    # Block external access to port 8081
    sudo ufw deny 8081
    sudo ufw allow from 127.0.0.1 to any port 8081
    
  3. Ensure ONLY your reverse proxy can reach Borg UI
    • Never expose the container port to the internet
    • Use Docker networks to isolate Borg UI from direct access

Why this matters:

  • If Borg UI is directly accessible, anyone can set the X-Forwarded-User header and impersonate any user
  • The proxy MUST strip/override user-supplied headers before forwarding

Example Configurations

Authentik

docker-compose.yml:

services:
  borg-ui:
    image: ainullcode/borg-ui:latest
    environment:
      - DISABLE_AUTHENTICATION=true
      - PROXY_AUTH_HEADER=X-authentik-username
    networks:
      - internal
    # NO ports exposed - only accessible via proxy

  authentik-proxy:
    image: ghcr.io/goauthentik/proxy:latest
    environment:
      - AUTHENTIK_HOST=https://auth.example.com
      - AUTHENTIK_INSECURE=false
      - AUTHENTIK_TOKEN=your-outpost-token
    ports:
      - "8443:8443"
    networks:
      - internal
      - external
    labels:
      - "authentik.enabled=true"
      - "authentik.upstream=http://borg-ui:8081"

Authentik Application Setup:

  1. Create new application in Authentik
  2. Select Proxy Provider
  3. Set External URL: https://backups.example.com
  4. Set Internal URL: http://borg-ui:8081
  5. Enable Forward auth (single application)
  6. Set authorization flow and user/group bindings
Authelia

Authelia configuration.yml:

access_control:
  rules:
    - domain: backups.example.com
      policy: one_factor  # or two_factor
      subject:
        - "group:admins"
        - "group:backup-users"

nginx configuration:

server {
    listen 443 ssl http2;
    server_name backups.example.com;

    # SSL configuration...

    # Authelia authentication
    include /path/to/authelia-authrequest.conf;

    location / {
        # Forward authenticated username to Borg UI
        proxy_set_header X-Remote-User $remote_user;
        proxy_set_header X-Forwarded-User $remote_user;

        proxy_pass http://127.0.0.1:8081;
        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;
    }
}
Cloudflare Access

1. Create Cloudflare Access application:

  • Application name: Borg Backups
  • Session duration: 24 hours
  • Add policies for users/groups

2. Configure Borg UI:

environment:
  - DISABLE_AUTHENTICATION=true
  - PROXY_AUTH_HEADER=Cf-Access-Authenticated-User-Email

3. Cloudflare Access forwards the user’s email in the Cf-Access-Authenticated-User-Email header

Nginx with Basic Auth (Simple Setup)

For basic HTTP authentication:

server {
    listen 443 ssl http2;
    server_name backups.example.com;

    # SSL configuration...

    auth_basic "Borg Backups";
    auth_basic_user_file /etc/nginx/.htpasswd;

    location / {
        # Forward authenticated username
        proxy_set_header X-Remote-User $remote_user;
        proxy_set_header X-Forwarded-User $remote_user;

        proxy_pass http://127.0.0.1:8081;
        # Other proxy headers...
    }
}

Create users:

htpasswd -c /etc/nginx/.htpasswd username

User Management with Proxy Auth

First-time access:

  • Users are auto-created when they first access the application
  • New users are created as regular users (not admins)
  • Users inherit the username from the proxy header

Making users admins:

  1. Admin manually promotes users via Settings > User Management
  2. Or update database directly:
    docker exec borg-web-ui sqlite3 /data/borg.db "UPDATE users SET is_admin=1 WHERE username='alice';"
    

Disabling users:

  • Set is_active=0 in the database
  • Or use the User Management interface (when user is admin)

Testing Proxy Auth

Verify headers are being sent:

# From your reverse proxy server
curl -H "X-Forwarded-User: testuser" http://localhost:8081/api/auth/me

Check application logs:

docker logs borg-web-ui 2>&1 | grep "proxy"
docker logs borg-web-ui 2>&1 | grep "X-Forwarded-User"

Test with direct access (should use fallback):

# Without proxy header - falls back to 'admin' user
curl http://localhost:8081/api/auth/me

Switching Between Auth Methods

To disable proxy auth and return to built-in authentication:

  1. Remove environment variables:
    environment:
      # - DISABLE_AUTHENTICATION=true  # Commented out
      # - PROXY_AUTH_HEADER=X-Forwarded-User
    
  2. Restart container:
    docker compose up -d
    
  3. Users can now log in with passwords again

Note: Existing users created via proxy auth will still exist, but they’ll need passwords set by an admin.

Troubleshooting

Problem: Login screen still appears

  • Verify DISABLE_AUTHENTICATION=true is set
  • Check environment variables: docker exec borg-ui env | grep DISABLE
  • Restart container after changing environment

Problem: “Could not validate credentials” errors

  • Check proxy is sending the authentication header
  • Verify header name matches PROXY_AUTH_HEADER
  • Check logs: docker logs borg-web-ui | grep "proxy"

Problem: Wrong user is logged in

  • Proxy may not be stripping user-supplied headers
  • Verify Borg UI is only accessible via proxy (not directly)
  • Check firewall rules and port bindings

Problem: Users can’t access after proxy auth is enabled

  • First access auto-creates regular users (not admins)
  • Admin must manually promote users to admin
  • Check user status: docker exec borg-web-ui sqlite3 /data/borg.db "SELECT * FROM users;"

Security Checklist for Proxy Auth

  • DISABLE_AUTHENTICATION=true is set
  • Borg UI bound to localhost only (127.0.0.1:8081)
  • Firewall blocks external access to port 8081
  • Reverse proxy is configured to authenticate users
  • Reverse proxy strips user-supplied authentication headers
  • HTTPS is enabled on the reverse proxy
  • Reverse proxy has proper access controls (groups, 2FA, etc.)
  • First admin user has been promoted manually
  • Tested that direct access falls back safely
  • Application logs reviewed for proxy auth events

Network Security

Use HTTPS in Production

Never expose Borg Web UI directly to the internet without HTTPS.

Option 1: Nginx Reverse Proxy

server {
    listen 443 ssl http2;
    server_name backups.example.com;

    ssl_certificate /etc/letsencrypt/live/backups.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/backups.example.com/privkey.pem;

    # Strong SSL configuration
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    location / {
        proxy_pass http://localhost:8081;
        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;
    }
}

Option 2: Traefik with Let’s Encrypt

services:
  borg-ui:
    image: ainullcode/borg-ui:latest
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.borg-ui.rule=Host(`backups.example.com`)"
      - "traefik.http.routers.borg-ui.entrypoints=websecure"
      - "traefik.http.routers.borg-ui.tls.certresolver=letsencrypt"
      - "traefik.http.services.borg-ui.loadbalancer.server.port=8081"

Option 3: Caddy (Automatic HTTPS)

backups.example.com {
    reverse_proxy localhost:8081
}

Restrict Access by IP

Docker-level restriction:

ports:
  - "127.0.0.1:8081:8081"  # Only localhost

Firewall rules:

# Linux (ufw)
sudo ufw allow from 192.168.1.0/24 to any port 8081

# Linux (iptables)
sudo iptables -A INPUT -p tcp -s 192.168.1.0/24 --dport 8081 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 8081 -j DROP

VPN Access

For remote access, use VPN instead of exposing to internet:

  • WireGuard
  • OpenVPN
  • Tailscale
  • ZeroTier

File System Security

Restrict Volume Mounts

⚠️ Critical Security Practice

Never mount the entire filesystem in production:

volumes:
  # ❌ DANGEROUS: Full filesystem access
  - /:/local:rw  # Development/testing ONLY

✅ Recommended (principle of least privilege):

volumes:
  # Application data (required)
  - borg_data:/data
  - borg_cache:/home/borg/.cache/borg

  # Backup sources - mount only what you need
  - /home/username:/local:ro              # Home directory (read-only)
  - /var/www:/local/www:ro                # Website files (read-only)
  - /mnt/backups:/local/backup:rw         # Backup destination (read-write)

Why this matters:

  • Reduces attack surface - Container can only access specified directories
  • Prevents data leakage - Accidental exposure is limited to mounted paths
  • Audit trail - Clear documentation of what’s accessible
  • Defense in depth - If container is compromised, damage is contained

Set Appropriate Permissions

Match container user with host user:

environment:
  - PUID=1000  # Your user ID
  - PGID=1000  # Your group ID

This prevents:

  • Unauthorized file access
  • Permission denied errors
  • Files owned by root when created by container

Read-Only Mounts for Sources

Always mount backup sources as read-only when possible:

volumes:
  # ✅ Read-only for backup-only directories
  - /var/www:/local/www:ro            # Can't be modified
  - /home/user/documents:/local:ro    # Protected from writes

  # ⚠️ Read-write only when needed for restores
  - /mnt/backups:/local/backup:rw     # Backup storage location

Benefits:

  • Prevents accidental modification during backup operations
  • Protects against ransomware that might target backup source
  • Makes it clear which directories are backup sources vs. destinations
  • Additional layer of protection if backup script has bugs

SSH Security

Use SSH Keys (Not Passwords)

Always use SSH keys for remote repositories.

Generate keys through the web interface:

  1. Go to SSH Keys
  2. Click Generate SSH Key
  3. Use ED25519 (modern) or RSA 4096 (compatible)

Restrict SSH Key Access

On the remote server, restrict what the key can do:

# In ~/.ssh/authorized_keys
command="borg serve --restrict-to-path /backups/borg-repo",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-ed25519 AAAAC3... borg-web-ui

This:

  • Limits to borg serve command only
  • Restricts access to specific path
  • Disables port forwarding
  • Disables X11 forwarding
  • Prevents interactive shell

SSH Server Hardening

On remote backup servers:

# /etc/ssh/sshd_config
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
AllowUsers backup-user
Port 2222  # Use non-standard port

Restart SSH: sudo systemctl restart sshd


Repository Security

Use Encryption

Always encrypt repositories, especially for offsite/cloud backups.

Choose encryption mode when creating repository:

  • repokey-blake2 (recommended) - Key stored in repo, fast
  • keyfile-blake2 - Key stored locally only
  • repokey - AES-256, widely compatible

Strong Passphrases

Use strong repository passphrases:

  • Minimum 20 characters
  • Mix of characters, numbers, symbols
  • Generated randomly (use password manager)
  • Unique per repository

Example: Xk9#mP2$vL8@qR5!wT3&hN7*

Store Passphrases Securely

  • Use a password manager (Bitwarden, 1Password, KeePass)
  • Never commit passphrases to git
  • Document where passphrase is stored
  • Have a recovery plan

Backup Repository Keys

For keyfile encryption mode:

# Export key
docker exec borg-web-ui borg key export /path/to/repo backup-key.txt

# Store securely (offline, encrypted USB drive, password manager)

Without the key, your backups are unrecoverable.


Application Security

SECRET_KEY Rotation

The SECRET_KEY is used for session management and JWT tokens.

To rotate:

docker exec borg-web-ui rm /data/.secret_key
docker restart borg-web-ui

Note: This logs out all users and invalidates all tokens.

Database Encryption

The SQLite database contains:

  • User credentials (hashed)
  • Repository configurations
  • Notification service URLs (may contain credentials)
  • SSH key paths

Protect /data volume:

  • Secure file permissions
  • Regular backups
  • Encrypt at rest (LUKS, dm-crypt)

Secure Notification URLs

Notification service URLs often contain credentials:

mailto://user:app_password@gmail.com?smtp=smtp.gmail.com
slack://TokenA/TokenB/TokenC/

Best practices:

  • Don’t share notification configurations
  • Rotate tokens periodically
  • Use least-privilege service accounts

Monitoring and Auditing

Enable Logging

environment:
  - LOG_LEVEL=INFO  # or DEBUG for troubleshooting

Application logs are sent to Docker logs (stdout/stderr). Job logs are stored in /data/logs/.

Review Logs Regularly

# View application logs (authentication, errors, API requests)
docker logs borg-web-ui

# Tail application logs in real-time
docker logs -f borg-web-ui

# Search for failed logins
docker logs borg-web-ui 2>&1 | grep "authentication failed"

# Check for errors
docker logs borg-web-ui 2>&1 | grep "ERROR"

# View job logs (backup, check, compact operations)
docker exec borg-web-ui ls -lh /data/logs/

Monitor Failed Login Attempts

Watch for suspicious activity:

# Failed authentication attempts
docker logs borg-web-ui 2>&1 | grep "401 Unauthorized"

# Multiple failed attempts from same IP
docker logs borg-web-ui 2>&1 | grep "authentication" | sort | uniq -c

Set Up Alerts

Use notifications to get alerts for:

  • Backup failures
  • Schedule failures
  • System errors

Update Security

Keep Software Updated

# Check for updates
docker pull ainullcode/borg-ui:latest

# Update
docker compose pull
docker compose up -d

Subscribe to Security Announcements


Backup Security

Backup Strategy

3-2-1 Rule:

  • 3 copies of data
  • 2 different media types
  • 1 offsite backup

Secure Backup Locations

For offsite backups:

  • Use encrypted repositories
  • Verify physical security of remote location
  • Use VPN or SSH tunnels for transmission
  • Regular integrity checks

Test Restores

Regularly test restoring from backups:

  1. Verify backups are accessible
  2. Check data integrity
  3. Confirm encryption keys work
  4. Document restore procedures

Incident Response

If Credentials Are Compromised

  1. Change passwords immediately
    • Admin password in Borg Web UI
    • Repository passphrases
    • Remote server passwords
  2. Rotate SSH keys
    • Generate new keys
    • Deploy to servers
    • Remove old keys
  3. Rotate SECRET_KEY
    docker exec borg-web-ui rm /data/.secret_key
    docker restart borg-web-ui
    
  4. Review logs for unauthorized access

  5. Check backups for tampering

If Container Is Compromised

  1. Stop the container immediately
    docker stop borg-web-ui
    
  2. Preserve logs for analysis
    docker logs borg-web-ui > incident-logs.txt
    
  3. Check for malware

  4. Restore from known-good backup

  5. Investigate root cause

  6. Update and strengthen security

Security Best Practices Summary

  1. Authentication
    • Strong unique passwords
    • Change default credentials
    • Regular password rotation
  2. Network
    • Always use HTTPS in production
    • Restrict access by IP/VPN
    • Never expose directly to internet
  3. File System
    • Restrict volume mounts
    • Use read-only for sources
    • Proper PUID/PGID
  4. SSH
    • Use keys, not passwords
    • Restrict key permissions
    • Non-standard ports
  5. Repositories
    • Always use encryption
    • Strong passphrases
    • Backup repository keys
  6. Monitoring
    • Enable logging
    • Review logs regularly
    • Set up failure alerts
  7. Updates
    • Keep software current
    • Subscribe to security announcements
    • Test updates in staging first

Security Resources


Reporting Security Issues

If you discover a security vulnerability:

  1. Do NOT open a public issue
  2. Email: security contact via GitHub Security Advisories
  3. Include:
    • Description of vulnerability
    • Steps to reproduce
    • Potential impact
    • Suggested fix (if any)

We take security seriously and will respond promptly.


Back to top

Copyright © 2024 Karan Hudia. Distributed under the AGPL-3.0 License.

This site uses Just the Docs, a documentation theme for Jekyll.