PBIHoster implements comprehensive security features for production deployment. This document outlines all security measures and configuration options.
- Minimum/Maximum Length: Configurable password length requirements
- Complexity Requirements:
- Uppercase letters
- Lowercase letters
- Digits
- Special characters
- Account Lockout: Automatic lockout after failed login attempts
- Validation: Password strength validated on registration and password change
Configuration (via environment variables):
PASSWORD_MIN_LENGTH=8
PASSWORD_MAX_LENGTH=128
PASSWORD_REQUIRE_UPPERCASE=true
PASSWORD_REQUIRE_LOWERCASE=true
PASSWORD_REQUIRE_DIGIT=true
PASSWORD_REQUIRE_SPECIAL=true
PASSWORD_MAX_FAILED_ATTEMPTS=5
PASSWORD_LOCKOUT_MINUTES=15- Automatic Lockout: Account locked after N failed login attempts
- Time-based Unlock: Automatically unlocks after configured duration
- Login Attempt Tracking: All login attempts logged with IP, timestamp, success/failure
- Brute Force Prevention: Rate limiting on auth endpoints prevents rapid-fire attacks
How it works:
flowchart TD
Start(["Login Attempt"])
RateCheck{"Rate limit\nexceeded?"}
Return429["429 Too Many Requests"]
LookupUser["Look up user by username"]
UserExists{"User found?"}
LockCheck{"Account\nlocked?"}
LockExpired{"Lock window\nexpired?"}
ReturnLocked["400 Account Locked\n(retry in N minutes)"]
VerifyPwd{"Password\ncorrect?"}
CountFails["Record failed attempt\n+ check recent failure count"]
FailThreshold{"Failures ≥\nthreshold?"}
LockAccount["Lock account\n+ audit log ACCOUNT_LOCKED"]
ReturnInvalid["400 Invalid credentials"]
ClearAttempts["Clear lockout &\nfailed attempts"]
IssueJWT["Issue JWT\n+ audit log LOGIN"]
ReturnToken["200 { token }"]
Start --> RateCheck
RateCheck -->|Yes| Return429
RateCheck -->|No| LookupUser
LookupUser --> UserExists
UserExists -->|No| ReturnInvalid
UserExists -->|Yes| LockCheck
LockCheck -->|Yes| LockExpired
LockExpired -->|No| ReturnLocked
LockExpired -->|Yes| VerifyPwd
LockCheck -->|No| VerifyPwd
VerifyPwd -->|Wrong| CountFails
CountFails --> FailThreshold
FailThreshold -->|Yes| LockAccount --> ReturnInvalid
FailThreshold -->|No| ReturnInvalid
VerifyPwd -->|Correct| ClearAttempts --> IssueJWT --> ReturnToken
- General Endpoints: 100 requests/minute (configurable)
- Auth Endpoints: 5 requests/minute (configurable)
- IP-based Tracking: Limits enforced per IP address
- Automatic 429 Responses: Rate-limited requests receive HTTP 429 status
Configuration:
RATE_LIMIT_ENABLED=true
RATE_LIMIT_GENERAL=100
RATE_LIMIT_GENERAL_PERIOD=1m
RATE_LIMIT_AUTH=5
RATE_LIMIT_AUTH_PERIOD=1m- Configurable Origins: Whitelist specific frontend domains
- Credential Support: Allow cookies/auth headers for cross-origin requests
- Flexible Methods/Headers: Supports all HTTP methods and headers
Configuration:
CORS_ORIGIN_1=https://reports.example.com
CORS_ORIGIN_2=https://reports-staging.example.com
CORS_ALLOW_CREDENTIALS=trueAll responses include security headers to prevent common attacks:
| Header | Value | Purpose |
|---|---|---|
X-Frame-Options |
DENY |
Prevents clickjacking |
X-Content-Type-Options |
nosniff |
Prevents MIME sniffing |
X-XSS-Protection |
1; mode=block |
Enables XSS filter |
Referrer-Policy |
strict-origin-when-cross-origin |
Controls referrer info |
Permissions-Policy |
geolocation=(), microphone=(), camera=() |
Restricts browser features |
- Strong Signing Keys: Minimum 256-bit keys required
- Configurable Expiry: Default 8 hours, adjustable
- Role-based Claims: Supports Admin, Editor, Viewer roles
- Group Claims: Includes user group memberships
- Secure Storage: Keys managed via environment variables
Configuration:
JWT_KEY=your-secure-256-bit-key-here
JWT_ISSUER=ReportTree
JWT_EXPIRY_HOURS=8- Forwarded Headers: Correctly handles X-Forwarded-For and X-Forwarded-Proto
- Real IP Detection: Rate limiting and audit logs use real client IP
- Caddy Integration: Pre-configured for Caddy reverse proxy with HTTPS
- Provider secret boundary:
Authority,ClientId,ClientSecret, callback path, and protocol settings are configuration/environment managed and not writable via admin API. - Open redirect protection: External login
returnUrlis normalized to local relative routes only. - Short-lived external cookie: OIDC callback uses a dedicated short-lived cookie scheme and clears the cookie after callback processing.
- Safe provider discovery: Public provider discovery endpoint only returns
id,displayName, and authscheme. - Role/group allowlist normalization: External mappings are normalized and restricted to internal roles
Admin|Editor|Viewer. - Least privilege fallback: When mappings do not resolve, users fall back to configured default role.
- Comprehensive Tracking: All security events logged
- Event Types: Login attempts, password changes, lockouts, failed auth
- Context Capture: Username, IP, user agent, timestamp
- Success/Failure: Tracks both successful and failed operations
- Compliance Export: Administrators can export filtered audit logs by date range, user, action type, resource, and success state as CSV or PDF for review and retention workflows
The docker-compose.yml includes all security environment variables with sensible defaults. To customize:
-
Copy
.env.exampleto.env:cp deployment/.env.example deployment/.env
-
Generate a secure JWT key:
openssl rand -base64 32
-
Update
.envwith your values:JWT_KEY=<your-generated-key> CORS_ORIGIN_1=https://yourdomain.com
-
Deploy:
cd deployment docker-compose up -d
JWT_KEY: MUST be changed from default. Generate withopenssl rand -base64 32
All other security settings have production-ready defaults but can be customized:
- Password policy settings
- Rate limiting thresholds
- CORS origins
- Token expiry
- Change JWT_KEY to a strong random value (256+ bits)
- Configure CORS origins to match your frontend domain(s)
- Review password policy and adjust for your requirements
- Test rate limiting doesn't impact legitimate users
- Enable HTTPS via Caddy (already configured)
- Set up database backups (mount
/datavolume) - Review audit logs regularly for suspicious activity
- Test account lockout recovery process
- Document your security configuration for operations team
- Monitor Audit Logs: Review
AuditLogcollection regularly - Watch Failed Logins: Spike in failed attempts may indicate attack
- Rotate JWT Keys: Periodically rotate signing keys (invalidates all tokens)
- Update Dependencies: Keep Docker images and packages updated
- Backup Database: Regular backups of LiteDB file in
/datavolume
- Environment-first: All secrets (JWT signing key, Power BI credentials) are read from environment variables at startup. The app will refuse to boot if any are missing or using default/weak placeholders.
- Azure Key Vault ready: Set
KEY_VAULT_URI(orAZURE_KEY_VAULT_URI) to load secrets directly from a vault via managed identity. Ensure secret names match configuration keys, e.g.Jwt--Key,PowerBI--ClientSecret. - Deployment safety: Keep secrets out of
appsettings.jsonand git. Validate deployments by checking container logs forMissing or insecure secretserrors before exposing endpoints. - External auth credentials: Keep external provider client secrets in environment variables or Key Vault. Do not manage provider secrets in admin UI.
- Prepare new secrets: Generate a new 256-bit JWT key (
openssl rand -base64 32) and, if applicable, a new Power BI client secret or certificate. - Stage in Key Vault: Add the new values as fresh versions in Key Vault (
KEY_VAULT_URI) using the same secret names. Verify access withaz keyvault secret show --name Jwt--Key --vault-name <name>. - Update environment: If not using Key Vault, update
JWT_KEY/POWERBI_CLIENT_SECRETenvironment variables on the host or orchestration platform. Never check values into source control. - Restart with validation: Restart the
pbihostercontainer or app service. Startup validation will fail fast if any value is missing, preventing partial deployments with mixed secrets. - Invalidate old tokens: After successful rollout, remove old secret versions and prompt users to re-authenticate.
- Scope to workspaces: Grant the service principal access only to required workspaces (e.g., Contributor on specific workspaces). Avoid tenant-wide roles.
- Limit API permissions: In Azure AD, keep permissions minimal (
Dataset.Read.All/Report.Read.Allas required) and avoid broad admin consents unless explicitly needed. - Rotate client credentials: Store
POWERBI_CLIENT_SECRETor certificates in Key Vault, and rotate alongside JWT keys. - Audit regularly: Review activity logs for the service principal and remove unused workspaces or roles. Validate that multi-tenant access is disabled unless intentionally configured.
- CI enforcement: The
Security Scansworkflow runs CodeQL SAST, gitleaks secret scanning, dependency review (PRs), and vulnerability audits for .NET (dotnet list package --vulnerable) and the Vue client (npm audit --audit-level=high). - Dependency alerts: Fix or suppress findings in CI before merging. Keep lockfiles updated to capture patched transitive dependencies.
graph TB
Internet(["🌐 Internet"])
Caddy["Caddy Reverse Proxy<br/>HTTPS/TLS Termination · Certificate Management"]
App["ASP.NET Core Application<br/>Security Headers · Rate Limiting · CORS<br/>JWT Authentication · Account Lockout · Password Policy · Audit Logging"]
Data[("Database<br/>BCrypt Passwords · Encrypted Settings")]
Internet -->|HTTPS| Caddy -->|HTTP internal| App --> Data
- Failed Login Rate: Sudden spikes indicate brute force attempts
- Account Lockouts: High lockout rate may indicate attack or UX issues
- Rate Limit Hits: Frequent 429 responses may indicate bot activity
- Token Expirations: Unusual patterns may indicate session hijacking attempts
Check for suspicious activity:
- Recent failed logins:
db.AuditLog.find({ Action: "LOGIN", Success: false }) - Account lockouts:
db.AuditLog.find({ Action: "ACCOUNT_LOCKED" }) - Password changes:
db.AuditLog.find({ Action: "CHANGE_PASSWORD" })
Symptom: User cannot login, receives "Account is locked" message Solution:
- Check
AccountLockoutcollection for user record - Wait for lockout period to expire, or
- Manually remove lockout:
db.AccountLockout.delete({ Username: "user" })
Symptom: Legitimate users receiving 429 errors Solution:
- Check current limits in environment variables
- Increase
RATE_LIMIT_GENERALorRATE_LIMIT_GENERAL_PERIOD - Restart application:
docker-compose restart pbihoster
Symptom: Frontend cannot make API requests, browser shows CORS error Solution:
- Add frontend origin to
CORS_ORIGIN_1environment variable - Ensure origin includes protocol (https://)
- Restart application
Symptom: Valid tokens rejected, users cannot authenticate Solution:
- Verify
JWT_KEYhasn't changed (changing key invalidates all tokens) - Check token expiry with
JWT_EXPIRY_HOURS - Ensure system clock is correct
- Refresh Tokens: Long-lived tokens for token renewal without re-login
- Multi-Factor Authentication (MFA): TOTP-based 2FA
- Session Management: Track active sessions, force logout
- IP Whitelisting: Allow specific IPs to bypass rate limits
- Security Events API: Expose security events for SIEM integration
- Password History: Prevent password reuse
- Email Notifications: Alert users of security events
If you identify security vulnerabilities or have enhancement suggestions:
- Do not create public GitHub issues for vulnerabilities
- Use GitHub Security Advisories to report vulnerabilities privately
- Include detailed description and reproduction steps
- Allow reasonable time for patching before public disclosure