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:
docker-compose exec app flask db upgrade
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 dbGet user and role
user = AdminUser.query.filterby(username='yourusername').first()
adminrole = Role.query.filterby(name='admin').first()Assign role
user.roleid = adminrole.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
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
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 appcore.auth.roles import requirepermission@app.route('/admin/users')
@requirepermission('system.manageusers')
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 appcore.auth.roles import requireanypermission, requireall_permissionsRequire ANY of these permissions
@requireanypermission('alerts.view', 'alerts.create')
def alerts_page():
passRequire ALL of these permissions
@requireallpermissions('alerts.delete', 'system.configure')
def critical_operation():
pass
Checking Permissions Programmatically
from appcore.auth.roles import haspermissionif 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 dbCreate 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.enrolled/mfa.disabledmfa.verify.success/mfa.verify.failuremfa.backup_code.used
user.created/user.updated/user.deleteduser.activated/user.deactivateduser.role.changeduser.password.changed
role.created/role.updated/role.deletedpermission.granted/permission.revoked
eas.broadcast/eas.manual_activation/eas.cancellationconfig.updatedgpio.activated/gpio.deactivatedalert.deleted/log.exported/log.deleted
security.permission_deniedsecurity.invalid_tokensecurity.ratelimitexceeded
Viewing Audit Logs
Via API
Recent logs (last 30 days)
curl 'http://localhost:5000/security/audit-logs?days=30' \
-b cookies.txtFilter by user
curl 'http://localhost:5000/security/audit-logs?user_id=1' \
-b cookies.txtFilter by action
curl 'http://localhost:5000/security/audit-logs?action=auth.login.failure' \
-b cookies.txtOnly 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, AuditActionSimple logging
AuditLogger.log(
action=AuditAction.CONFIG_UPDATED,
resourcetype='audiosource',
resource_id='1',
details={'field': 'volume', 'oldvalue': 0.8, 'newvalue': 0.9}
)Convenience methods
AuditLogger.logloginsuccess(user_id, username)
AuditLogger.logpermissiondenied(user_id, username, 'system.configure', '/admin/settings')
Retention Management
Clean up old audit logs:
from app_core.auth.audit import AuditLoggerDelete logs older than 90 days
deletedcount = AuditLogger.cleanupold_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.txtPermission 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['SESSIONCOOKIESECURE'] = True # HTTPS only
app.config['SESSIONCOOKIEHTTPONLY'] = True # Prevent JavaScript access
app.config['SESSIONCOOKIESAMESITE'] = 'Lax' # CSRF protection
app.config['PERMANENTSESSIONLIFETIME'] = 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 redef 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
pgdump -U postgres -d easstation -t adminusers -t roles -t permissions -t auditlogs > 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 ForbiddenSolution: Assign a role to the userfrom app_core.models import AdminUser
from app_core.auth.roles import Role
from app_core.extensions import dbuser = AdminUser.query.filter_by(username='username').first()
adminrole = Role.query.filterby(name='admin').first()
user.roleid = adminrole.id
db.session.commit()
MFA Enrollment Fails
Symptom: "pyotp is required" errorSolution: Ensure dependencies are installedpip install pyotp==2.9.0 qrcode==8.0
Or rebuild Docker container
docker-compose build app
QR Code Not Displaying
Symptom: 500 error when accessing/security/mfa/enroll/qrSolution: Check that Pillow is installedpip install Pillow==10.4.0
Permission Decorator Not Working
Symptom: Routes accessible even without permissionSolution: Ensure user has a role assigned and role has the required permissionfrom appcore.auth.roles import haspermissionDebug permission check
userid = session.get('userid')
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 rapidlySolution: Implement scheduled cleanupAdd to scheduled task (cron/celery)
from app_core.auth.audit import AuditLoggerKeep 90 days of logs
AuditLogger.cleanupoldlogs(days=90)
Session Expires During MFA
Symptom: "Session expired. Please log in again." after entering passwordSolution: 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/AuthenticationCheatSheet.html
- 47 CFR Part 11: FCC EAS Regulations (compliance requirements)
Support
For security issues or questions:
- Review audit logs for suspicious activity
- Check system logs:
docker-compose logs app - Contact: security@eas-station.example.com (update with your contact)
- File GitHub issue: https://github.com/your-repo/eas-station/issues
This document is served from docs/security/SECURITY.md in the EAS Station installation.