Security Guide
This guide covers security best practices for deploying and operating Milvaion in production environments. All security configurations are managed through appsettings.json, environment variables, and infrastructure settings.
Authentication Configuration
JWT Token Settings
Configure token security in appsettings.json:
{
"Milvasoft": {
"Identity": {
"Token": {
"UseUtcForDateTimes": true,
"ExpirationMinute": 90,
"TokenValidationParameters": {
"ValidateIssuer": true,
"ValidateAudience": true,
"ValidIssuer": "https://your-domain.com",
"ValidAudience": "milvaion-clients"
},
"SecurityKeyType": 0,
"SymmetricPublicKey": "your-256-bit-secret-key-here"
}
}
}
}
Recommended Settings
| Setting | Development | Production |
|---|---|---|
ExpirationMinute | 90 | 15-30 |
ValidateIssuer | false | true |
ValidateAudience | false | true |
SymmetricPublicKey | Any | Random 256-bit key |
Generate a Secure Key
MilvaIdentity requires a 256-bit (32 byte) symmetric key for JWT signing.
The key may be provided in hexadecimal or Base64 format as long as it represents 32 cryptographically secure bytes.
Windows (PowerShell – Hex Format)
# Generates a 256-bit (32 byte) AES-compatible key in HEX format
$bytes = New-Object byte[] 32
[Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($bytes)
($bytes | ForEach-Object { $_.ToString("x2") }) -join ""
Example output:
dc778fe1bf2572e91cba54a9835fb2268ad7fdc8bfc2e313d23f975b09111ae3
Linux / macOS (Hex Format)
openssl rand -hex 32
Optional: Base64 Format
# Windows
[Convert]::ToBase64String(
(1..32 | ForEach-Object { Get-Random -Maximum 256 })
)
# Linux / macOS
openssl rand -base64 32
⚠️ Important Security Notes
- Never reuse keys between environments
- Never commit production keys to source control
- Store keys in environment variables or secret managers
- Rotate JWT signing keys periodically (recommended: every 90 days)
Token Expiration Behavior
Milvaion handles token states with specific HTTP status codes:
| Status Code | Meaning | Client Action |
|---|---|---|
| 401 | Invalid or missing token | Redirect to login |
| 419 | Token expired | Refresh token or re-login |
| 403 | Insufficient permissions | Show access denied |
Password Security
Password Policy Configuration
Configure password requirements in appsettings.json:
{
"Milvasoft": {
"Identity": {
"Password": {
"Hasher": {
"IterationCount": 10000
},
"RequiredLength": 8,
"RequiredUniqueChars": 2,
"RequireNonAlphanumeric": true,
"RequireLowercase": true,
"RequireUppercase": true,
"RequireDigit": true
}
}
}
}
Recommended Values
| Setting | Minimum | High Security |
|---|---|---|
RequiredLength | 8 | 12+ |
IterationCount | 10000 | 100000+ |
RequiredUniqueChars | 2 | 4 |
Account Lockout Protection
Protect against brute-force attacks:
{
"Milvasoft": {
"Identity": {
"Lockout": {
"AllowedForNewUsers": true,
"MaxFailedAccessAttempts": 5,
"DefaultLockoutTimeSpan": 15
}
}
}
}
Behavior:
- After 5 failed attempts → Account locked for 15 minutes
- Lockout duration shown to user
- Successful login resets the counter
🌐 Cross-Origin Resource Sharing (CORS)
Milvaion uses a configuration-driven CORS model.
All CORS policies are defined in appsettings.json and applied dynamically at startup.
⚠️ Misconfigured CORS is a common source of security vulnerabilities. Always restrict origins in production environments.
CORS Configuration
Example appsettings.json
{
"Cors": {
"DefaultPolicy": "Public",
"Policies": {
"Public": {
"Origins": [
"https://app.your-domain.com"
],
"Methods": [ "GET", "POST", "PUT", "PATCH", "DELETE" ],
"Headers": [ "Content-Type", "Authorization" ],
"AllowCredentials": false
}
}
}
}
Internal / Trusted Environments (Allow All with Credentials)
For internal APIs, admin panels, or gateway-protected services, Milvaion supports a
controlled “allow all origins with credentials” mode using SetIsOriginAllowed.
{
"Cors": {
"DefaultPolicy": "AllowAll",
"Policies": {
"AllowAll": {
"AllowAnyOriginWithCredentials": true,
"Origins": [ "All" ],
"Methods": [ "All" ],
"Headers": [ "All" ],
"ExposedHeaders": [ "Content-Disposition" ],
"AllowCredentials": true
}
}
}
}
⚠️ Warning: This configuration MUST NOT be exposed to the public internet. Use it only behind a reverse proxy, VPN, or API gateway.
Environment-Based CORS Strategy
| Environment | Recommended Policy |
|---|---|
| Development | AllowAll |
| QA / Staging | Whitelisted domains |
| Production (Public API) | Strict origin whitelist |
| Production (Internal API) | Gateway-only AllowAll |
Example override:
// appsettings.Development.json
{
"Cors": {
"DefaultPolicy": "AllowAll"
}
}
// appsettings.Production.json
{
"Cors": {
"DefaultPolicy": "Public"
}
}
Default Policy Behavior
The policy defined by Cors:DefaultPolicy is automatically applied globally and default configuration value is AllowAll;
"Cors": {
"DefaultPolicy": "AllowAll",
"Policies": {
"AllowAll": {
"AllowAnyOriginWithCredentials": true,
"Origins": [ "All" ],
"Methods": [ "All" ],
"Headers": [ "All" ],
"ExposedHeaders": [ "Content-Disposition" ],
"AllowCredentials": true
}
}
}
but fully configuration-driven.
Infrastructure Security
PostgreSQL
Secure Connection String
{
"ConnectionStrings": {
"DefaultConnectionString": "Host=postgres;Port=5432;Database=MilvaionDb;Username=milvaion_app;Password=SECURE_PASSWORD;SSL Mode=Require;Trust Server Certificate=false;Pooling=true;Maximum Pool Size=100"
}
}
Best Practices
| Practice | Implementation |
|---|---|
| Dedicated user | Create milvaion_app with minimum required permissions |
| SSL/TLS | Enable SSL Mode=Require |
| Network isolation | Use private network, no public IP |
| Connection limits | Set Maximum Pool Size appropriately |
Database User Permissions
-- Create application user with minimal permissions
CREATE USER milvaion_app WITH PASSWORD 'secure_password';
-- Grant only necessary permissions
GRANT CONNECT ON DATABASE MilvaionDb TO milvaion_app;
GRANT USAGE ON SCHEMA public TO milvaion_app;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO milvaion_app;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO milvaion_app;
-- For migrations (use a separate admin user)
CREATE USER milvaion_admin WITH PASSWORD 'admin_password';
GRANT CREATE ON SCHEMA public TO milvaion_admin;
Redis
API Configuration
{
"MilvaionConfig": {
"Redis": {
"ConnectionString": "redis:6379,ssl=true",
"Password": "STRONG_REDIS_PASSWORD",
"Database": 0,
"ConnectTimeout": 5000,
"SyncTimeout": 5000
}
}
}
Worker Configuration
{
"Worker": {
"Redis": {
"ConnectionString": "redis:6379",
"Password": "SAME_REDIS_PASSWORD",
"Database": 0,
"CancellationChannel": "Milvaion:JobScheduler:cancellation_channel"
}
}
}
Redis Server Hardening
# redis.conf
requirepass YOUR_STRONG_PASSWORD
bind 127.0.0.1 # Or private network IP only
protected-mode yes
rename-command FLUSHALL ""
rename-command FLUSHDB ""
rename-command DEBUG ""
RabbitMQ
API Configuration
{
"MilvaionConfig": {
"RabbitMQ": {
"Host": "rabbitmq",
"Port": 5672,
"Username": "milvaion_producer",
"Password": "SECURE_RABBITMQ_PASSWORD",
"VirtualHost": "/milvaion"
}
}
}
Worker Configuration
{
"Worker": {
"RabbitMQ": {
"Host": "rabbitmq",
"Port": 5672,
"Username": "milvaion_consumer",
"Password": "SECURE_RABBITMQ_PASSWORD",
"VirtualHost": "/milvaion"
}
}
}
Virtual Host Isolation
Create a dedicated virtual host for Milvaion:
# Create virtual host
rabbitmqctl add_vhost /milvaion
# Create users with specific permissions
rabbitmqctl add_user milvaion_producer SECURE_PASSWORD
rabbitmqctl set_permissions -p /milvaion milvaion_producer ".*" ".*" ".*"
rabbitmqctl add_user milvaion_consumer SECURE_PASSWORD
rabbitmqctl set_permissions -p /milvaion milvaion_consumer ".*" ".*" ".*"
# Remove default guest user
rabbitmqctl delete_user guest
Disable Management UI in Production
# Disable management plugin entirely
rabbitmq-plugins disable rabbitmq_management
# Or restrict via environment variable
RABBITMQ_MANAGEMENT_LISTENER_IP=127.0.0.1
Secrets Management
Environment Variables
Override sensitive configuration with environment variables:
# docker-compose.yml
services:
milvaion-api:
environment:
- ConnectionStrings__DefaultConnectionString=Host=postgres;...;Password=${DB_PASSWORD}
- Milvasoft__Identity__Token__SymmetricPublicKey=${JWT_SECRET}
- MilvaionConfig__Redis__Password=${REDIS_PASSWORD}
- MilvaionConfig__RabbitMQ__Password=${RABBITMQ_PASSWORD}
- MILVAION_ROOT_PASSWORD=${ROOT_PASSWORD}
worker:
environment:
- Worker__Redis__Password=${REDIS_PASSWORD}
- Worker__RabbitMQ__Password=${RABBITMQ_PASSWORD}
Docker Secrets
For Docker Swarm deployments:
services:
milvaion-api:
secrets:
- db_password
- jwt_secret
- redis_password
- rabbitmq_password
secrets:
db_password:
file: ./secrets/db_password.txt
jwt_secret:
file: ./secrets/jwt_secret.txt
redis_password:
file: ./secrets/redis_password.txt
rabbitmq_password:
file: ./secrets/rabbitmq_password.txt
Kubernetes Secrets
apiVersion: v1
kind: Secret
metadata:
name: milvaion-secrets
namespace: milvaion
type: Opaque
stringData:
db-password: "your-secure-db-password"
jwt-secret: "your-256-bit-jwt-secret"
redis-password: "your-redis-password"
rabbitmq-password: "your-rabbitmq-password"
Reference in deployment:
env:
- name: MilvaionConfig__Redis__Password
valueFrom:
secretKeyRef:
name: milvaion-secrets
key: redis-password
Logging Security
Log Level Configuration
Reduce sensitive information in logs for production:
{
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft.AspNetCore.Authentication": "Warning",
"Microsoft.AspNetCore.Authorization": "Warning",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning"
}
}
}
}
Seq Log Server Security
# docker-compose.yml
seq:
environment:
- SEQ_FIRSTRUN_ADMINPASSWORD=${SEQ_ADMIN_PASSWORD}
- ACCEPT_EULA=Y
⚠️ Important: Change the Seq admin password immediately after first deployment.
Worker Security
Resource Limits
Prevent resource exhaustion attacks:
# docker-compose.yml
worker:
deploy:
resources:
limits:
cpus: '2'
memory: 2048M
reservations:
cpus: '0.5'
memory: 512M
Worker Configuration
{
"Worker": {
"WorkerId": "worker-01",
"MaxParallelJobs": 32,
"ExecutionTimeoutSeconds": 300
}
}
| Setting | Description | Security Impact |
|---|---|---|
MaxParallelJobs | Limit concurrent executions | Prevents resource exhaustion |
ExecutionTimeoutSeconds | Kill long-running jobs | Prevents hung processes |
Network Security
Reverse Proxy Configuration (nginx)
server {
listen 443 ssl http2;
server_name milvaion.your-domain.com;
ssl_certificate /etc/ssl/certs/milvaion.crt;
ssl_certificate_key /etc/ssl/private/milvaion.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;
# Security headers
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
location / {
proxy_pass http://milvaion-api:5000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Firewall Rules
Only expose necessary ports:
| Service | Port | Exposure |
|---|---|---|
| Milvaion API | 5000 | Via reverse proxy only |
| PostgreSQL | 5432 | Private network only |
| Redis | 6379 | Private network only |
| RabbitMQ | 5672 | Private network only |
| RabbitMQ Management | 15672 | Disabled or internal only |
| Seq | 5341 | Internal only |
Security Checklist
Pre-Production
- Change all default passwords (PostgreSQL, Redis, RabbitMQ, Seq)
- Generate new JWT signing key (256-bit random)
- Enable SSL/TLS for database connections
- Set strong password policies
- Configure account lockout
- Set
MILVA_ENV=prodenvironment variable- This will disable open api client and developer endpoints etc.
Infrastructure
- Place databases on private networks
- Configure reverse proxy with TLS termination
- Set up firewall rules
- Disable RabbitMQ management UI or restrict access
- Enable Redis protected mode
- Remove default
guestuser from RabbitMQ
Monitoring
- Set up alerts for failed login attempts
- Monitor worker health anomalies
- Configure log retention policies
- Enable database backup monitoring
Regular Maintenance
- Rotate secrets quarterly
- Update container images for security patches
- Review activity logs regularly
- Audit user permissions
- Test backup restoration procedures
Incident Response
Signs of Compromise
| Indicator | Action |
|---|---|
| Multiple failed logins from same IP | Review logs, consider IP blocking |
| Unexpected admin user creation | Disable account, investigate |
| Unusual job execution patterns | Review job logs and payloads |
| Database connection spikes | Check for injection attempts |
| Worker heartbeat anomalies | Investigate worker health |
Response Steps
- Contain - Isolate affected systems
- Identify - Review logs in Seq/Grafana
- Eradicate - Remove threat, rotate all credentials
- Recover - Restore from known-good backups
- Document - Record incident details and lessons learned