You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
docuseal/docs/architecture/security.md

1112 lines
25 KiB

# Security Architecture - FloDoc
**Document**: Security Design & Implementation
**Version**: 1.0
**Last Updated**: 2026-01-14
**Compliance**: POPIA, OWASP Top 10
---
## 🛡️ Security Overview
FloDoc implements defense-in-depth security across all layers. The system handles sensitive student PII and must comply with South African POPIA (Protection of Personal Information Act).
**Security Principles**:
1. **Least Privilege**: Users only access what they need
2. **Defense in Depth**: Multiple security layers
3. **Zero Trust**: Verify everything, trust nothing
4. **Audit Everything**: Complete audit trail
5. **Secure by Default**: Safe defaults, opt-in for risky features
---
## 🔐 Authentication
### 1. TP Portal (Devise + JWT)
**Authentication Flow**:
```
User Login (Email/Password)
Devise Validates
2FA Check (if enabled)
JWT Token Generated
Token Stored (HTTP-only cookie or localStorage)
Subsequent Requests Include Token
```
**Implementation**:
```ruby
# app/models/user.rb
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable,
:two_factor_authenticatable,
:otp_secret_encryption_key => ENV['OTP_SECRET_KEY']
# JWT token generation
def generate_jwt
payload = {
user_id: id,
exp: 7.days.from_now.to_i,
role: role
}
JWT.encode(payload, ENV['JWT_SECRET_KEY'], 'HS256')
end
def valid_otp?(code)
return true unless otp_required_for_login?
super
end
end
```
**Token Storage**:
```javascript
// Frontend - Secure storage
// Option 1: HTTP-only cookie (more secure)
// Option 2: Memory (less persistent but secure)
// Recommended: Memory storage with refresh
const authStore = defineStore('auth', {
state: () => ({
token: null,
user: null
}),
actions: {
async login(email, password) {
const response = await axios.post('/api/v1/auth/token', { email, password })
this.token = response.data.token
// Store in memory only
axios.defaults.headers.common['Authorization'] = `Bearer ${this.token}`
},
async refresh() {
if (this.isTokenExpiringSoon) {
const response = await axios.post('/api/v1/auth/refresh')
this.token = response.data.token
}
}
},
getters: {
isTokenExpiringSoon: (state) => {
if (!state.token) return false
const decoded = jwtDecode(state.token)
const expiry = decoded.exp * 1000
return expiry - Date.now() < 5 * 60 * 1000 // 5 minutes
}
}
})
```
**Session Management**:
- Token expiry: 7 days
- Auto-refresh: 5 minutes before expiry
- Logout: Token invalidation on server
- Concurrent session limit: 3 sessions per user
---
### 2. Student Portal (Ad-hoc Token Authentication)
**Authentication Flow**:
```
TP Creates Enrollment
Generate Unique Token (32 bytes)
Send Email with Link: /enroll/:token
Student Clicks Link
Token Validated (1-time use, expires in 7 days)
Session Established (no account needed)
```
**Token Generation**:
```ruby
# app/models/cohort_enrollment.rb
class CohortEnrollment < ApplicationRecord
before_create :generate_secure_token
def generate_secure_token
self.token = SecureRandom.urlsafe_base64(32)
self.token_expires_at = 7.days.from_now
end
def valid_token?(provided_token)
return false if token_used?
return false if token_expires_at < Time.current
provided_token == token
end
def mark_token_used!
update!(token_used: true, token_used_at: Time.current)
end
end
```
**Token Security**:
- **Length**: 256-bit (32 bytes URL-safe base64)
- **Entropy**: High (SecureRandom)
- **Expiry**: 7 days from creation
- **One-time use**: Marked as used after first access
- **Revocation**: Can be regenerated by TP admin
**Email Template**:
```erb
<!-- app/views/cohort_mailer/enrollment_invitation.html.erb -->
<p>You've been invited to enroll in <%= @cohort.name %></p>
<p>
<%= link_to 'Click here to enroll',
student_enroll_url(token: @enrollment.token) %>
</p>
<p>
This link expires in 7 days and can only be used once.<br>
If you didn't request this, please ignore this email.
</p>
```
---
### 3. Sponsor Portal (Single Email Token)
**Authentication Flow**:
```
Cohort Reaches Sponsor Phase
Generate Single Token for Bulk Signing
Send Email to Sponsor
Sponsor Clicks Link: /sponsor/:token
Token Validated (expires in 3 days)
Bulk Signing Interface
```
**Special Characteristics**:
- **Single Email Rule**: One email per cohort, not per student
- **Bulk Operations**: Sign once for all students
- **Short Expiry**: 3 days (more sensitive)
- **Read-Only Preview**: Can view before signing
**Implementation**:
```ruby
# app/models/cohort.rb
class Cohort < ApplicationRecord
def generate_sponsor_token
self.sponsor_token = SecureRandom.urlsafe_base64(32)
self.sponsor_token_expires_at = 3.days.from_now
save!
end
def sponsor_token_valid?
return false if sponsor_token.blank?
sponsor_token_expires_at > Time.current
end
def send_sponsor_invitation
generate_sponsor_token
CohortMailer.sponsor_invitation(self).deliver_later
end
end
```
---
## 🔑 Authorization
### 1. Role-Based Access Control (RBAC)
**Roles**:
- `tp_admin`: Full access to TP portal
- `tp_user`: Limited access (view only)
- `student`: Ad-hoc access to own enrollment
- `sponsor`: Ad-hoc access to cohort signing
**Ability Class**:
```ruby
# app/models/ability.rb
class Ability
include CanCanCan
def initialize(user)
return unless user.present?
if user.tp_admin?
can :manage, :all
cannot :manage, User unless user.super_admin?
elsif user.tp_user?
can :read, Cohort, institution_id: user.institution_id
can :manage, CohortEnrollment, cohort: { institution_id: user.institution_id }
end
end
end
```
**Controller Authorization**:
```ruby
# app/controllers/tp/cohorts_controller.rb
class tp::CohortsController < ApplicationController
before_action :authenticate_user!
load_and_authorize_resource
def index
@cohorts = current_institution.cohorts
end
def create
@cohort = current_institution.cohorts.new(cohort_params)
if @cohort.save
redirect_to tp_cohort_path(@cohort)
else
render :new
end
end
end
```
---
### 2. Ad-hoc Token Authorization
**Token-Based CanCanCan**:
```ruby
# app/controllers/student/enrollment_controller.rb
class Student::EnrollmentController < ApplicationController
skip_before_action :authenticate_user!
before_action :validate_token
def show
# @enrollment set by before_action
end
private
def validate_token
@enrollment = CohortEnrollment.find_by(token: params[:token])
unless @enrollment&.valid_token?(params[:token])
redirect_to root_path, alert: 'Invalid or expired link'
return
end
# Auto-mark as used after first access
@enrollment.mark_token_used! unless @enrollment.token_used?
end
end
```
**Authorization Matrix**:
| Action | TP Admin | TP User | Student | Sponsor |
|--------|----------|---------|---------|---------|
| Create Cohort | ✅ | ❌ | ❌ | ❌ |
| View Cohorts | ✅ | ✅ | ❌ | ❌ |
| Manage Enrollments | ✅ | ✅ | ❌ | ❌ |
| Enroll Self | ❌ | ❌ | ✅ (token) | ❌ |
| Upload Documents | ❌ | ❌ | ✅ (token) | ❌ |
| View Cohort (Sponsor) | ❌ | ❌ | ❌ | ✅ (token) |
| Bulk Sign | ❌ | ❌ | ❌ | ✅ (token) |
| Finalize Cohort | ✅ | ❌ | ❌ | ❌ |
---
## 🔒 Data Security
### 1. Encryption at Rest
**Database Encryption**:
```ruby
# app/models/cohort_enrollment.rb
class CohortEnrollment < ApplicationRecord
# Encrypt sensitive fields
encrypts :student_email
encrypts :student_name
encrypts :student_surname
encrypts :student_id
# Rails 7+ built-in encryption
# Requires: ENCRYPTION_KEY in environment
end
```
**Configuration**:
```ruby
# config/initializers/active_record_encryption.rb
Rails.application.config.active_record.encryption.primary_key = ENV['ENCRYPTION_PRIMARY_KEY']
Rails.application.config.active_record.encryption.deterministic_key = ENV['ENCRYPTION_DETERMINISTIC_KEY']
Rails.application.config.active_record.encryption.key_derivation_salt = ENV['ENCRYPTION_SALT']
```
**Key Management**:
- Use environment variables (never commit keys)
- Rotate keys periodically
- Backup keys securely
- Use AWS KMS or similar for production
---
### 2. File Upload Security
**Active Storage Configuration**:
```ruby
# config/storage.yml
local:
service: Disk
root: <%= Rails.root.join("storage") %>
minio:
service: S3
access_key_id: <%= ENV['AWS_ACCESS_KEY_ID'] %>
secret_access_key: <%= ENV['AWS_SECRET_ACCESS_KEY'] %>
region: us-east-1
endpoint: <%= ENV['AWS_ENDPOINT_URL'] %>
bucket: <%= ENV['AWS_BUCKET_NAME'] %>
```
**Upload Validation**:
```ruby
# app/models/cohort_enrollment.rb
class CohortEnrollment < ApplicationRecord
has_many_attached :documents
validate :documents_are_valid
private
def documents_are_valid
documents.each do |doc|
# Size limit (5MB)
if doc.byte_size > 5.megabytes
errors.add(:documents, 'must be less than 5MB')
end
# Type validation
allowed_types = ['application/pdf', 'image/jpeg', 'image/png']
unless allowed_types.include?(doc.content_type)
errors.add(:documents, 'must be PDF, JPG, or PNG')
end
# Virus scan (if ClamAV available)
if defined?(ClamAV)
clam = ClamAV.instance
scan = clam.scanfile(doc.path)
if scan.is_a?(ClamAV::VirusResponse)
errors.add(:documents, 'contains virus')
end
end
end
end
end
```
**Content Security Policy**:
```ruby
# config/initializers/content_security_policy.rb
Rails.application.config.content_security_policy do |policy|
policy.default_src :self
policy.font_src :self, :https, :data
policy.img_src :self, :https, :data
policy.object_src :none
policy.script_src :self, :https
policy.style_src :self, :https, :unsafe_inline
policy.connect_src :self, :https, 'http://localhost:3000' if Rails.env.development?
end
```
---
### 3. Input Validation & Sanitization
**Model Validations**:
```ruby
class Cohort < ApplicationRecord
validates :name, presence: true, length: { maximum: 100 }
validates :sponsor_email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :program_type, inclusion: { in: %w[learnership internship candidacy] }
# Sanitize inputs
before_validation :strip_whitespace
private
def strip_whitespace
self.name = name&.strip
self.sponsor_email = sponsor_email&.strip&.downcase
end
end
```
**Controller Parameter Sanitization**:
```ruby
class Api::V1::CohortsController < Api::V1::BaseController
def create
# Strong parameters prevent mass assignment
@cohort = current_institution.cohorts.new(cohort_params)
# ...
end
private
def cohort_params
params.require(:cohort).permit(
:name,
:program_type,
:sponsor_email,
:template_id,
required_student_uploads: [],
cohort_metadata: [:start_date, :duration_months, :stipend_amount]
)
end
end
```
**SQL Injection Prevention**:
```ruby
# ✅ Safe - Uses ActiveRecord
Cohort.where("name ILIKE ?", "%#{sanitize_sql_like(search)}%")
# ❌ Dangerous - Raw SQL with interpolation
Cohort.where("name ILIKE '%#{search}%'")
```
**XSS Prevention**:
```ruby
# Rails automatically escapes in views
<%= @cohort.name %> # Safe
# Mark as safe only if trusted
<%= raw @trusted_html %> # Only if you control the content
```
---
## 🛡️ Web Security
### 1. CSRF Protection
**Rails Default**:
```ruby
# config/initializers/csrf_protection.rb
Rails.application.config.action_controller.csrf_protection_enabled = true
```
**API Controllers**:
```ruby
class Api::V1::BaseController < ActionController::API
# APIs typically skip CSRF (use token auth instead)
skip_before_action :verify_authenticity_token
end
```
**Web Controllers**:
```ruby
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
end
```
---
### 2. CORS Configuration
```ruby
# config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins 'http://localhost:3000' if Rails.env.development?
origins 'https://flodoc.com' if Rails.env.production?
resource '/api/v1/*',
headers: :any,
methods: [:get, :post, :put, :patch, :delete, :options, :head],
credentials: false,
expose: ['Authorization']
end
end
```
---
### 3. Security Headers
```ruby
# config/initializers/secure_headers.rb
Rails.application.config.middleware.use SecureHeaders::Middleware
SecureHeaders::Configuration.configure do |config|
config.csp = {
default_src: %w['self'],
script_src: %w['self' 'unsafe-inline'],
style_src: %w['self' 'unsafe-inline'],
img_src: %w['self' data:],
font_src: %w['self' https: data:],
connect_src: %w['self'],
object_src: %w['none'],
base_uri: %w['self'],
form_action: %w['self']
}
config.hsts = "max-age=#{1.year.to_i}; includeSubDomains; preload"
config.x_frame_options = "DENY"
config.x_content_type_options = "nosniff"
config.x_xss_protection = "1; mode=block"
config.referrer_policy = "strict-origin-when-cross-origin"
end
```
---
## 📝 Audit & Logging
### 1. Audit Trail
**Audit Model**:
```ruby
# app/models/audit_log.rb
class AuditLog < ApplicationRecord
belongs_to :user, optional: true
belongs_to :auditable, polymorphic: true
enum action: {
cohort_created: 0,
cohort_activated: 1,
enrollment_created: 2,
enrollment_completed: 3,
sponsor_signed: 4,
cohort_finalized: 5,
document_uploaded: 6,
token_generated: 7,
login: 8,
logout: 9
}
validates :action, :auditable, presence: true
end
```
**Audit Logger**:
```ruby
# app/controllers/concerns/auditable.rb
module Auditable
extend ActiveSupport::Concern
included do
after_action :log_action, if: :auditable_action?
end
private
def log_action
AuditLog.create!(
user: current_user,
auditable: @auditable,
action: action_name,
ip_address: request.remote_ip,
user_agent: request.user_agent,
metadata: { params: params.except(:password, :token) }
)
end
def auditable_action?
%w[create update destroy].include?(action_name)
end
end
# Usage in controllers
class tp::CohortsController < ApplicationController
include Auditable
def create
@cohort = current_institution.cohorts.new(cohort_params)
@auditable = @cohort
# ...
end
end
```
**Audit Queries**:
```ruby
# Find all actions by a user
AuditLog.where(user: user).order(created_at: :desc)
# Find all actions on a cohort
AuditLog.where(auditable: cohort)
# Find suspicious activity
AuditLog.where('created_at > ?', 1.hour.ago)
.where(action: :login)
.group(:ip_address)
.count
.select { |_, count| count > 10 }
```
---
### 2. Application Logging
```ruby
# config/environments/production.rb
config.log_level = :info
config.logger = ActiveSupport::Logger.new(STDOUT)
# Log format
config.log_tags = [:request_id]
# Filter sensitive parameters
config.filter_parameters += [:password, :token, :secret, :key]
```
**Custom Logging**:
```ruby
class CohortWorkflowService
def initialize(cohort, user)
@cohort = cohort
@user = user
@logger = Rails.logger
end
def advance_to_active
@logger.info(
"[CohortWorkflow] User #{@user.id} advancing cohort #{@cohort.id} to active"
)
if @cohort.update(status: 'active')
@logger.info("[CohortWorkflow] Success: cohort #{@cohort.id}")
true
else
@logger.error(
"[CohortWorkflow] Failed: cohort #{@cohort.id}, errors: #{@cohort.errors.full_messages}"
)
false
end
end
end
```
---
## 🚨 Incident Response
### 1. Security Incident Detection
**Suspicious Activity Alerts**:
```ruby
# app/services/security_monitor.rb
class SecurityMonitor
def self.check_suspicious_activity
# Multiple failed logins
failed_logins = AuditLog.where(
action: :login_failed,
created_at: 1.hour.ago..Time.current
).group(:ip_address).count
failed_logins.each do |ip, count|
if count > 5
SecurityMailer.suspicious_activity(ip, count).deliver_later
end
end
# Multiple token access from different IPs
token_usage = AuditLog.where(
action: :token_access,
created_at: 24.hours.ago..Time.current
).group(:token_id, :ip_address).count
# Alert if same token used from > 2 IPs
end
end
```
**Scheduled Monitoring**:
```ruby
# config/schedule.rb (using whenever gem)
every 30.minutes do
runner "SecurityMonitor.check_suspicious_activity"
end
```
---
### 2. Incident Response Plan
**Severity Levels**:
**Critical** (Immediate action):
- Unauthorized database access
- Token leak
- PII exposure
- Ransomware detected
**High** (2 hours):
- Multiple failed login attempts
- Suspicious API usage
- File upload anomalies
**Medium** (24 hours):
- Unusual traffic patterns
- Failed webhook deliveries
- Performance degradation
**Response Steps**:
1. **Contain**: Disable affected accounts/tokens
2. **Investigate**: Review audit logs
3. **Remediate**: Patch vulnerability
4. **Notify**: Inform affected parties (POPIA requirement)
5. **Document**: Create incident report
6. **Prevent**: Implement additional controls
---
## 🔒 POPIA Compliance
### 1. Data Minimization
**Principle**: Only collect what's necessary
**Implementation**:
```ruby
# app/models/cohort_enrollment.rb
class CohortEnrollment < ApplicationRecord
# Only collect required fields
validates :student_email, presence: true
validates :student_name, presence: true, if: :required_for_program?
def required_for_program?
cohort.program_type == 'learnership'
end
end
```
### 2. Purpose Limitation
**Principle**: Use data only for specified purposes
**Implementation**:
```ruby
# Clear purpose statements in UI
# app/views/student/enrollment/show.html.erb
<h2>Document Upload</h2>
<p>
Your documents will be used for:
<ul>
<li>Verification by training provider</li>
<li>Submission to sponsor for signing</li>
<li>Legal compliance with learnership agreement</li>
</ul>
</p>
```
### 3. Data Retention
**Principle**: Don't keep data longer than necessary
**Implementation**:
```ruby
# app/models/cohort.rb
class Cohort < ApplicationRecord
# Auto-archive after 2 years
scope :for_archival, -> { where('created_at < ?', 2.years.ago) }
def archive!
# Move to cold storage
# Anonymize PII
update!(status: 'archived')
end
end
# Scheduled job
class ArchiveOldDataJob < ApplicationJob
def perform
Cohort.for_archival.find_each(&:archive!)
end
end
```
### 4. Right to Access & Deletion
**Implementation**:
```ruby
# app/controllers/api/v1/user/data_controller.rb
class Api::V1::User::DataController < Api::V1::BaseController
# GET /api/v1/user/data/export
def export
data = {
profile: current_user,
enrollments: current_user.cohort_enrollments,
audit_logs: AuditLog.where(user: current_user)
}
send_data data.to_json, filename: "my-data-#{Date.current}.json"
end
# DELETE /api/v1/user/data
def delete
# Anonymize instead of hard delete for audit
current_user.anonymize!
sign_out
head :no_content
end
end
```
---
## 🛡️ OWASP Top 10 Mitigation
### A01:2021 - Broken Access Control
- ✅ Role-based authorization (CanCanCan)
- ✅ Institution isolation
- ✅ Token-based ad-hoc access
- ✅ Audit all access attempts
### A02:2021 - Cryptographic Failures
- ✅ HTTPS enforced
- ✅ Password hashing (Devise bcrypt)
- ✅ JWT signing with secrets
- ✅ Database encryption
### A03:2021 - Injection
- ✅ ActiveRecord parameterized queries
- ✅ Strong parameters
- ✅ Input validation
- ✅ SQL LIKE sanitization
### A04:2021 - Insecure Design
- ✅ Threat modeling completed
- ✅ Security by default
- ✅ Defense in depth
- ✅ Least privilege principle
### A05:2021 - Security Misconfiguration
- ✅ Secure headers
- ✅ CORS configuration
- ✅ No default credentials
- ✅ Environment-based configs
### A06:2021 - Vulnerable and Outdated Components
- ✅ Regular dependency updates
- ✅ Security scanning (bundler-audit)
- ✅ Ruby/Rails version maintained
- ✅ Vue.js security updates
### A07:2021 - Identification and Authentication Failures
- ✅ 2FA for TP users
- ✅ Token expiration
- ✅ Rate limiting
- ✅ Secure token generation
### A08:2021 - Software and Data Integrity Failures
- ✅ Signed JWT tokens
- ✅ Webhook signature verification
- ✅ Code signing in CI/CD
- ✅ Dependency lock files
### A09:2021 - Security Logging and Monitoring Failures
- ✅ Comprehensive audit logs
- ✅ Failed login tracking
- ✅ Suspicious activity alerts
- ✅ Log retention policy
### A10:2021 - Server-Side Request Forgery (SSRF)
- ✅ Webhook URL validation
- ✅ No arbitrary URL fetching
- ✅ Internal network restrictions
- ✅ URL whitelist for webhooks
---
## 🔍 Security Testing
### 1. Static Analysis
```bash
# Ruby security scanning
bundle exec brakeman
bundle exec bundler-audit
# JavaScript security scanning
npm audit
yarn audit
# Code quality
bundle exec rubocop --only security
```
### 2. Dynamic Testing
```ruby
# spec/security/authentication_spec.rb
RSpec.describe 'Authentication Security', type: :request do
it 'rejects invalid JWT tokens' do
get '/api/v1/cohorts', headers: { 'Authorization' => 'Bearer invalid' }
expect(response).to have_http_status(:unauthorized)
end
it 'prevents token reuse after logout' do
token = user.generate_jwt
user.invalidate_all_tokens!
get '/api/v1/cohorts', headers: { 'Authorization' => "Bearer #{token}" }
expect(response).to have_http_status(:unauthorized)
end
end
```
### 3. Penetration Testing Checklist
**Authentication**:
- [ ] Brute force protection
- [ ] Session fixation prevention
- [ ] Token leakage protection
- [ ] 2FA bypass attempts
**Authorization**:
- [ ] Horizontal privilege escalation
- [ ] Vertical privilege escalation
- [ ] IDOR (Insecure Direct Object Reference)
- [ ] Mass assignment
**Input Validation**:
- [ ] SQL injection
- [ ] XSS (reflected, stored, DOM)
- [ ] Command injection
- [ ] File upload bypass
**API Security**:
- [ ] Rate limiting bypass
- [ ] CORS misconfiguration
- [ ] Authentication bypass
- [ ] Information disclosure
---
## 📊 Security Metrics
**Track These Metrics**:
- Failed login attempts per day
- Token reuse attempts
- Unauthorized access attempts
- Security incidents per month
- Time to detect incidents
- Time to remediate incidents
- Dependency vulnerabilities
- Code coverage for security tests
**Dashboard**:
```ruby
# Admin dashboard security widget
class SecurityDashboard
def self.metrics
{
failed_logins: AuditLog.where(action: :login_failed, created_at: 24.hours.ago..).count,
suspicious_ips: AuditLog.where('created_at > ?', 1.hour.ago).group(:ip_address).count.select { |_, c| c > 5 },
tokens_issued: CohortEnrollment.where('created_at > ?', 24.hours.ago).count,
security_incidents: SecurityIncident.where('created_at > ?', 30.days.ago).count
}
end
end
```
---
## 🎯 Security Checklist
### Application Security
- [ ] All inputs validated
- [ ] SQL injection prevented
- [ ] XSS prevented
- [ ] CSRF protected
- [ ] Authentication required
- [ ] Authorization enforced
- [ ] Sensitive data encrypted
- [ ] Secure headers configured
- [ ] CORS properly configured
- [ ] Rate limiting implemented
### Data Security
- [ ] Database encryption enabled
- [ ] File uploads validated
- [ ] PII minimized
- [ ] Retention policy defined
- [ ] Backup encryption
- [ ] Access logging enabled
### Authentication & Authorization
- [ ] 2FA for TP users
- [ ] Token expiration
- [ ] Secure token generation
- [ ] Session management
- [ ] Role-based access control
- [ ] Institution isolation
### Monitoring & Logging
- [ ] Audit logs implemented
- [ ] Failed login tracking
- [ ] Suspicious activity alerts
- [ ] Incident response plan
- [ ] Log retention policy
- [ ] Security metrics tracked
### Compliance
- [ ] POPIA compliance
- [ ] OWASP Top 10 addressed
- [ ] Security testing performed
- [ ] Documentation complete
- [ ] Incident response ready
---
## 🔐 Environment Security
### Development
```bash
# .env.example (never commit .env)
DATABASE_URL=postgresql://localhost/flo_doc
SECRET_KEY_BASE=dev_secret
JWT_SECRET_KEY=dev_jwt_secret
ENCRYPTION_PRIMARY_KEY=dev_encryption_key
```
### Production
```bash
# Use environment variables or secrets manager
DATABASE_URL=postgresql://...
SECRET_KEY_BASE=$(rails secret)
JWT_SECRET_KEY=$(openssl rand -base64 32)
ENCRYPTION_PRIMARY_KEY=$(openssl rand -base64 32)
```
**Never commit secrets**:
```gitignore
# .gitignore
.env
.env.production
config/secrets.yml
config/master.key
```
---
## 📚 Security Resources
- **OWASP Top 10**: https://owasp.org/Top10/
- **POPIA Guide**: https://www.justice.gov.za/legislation/acts/2013-013.pdf
- **Rails Security**: https://guides.rubyonrails.org/security.html
- **Vue.js Security**: https://vuejs.org/guide/best-practices/security.html
---
**Document Status**: ✅ Complete
**Last Security Review**: 2026-01-14
**Next Review**: After Phase 1 Implementation