Users & roles
PDNS Manager deliberately ships only two roles: admin and user. No fine-grained permission matrix because in 95 % of DNS use cases that's pure complexity without payoff. The fine control lives in zone assignments.
The roles
| Role | What they can do |
|---|---|
| Admin | Sees and changes everything: all servers, all zones, all records, all users, all settings, the audit log, templates, branding, ACME tokens. |
| User | Sees only assigned zones. Inside a zone they have either read or full access. They can't see other zones, server settings, the audit log, or manage users. |
Creating a user
Users → New user. Fields: username, email, password (or leave empty → reset mail is sent if SMTP is configured), role.
Zone assignment with permission level
For a user: Assign zones. Per zone:
- Read – the user sees the zone and its records but can't change anything. Write API calls are server-side rejected with
HTTP 403– including records, DNSSEC toggles and NOTIFY. - Full – the user can do everything inside this zone. No server or settings rights, that stays admin.
The DB model (user_zone_access):
user_id zone_name permission
17 example.com. manage
17 intern.example. read
23 kunde-a.de. manage Two-factor auth (TOTP)
Each user can enable TOTP under Settings → API & Security → Enable 2FA:
- The backend generates a secret and a QR code (via
qrcode). - The user scans with an app (Aegis, Google Authenticator, 1Password, …).
- They enter a fresh 6-digit code → 2FA active.
On the next login the panel asks for username/password and then the TOTP code. Backend-side that's POST /api/v1/auth/login + POST /api/v1/auth/login/2fa.
Password reset
Three paths:
- User self-service:
/forgot-password→ mail with reset link → new password. - Admin: in the user list click a user → "Reset password". Set manually or send a reset link.
- Emergency (console) – when nothing else works:
docker compose exec backend python -c "
from app.core.database import async_session
from app.models.models import User
from app.core.auth import hash_password
import asyncio
async def reset():
async with async_session() as db:
admin = await db.get(User, 1) # user ID 1 = first admin
admin.hashed_password = hash_password('new-password')
await db.commit()
print('Password reset.')
asyncio.run(reset())
" Login rate limit
Since v2.3.7 the backend locks an IP for a few minutes after several failed logins (HTTP 429 with Retry-After). Brute force becomes pointless without legitimate users noticing – the window slides and resets after a successful login.