Outline Security Testing: Default Credentials, API Token, Database, and .env SECRET_KEY

Outline is a widely deployed open-source team wiki and knowledge base platform (Notion alternative) used by engineering teams to store documentation, runbooks, architecture diagrams, and internal processes — it typically contains highly sensitive internal information. Key assessment areas: the .env file stores SECRET_KEY (used for session token signing), AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY for S3 document storage, SLACK_CLIENT_SECRET for OAuth authentication, and DATABASE_URL with the PostgreSQL password; Outline's REST API allows enumerating all documents and users with a valid API token; and Outline's reliance on external SSO (Slack, Google) means OAuth credential compromise can provide access to all wiki content. This guide covers systematic Outline security assessment.

Table of Contents

  1. Authentication and API Token Testing
  2. REST API Document and User Enumeration
  3. env Credential Extraction: SECRET_KEY, S3, OAuth, Database
  4. Outline Security Hardening

Authentication and API Token Testing

# Outline — authentication and API token testing
OUTLINE_URL="https://wiki.example.com"

# Outline uses SSO (Slack/Google OAuth) — no traditional username/password
# API tokens are generated in user settings (Settings > API Tokens)
API_TOKEN="ol_api_your_token_here"

# Verify token and get current user info
curl -s -X POST "${OUTLINE_URL}/api/auth.info" \
  -H "Authorization: Bearer ${API_TOKEN}" \
  -H "Content-Type: application/json" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
if d.get('ok'):
    u = d.get('data',{}).get('user',{})
    team = d.get('data',{}).get('team',{})
    print(f'Authenticated as: {u.get(\"name\")} ({u.get(\"email\")})')
    print(f'Role: {u.get(\"role\")}')
    print(f'Team: {team.get(\"name\")} ({team.get(\"subdomain\")})')
else:
    print(f'Auth failed: {d}')
" 2>/dev/null

# Check if public API is accessible
curl -s "${OUTLINE_URL}/api/auth.config" \
  -H "Content-Type: application/json" \
  -d '{}' 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
if d.get('ok'):
    providers = d.get('data',{}).get('providers',[])
    print(f'Auth providers: {[p.get(\"id\") for p in providers]}')
" 2>/dev/null

REST API Document and User Enumeration

# Outline REST API — document and user enumeration
OUTLINE_URL="https://wiki.example.com"
API_TOKEN="ol_api_your_token_here"

# List all collections — top-level organizational structure
curl -s -X POST "${OUTLINE_URL}/api/collections.list" \
  -H "Authorization: Bearer ${API_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"limit":100}' 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
if d.get('ok'):
    cols = d.get('data',[])
    print(f'Collections: {len(cols)}')
    for c in cols:
        print(f'  [{c.get(\"id\")}] {c.get(\"name\")} private={c.get(\"private\")} docs={c.get(\"documentsCount\")}')
" 2>/dev/null

# List all documents — full wiki content
curl -s -X POST "${OUTLINE_URL}/api/documents.list" \
  -H "Authorization: Bearer ${API_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"limit":100,"sort":"updatedAt","direction":"DESC"}' 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
if d.get('ok'):
    docs = d.get('data',[])
    print(f'Documents: {len(docs)}')
    for doc in docs[:10]:
        print(f'  [{doc.get(\"id\")}] {doc.get(\"title\")} updatedAt={doc.get(\"updatedAt\")[:10] if doc.get(\"updatedAt\") else \"?\"} private={doc.get(\"isPrivate\")}')
" 2>/dev/null

# Export a specific document — full Markdown content
DOC_ID="your-document-id"
curl -s -X POST "${OUTLINE_URL}/api/documents.export" \
  -H "Authorization: Bearer ${API_TOKEN}" \
  -H "Content-Type: application/json" \
  -d "{\"id\":\"${DOC_ID}\"}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
if d.get('ok'):
    print(d.get('data','')[:500])
" 2>/dev/null

# List all users — admin can enumerate all team members
curl -s -X POST "${OUTLINE_URL}/api/users.list" \
  -H "Authorization: Bearer ${API_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"limit":100}' 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
