EAS Station Security Guide
Overview
EAS Station implements comprehensive security controls including Role-Based Access Control (RBAC), Multi-Factor Authentication (MFA), and detailed audit logging. This guide covers setup, configuration, and best practices for securing your EAS Station deployment.
Table of Contents
- Quick Start
- Role-Based Access Control
- Multi-Factor Authentication
- Audit Logging
- API Endpoints
- Security Best Practices
- Troubleshooting
Quick Start
1. Apply Database Migration
After pulling the latest changes, apply the security migration:
2. Initialize Default Roles
Initialize the three default roles (admin, operator, viewer) and their permissions:
curl -X POST http://localhost:5000/security/init-roles \
-H "Content-Type: application/json" \
-b cookies.txt
Or use the API endpoint after logging in as an admin.
3. Assign Roles to Users
Assign roles to existing users:
from app_core.models import AdminUser
from app_core.auth.roles import Role
from app_core.extensions import db
# Get user and role
user = AdminUser.query.filter_by(username='your_username').first()
admin_role = Role.query.filter_by(name='admin').first()
# Assign role
user.role_id = admin_role.id
db.session.commit()
Or via API:
curl -X PUT http://localhost:5000/security/users/1/role \
-H "Content-Type: application/json" \
-d '{"role_id": 1}' \
-b cookies.txt
Role-Based Access Control
Default Roles
EAS Station provides three predefined roles:
1. Admin (Full Access)
- All system permissions
- User and role management
- System configuration
- Alert and EAS operations
- Log management
- GPIO and hardware control
2. Operator (Operations Access)
- Alert viewing and creation
- EAS broadcast and manual activation
- GPIO control
- Log viewing and export
- Cannot: Manage users, modify system configuration, delete data
3. Viewer (Read-Only)
- View alerts, logs, and system status
- Export logs
- Cannot: Create/delete data, broadcast EAS, control GPIO, manage users
Permission Model
Permissions follow the format: resource.action
Resources:
alerts- CAP alerts and EAS messageseas- EAS broadcast operationssystem- System configuration and user managementlogs- System and audit logsreceivers- SDR receiversgpio- GPIO relay controlapi- API access
Actions:
view- Read accesscreate- Create new recordsdelete- Delete recordsexport- Export dataconfigure- Modify configurationcontrol- Activate/control hardwaremanage_users- User administration
Using Permission Decorators
In your route handlers:
from app_core.auth.roles import require_permission
@app.route('/admin/users')
@require_permission('system.manage_users')
def manage_users():
# Only accessible by users with system.manage_users permission
pass
@app.route('/alerts')
@require_permission('alerts.view')
def view_alerts():
# Accessible by users with alerts.view permission
pass
Multiple permission patterns:
from app_core.auth.roles import require_any_permission, require_all_permissions
# Require ANY of these permissions
@require_any_permission('alerts.view', 'alerts.create')
def alerts_page():
pass
# Require ALL of these permissions
@require_all_permissions('alerts.delete', 'system.configure')
def critical_operation():
pass
Checking Permissions Programmatically
from app_core.auth.roles import has_permission
if has_permission('eas.broadcast'):
# User can broadcast
pass
Creating Custom Roles
Via Python:
from app_core.auth.roles import Role, Permission
from app_core.extensions import db
# Create a custom role
custom_role = Role(
name='technician',
description='Technical support staff with limited access'
)
# Assign specific permissions
perms = Permission.query.filter(Permission.name.in_([
'receivers.view',
'receivers.configure',
'logs.view',
'system.view_config'
])).all()
custom_role.permissions.extend(perms)
db.session.add(custom_role)
db.session.commit()
Via API:
curl -X POST http://localhost:5000/security/roles \
-H "Content-Type: application/json" \
-d '{
"name": "technician",
"description": "Technical support role",
"permission_ids": [1, 2, 5, 8]
}' \
-b cookies.txt
Multi-Factor Authentication
Overview
EAS Station supports TOTP-based two-factor authentication compatible with:
- Google Authenticator
- Microsoft Authenticator
- Authy
- Any RFC 6238 compliant authenticator app
Enrolling in MFA
1. Start Enrollment
curl -X POST http://localhost:5000/security/mfa/enroll/start \
-b cookies.txt
Response includes:
{
"secret": "JBSWY3DPEHPK3PXP",
"provisioning_uri": "otpauth://totp/EAS%20Station:username?secret=...",
"message": "Scan the QR code with your authenticator app..."
}
2. Get QR Code
Open in browser while logged in:
http://localhost:5000/security/mfa/enroll/qr
Or via curl:
curl http://localhost:5000/security/mfa/enroll/qr \
-b cookies.txt \
-o mfa_qr.png
3. Verify Setup
Enter the 6-digit code from your authenticator app:
curl -X POST http://localhost:5000/security/mfa/enroll/verify \
-H "Content-Type: application/json" \
-d '{"code": "123456"}' \
-b cookies.txt
Response includes:
{
"success": true,
"backup_codes": [
"A1B2C3D4",
"E5F6G7H8",
...
],
"message": "MFA enrolled successfully..."
}
IMPORTANT: Save your backup codes in a secure location!
Login Flow with MFA
- Enter username and password
- If MFA is enabled, redirected to
/mfa/verify - Enter 6-digit TOTP code from authenticator app
- Alternatively, use an 8-character backup code
- Session established after successful verification
MFA Session Timeout
- Partial authentication (post-password) expires in 5 minutes
- Must complete MFA verification within timeout window
- Expired sessions require starting login process again
Backup Codes
- 10 backup codes generated during enrollment
- Each code is single-use
- 8 characters, alphanumeric (e.g., "A1B2C3D4")
- Hashed in database (bcrypt)
- Used when authenticator app unavailable
Disabling MFA
Requires password confirmation:
curl -X POST http://localhost:5000/security/mfa/disable \
-H "Content-Type: application/json" \
-d '{"password": "your_password"}' \
-b cookies.txt
Checking MFA Status
curl http://localhost:5000/security/mfa/status \
-b cookies.txt
Audit Logging
Overview
All security-sensitive operations are logged to the audit_logs table with:
- Timestamp (UTC)
- User ID and username
- Action type (e.g.,
auth.login.success,mfa.enrolled) - Resource affected (type and ID)
- IP address and user agent
- Success/failure status
- Additional details (JSON)
Audit Actions Tracked
Authentication:
auth.login.success/auth.login.failureauth.logoutauth.session.expired
MFA:
mfa.enrolled/mfa.disabledmfa.verify.success/mfa.verify.failuremfa.backup_code.used
User Management:
user.created/user.updated/user.deleteduser.activated/user.deactivateduser.role.changeduser.password.changed
Permissions:
role.created/role.updated/role.deletedpermission.granted/permission.revoked
Operations:
eas.broadcast/eas.manual_activation/eas.cancellationconfig.updatedgpio.activated/gpio.deactivatedalert.deleted/log.exported/log.deleted
Security Events:
security.permission_deniedsecurity.invalid_tokensecurity.rate_limit_exceeded
Viewing Audit Logs
Via API
# Recent logs (last 30 days)
curl 'http://localhost:5000/security/audit-logs?days=30' \
-b cookies.txt
# Filter by user
curl 'http://localhost:5000/security/audit-logs?user_id=1' \
-b cookies.txt
# Filter by action
curl 'http://localhost:5000/security/audit-logs?action=auth.login.failure' \
-b cookies.txt
# Only failed operations
curl 'http://localhost:5000/security/audit-logs?success=false' \
-b cookies.txt
Export as CSV
curl 'http://localhost:5000/security/audit-logs/export?days=90' \
-b cookies.txt \
-o audit_logs.csv
Programmatic Logging
from app_core.auth.audit import AuditLogger, AuditAction
# Simple logging
AuditLogger.log(
action=AuditAction.CONFIG_UPDATED,
resource_type='audio_source',
resource_id='1',
details={'field': 'volume', 'old_value': 0.8, 'new_value': 0.9}
)
# Convenience methods
AuditLogger.log_login_success(user_id, username)
AuditLogger.log_permission_denied(user_id, username, 'system.configure', '/admin/settings')
Retention Management
Clean up old audit logs:
from app_core.auth.audit import AuditLogger
# Delete logs older than 90 days
deleted_count = AuditLogger.cleanup_old_logs(days=90)
Recommended retention: 90 days (FCC compliance may require longer)
API Endpoints
Authentication
POST /login- Username/password authenticationPOST /mfa/verify- MFA verificationGET /logout- Sign out
MFA Management
GET /security/mfa/status- Check MFA statusPOST /security/mfa/enroll/start- Start MFA enrollmentGET /security/mfa/enroll/qr- Get QR code imagePOST /security/mfa/enroll/verify- Complete enrollmentPOST /security/mfa/disable- Disable MFA
Role Management
GET /security/roles- List all rolesGET /security/roles/<id>- Get role detailsPOST /security/roles- Create custom rolePUT /security/roles/<id>- Update roleGET /security/permissions- List all permissionsPUT /security/users/<id>/role- Assign role to user
Audit Logs
GET /security/audit-logs- List audit logs (paginated, filterable)GET /security/audit-logs/export- Export logs as CSV
Utilities
POST /security/init-roles- Initialize default rolesPOST /security/permissions/check- Check if user has permission
Security Best Practices
1. Enable MFA for All Admin Users
Require MFA for users with admin role:
# Enforce MFA policy
admin_users = AdminUser.query.join(Role).filter(Role.name == 'admin').all()
for user in admin_users:
if not user.mfa_enabled:
# Send notification to enable MFA
pass
2. Principle of Least Privilege
- Assign
viewerrole by default - Grant
operatorrole only when needed - Limit
adminrole to system administrators
3. Regular Audit Log Review
Schedule weekly reviews:
# Failed login attempts
curl 'http://localhost:5000/security/audit-logs?action=auth.login.failure&days=7' \
-b cookies.txt
# Permission denied events
curl 'http://localhost:5000/security/audit-logs?action=security.permission_denied&days=7' \
-b cookies.txt
4. Session Management
Configure secure session settings in app.py:
app.config['SESSION_COOKIE_SECURE'] = True # HTTPS only
app.config['SESSION_COOKIE_HTTPONLY'] = True # Prevent JavaScript access
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' # CSRF protection
app.config['PERMANENT_SESSION_LIFETIME'] = 3600 # 1 hour timeout
5. Network Security
- Run EAS Station behind reverse proxy (nginx/Apache)
- Enable HTTPS with valid TLS certificates
- Use firewall rules to restrict admin access
- Consider VPN for remote administration
6. Password Policies
Implement strong password requirements:
import re
def validate_password(password):
"""Enforce password complexity."""
if len(password) < 12:
return False, "Password must be at least 12 characters"
if not re.search(r'[A-Z]', password):
return False, "Password must contain uppercase letter"
if not re.search(r'[a-z]', password):
return False, "Password must contain lowercase letter"
if not re.search(r'[0-9]', password):
return False, "Password must contain number"
if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
return False, "Password must contain special character"
return True, "Valid"
7. Backup Security Data
Regularly backup:
- User accounts and roles
- Audit logs (before cleanup)
- Security configuration
pg_dump -U postgres -d eas_station -t admin_users -t roles -t permissions -t audit_logs > security_backup.sql
8. Monitor for Anomalies
Set up alerts for:
- Multiple failed login attempts
- MFA verification failures
- Permission denied events
- Unexpected role changes
- Admin account creation
Troubleshooting
User Cannot Log In After Migration
Symptom: User has no role assigned, all routes return 403 Forbidden
Solution: Assign a role to the user
from app_core.models import AdminUser
from app_core.auth.roles import Role
from app_core.extensions import db
user = AdminUser.query.filter_by(username='username').first()
admin_role = Role.query.filter_by(name='admin').first()
user.role_id = admin_role.id
db.session.commit()
MFA Enrollment Fails
Symptom: "pyotp is required" error
Solution: Ensure dependencies are installed
pip install pyotp==2.9.0 qrcode==8.0
QR Code Not Displaying
Symptom: 500 error when accessing /security/mfa/enroll/qr
Solution: Check that Pillow is installed
pip install Pillow==10.4.0
Permission Decorator Not Working
Symptom: Routes accessible even without permission
Solution: Ensure user has a role assigned and role has the required permission
from app_core.auth.roles import has_permission
# Debug permission check
user_id = session.get('user_id')
user = AdminUser.query.get(user_id)
print(f"User role: {user.role.name if user.role else None}")
print(f"Has permission: {has_permission('alerts.view')}")
Audit Logs Growing Too Large
Symptom: Database size increasing rapidly
Solution: Implement scheduled cleanup
# Add to scheduled task (cron/celery)
from app_core.auth.audit import AuditLogger
# Keep 90 days of logs
AuditLogger.cleanup_old_logs(days=90)
Session Expires During MFA
Symptom: "Session expired. Please log in again." after entering password
Solution: Complete MFA verification within 5 minutes. If consistently timing out, increase timeout:
# In app_core/auth/mfa.py
class MFASession:
TIMEOUT_MINUTES = 10 # Increase from 5 to 10
Migration Checklist
When deploying security features to existing EAS Station:
- Pull latest code from security branch
- Install new dependencies:
pip install -r requirements.txt - Run database migration:
flask db upgrade - Initialize default roles:
POST /security/init-roles - Assign roles to all existing users
- Test login with each role
- Enroll MFA for admin users
- Test MFA login flow
- Review audit log functionality
- Update documentation/procedures
- Configure log retention policy
- Set up audit log monitoring
- Enable HTTPS if not already active
- Review session timeout settings
Additional Resources
- RFC 6238: TOTP Algorithm Specification
- NIST SP 800-63B: Digital Identity Guidelines (Authentication)
- OWASP Authentication Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
- 47 CFR Part 11: FCC EAS Regulations (compliance requirements)
Support
For security issues or questions:
- Review audit logs for suspicious activity
- Contact: security@eas-station.example.com (update with your contact)
- File GitHub issue: https://github.com/your-repo/eas-station/issues
Security Vulnerabilities: Report privately via email, not public issues.
Security Features
This section describes the comprehensive security features implemented to protect against malicious login attempts, SQL injection, command injection, and brute force attacks.
1. Input Validation
Location: app_core/auth/input_validation.py
The input validator checks for:
- SQL injection patterns (
' OR 1=1,UNION SELECT, etc.) - Command injection patterns (
&&,||,;, backticks, etc.) - Non-printable characters
- Overly long inputs
All malicious inputs are:
- Rejected immediately
- Sanitized before logging
- Logged for security analysis
- Trigger automatic IP banning (24 hours)
2. Rate Limiting
Location: app_core/auth/rate_limiter.py
Configuration:
- Max attempts: 5 failed logins
- Lockout duration: 15 minutes
- Attempt window: 5 minutes
Features:
- Per-IP tracking of failed attempts
- Automatic lockout after threshold
- Cleanup of old entries
3. Flood Protection
Location: app_core/auth/ip_filter.py (FloodProtection class)
Configuration:
- Max attempts per minute: 10
- Auto-ban duration: 1 hour
Detects and automatically bans IPs making rapid-fire login attempts.
4. IP Filtering (Allowlist/Blocklist)
Location: app_core/auth/ip_filter.py
Database Table: ip_filters
Features:
- Allowlist: Trusted IPs/ranges that bypass all checks
- Blocklist: Banned IPs/ranges that cannot access the system
- CIDR range support (e.g.,
192.168.1.0/24) - Optional expiration times
- Active/inactive toggle
- Manual and automatic entries
Auto-ban Triggers:
- Malicious Input (24 hours): SQL/command injection attempt
- Brute Force (24 hours): 5 failed login attempts
- Flooding (1 hour): >10 attempts per minute
5. Security Logging
Location: app_core/auth/security_logger.py
Log File: /var/log/eas-station/security.log
Event Types:
MALICIOUS_LOGIN: SQL/command injection attemptFAILED_LOGIN: Regular failed loginRATE_LIMIT_EXCEEDED: Too many attempts
Format (fail2ban compatible):
[YYYY-MM-DD HH:MM:SS UTC] EVENT_TYPE from IP_ADDRESS username=USERNAME details=DETAILS
6. Audit Logging
Location: app_core/auth/audit.py
Database Table: audit_logs
Comprehensive audit trail of all security events including:
- Login successes/failures
- MFA events
- IP filter changes
- Permission denials
fail2ban Integration
Installation
- Install fail2ban:
sudo apt-get install fail2ban
- Configure Jail (
/etc/fail2ban/jail.local):
[eas-station-malicious]
enabled = true
port = http,https
filter = eas-station-malicious
logpath = /var/log/eas-station/security.log
maxretry = 1
bantime = 3600
findtime = 600
[eas-station-auth]
enabled = true
port = http,https
filter = eas-station-auth
logpath = /var/log/eas-station/security.log
maxretry = 5
bantime = 1800
findtime = 600
- Create Malicious Filter (
/etc/fail2ban/filter.d/eas-station-malicious.conf):
[Definition]
failregex = ^.*MALICIOUS_LOGIN from <HOST>.*$
ignoreregex =
- Create Auth Filter (
/etc/fail2ban/filter.d/eas-station-auth.conf):
[Definition]
failregex = ^.*(FAILED_LOGIN|RATE_LIMIT_EXCEEDED) from <HOST>.*$
ignoreregex =
- Restart fail2ban:
sudo systemctl restart fail2ban
Testing fail2ban
# Check status
sudo fail2ban-client status eas-station-malicious
sudo fail2ban-client status eas-station-auth
# View banned IPs
sudo fail2ban-client get eas-station-malicious banned
# Unban an IP
sudo fail2ban-client set eas-station-malicious unbanip 192.168.1.1
IP Filter Web Interface
Malicious Login Attempts Page
URL: /admin/malicious-logins
Features:
- View all malicious login attempts
- Statistics dashboard (total attempts, unique IPs, today's attempts)
- Top attacking IP addresses
- Quick ban from statistics
- fail2ban configuration viewer
IP Filter Management
Location: Same page as malicious logins
Actions:
- Add to allowlist/blocklist
- Support for single IPs or CIDR ranges
- Set expiration time (blocklist only)
- Toggle active/inactive
- Delete filters
- View reason and description
Example Allowlist Entries:
192.168.1.0/24- Internal network10.0.0.5- Admin workstation
Example Blocklist Entries:
1.2.3.4- Known attacker5.6.7.0/24- Malicious subnet
IP Filter API Endpoints
GET /security/ip-filters- List all filtersPOST /security/ip-filters- Add new filterDELETE /security/ip-filters/<id>- Delete filterPOST /security/ip-filters/<id>/toggle- Toggle active statusPOST /security/ip-filters/cleanup- Clean up expired filtersGET /security/malicious-login-attempts- List malicious attempts with IP statistics
Security Feature Configuration
Rate Limiting
Edit app_core/auth/rate_limiter.py:
MAX_ATTEMPTS = 5 # Maximum failed attempts before lockout
LOCKOUT_DURATION = timedelta(minutes=15) # How long to lock out
ATTEMPT_WINDOW = timedelta(minutes=5) # Time window to count attempts
Flood Protection
Edit app_core/auth/ip_filter.py:
MAX_ATTEMPTS_PER_MINUTE = 10 # Max attempts per minute
FLOOD_BAN_HOURS = 1 # Ban duration for flooding
Auto-Ban
Edit app_core/auth/ip_filter.py:
FAILED_ATTEMPTS_THRESHOLD = 5 # Failed attempts before auto-ban
BAN_DURATION_HOURS = 24 # Ban duration
Security Monitoring
Recommended monitoring:
- Track failed login attempts per hour
- Alert on >50 malicious attempts in 1 hour
- Alert on auto-ban triggers
- Weekly review of blocklist entries
- Monitor fail2ban ban count
Locked Out Admin
If an admin IP is accidentally blocked:
- Via Database:
DELETE FROM ip_filters WHERE ip_address = '1.2.3.4';
- Via fail2ban:
sudo fail2ban-client set eas-station-auth unbanip 1.2.3.4
Check Security Logs
tail -f /var/log/eas-station/security.log
This document is served from docs/security/SECURITY.md in the EAS Station installation.