if d.get('ok'):
    users = d.get('data',[])
    print(f'Users: {len(users)}')
    for u in users[:10]:
        print(f'  [{u.get(\"id\")}] {u.get(\"name\")} email={u.get(\"email\")} role={u.get(\"role\")} isAdmin={u.get(\"isAdmin\")}')
" 2>/dev/null

.env Credential Extraction: SECRET_KEY, S3, OAuth, Database

# Outline .env credential extraction

# .env file — contains all secrets for Outline
cat /var/www/outline/.env 2>/dev/null | grep -E "SECRET_KEY|AWS|SLACK|GOOGLE|DATABASE|SMTP|OIDC"

# KEY credentials to extract:
# SECRET_KEY — 32-byte hex string used for session cookie signing
#   → enables forging session tokens as any user
# DATABASE_URL — full PostgreSQL DSN including password
#   → postgres://outline:PASSWORD@localhost/outline
# AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY — S3 credentials for document storage
#   → enables downloading all document attachments and images from S3
# AWS_S3_UPLOAD_BUCKET_NAME — the S3 bucket containing all uploaded files
# SLACK_CLIENT_SECRET — OAuth2 secret for Slack SSO
#   → enables impersonating the Slack OAuth app
# GOOGLE_CLIENT_SECRET — OAuth2 secret for Google SSO

# Docker deployment
docker inspect outline 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 ['SECRET','AWS','SLACK','DATABASE','GOOGLE','PASSWORD']):
            print(e)
" 2>/dev/null

# PostgreSQL direct access — users and API tokens
psql "postgres://outline:PASSWORD@localhost/outline" 2>/dev/null << 'EOF'
-- All users with email and authentication provider
SELECT id, name, email, role, "createdAt", "lastActiveAt",
       "isAdmin", "isSuspended"
FROM users
ORDER BY "isAdmin" DESC, "lastActiveAt" DESC
LIMIT 20;
EOF

# API tokens table — all active API tokens
psql "postgres://outline:PASSWORD@localhost/outline" 2>/dev/null << 'EOF'
SELECT t.id, t.name, t."userId", u.email, t."lastActiveAt",
       t.hash  -- SHA-256 hashed token
FROM api_keys t
JOIN users u ON t."userId" = u.id
ORDER BY t."lastActiveAt" DESC
LIMIT 20;
EOF

# S3 bucket access with extracted credentials
AWS_ACCESS_KEY_ID="extracted-key" AWS_SECRET_ACCESS_KEY="extracted-secret" \
  aws s3 ls s3://outline-uploads-bucket/ --recursive 2>/dev/null | head -20
# Lists all uploaded document attachments and images

Outline Security Hardening

Outline Security Hardening Checklist:
Security TestMethodRisk
SECRET_KEY extraction and session token forgeryRead .env SECRET_KEY — 32-byte hex session signing key; forge valid session cookies for any Outline user bypassing SSO authentication entirelyCritical
AWS S3 credential extraction and document bucket accessRead .env AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY; aws s3 ls s3://bucket/ — all uploaded document attachments, images, and exports; may contain credentials and sensitive internal documentsCritical
API document and collection enumerationPOST /api/documents.list — all team documents; POST /api/documents.export returns full Markdown content of any document; complete access to team knowledge baseHigh
Slack/Google OAuth secret extractionRead .env SLACK_CLIENT_SECRET/GOOGLE_CLIENT_SECRET — OAuth application secrets; enables authorization code interception and SSO session hijackingCritical
DATABASE_URL PostgreSQL credential extractionRead .env DATABASE_URL — full connection string with password; direct PostgreSQL access to all Outline data including user table and SHA-256 hashed API tokensCritical

Automate Outline Security Testing

Ironimo tests Outline deployments for .env SECRET_KEY extraction and session forgery, AWS S3 credential extraction with bucket enumeration and document download, Slack/Google OAuth secret exposure, DATABASE_URL PostgreSQL credential extraction, REST API document and collection enumeration, user email harvesting, API token audit, Docker environment variable credential exposure, S3 bucket policy misconfiguration, and PostgreSQL network exposure on port 5432.

Start free scan