Security Hardening Guide
Last Updated: 2026-02-14 Status: Implemented Plan Reference: 021-security-hardening.md, 073-security-improvement.md
Overview
The Security Hardening system implements comprehensive security measures to protect the portal from common vulnerabilities. It includes authentication security, data protection, input validation, CSRF protection, security monitoring, a real-time security dashboard, and comprehensive audit reporting.
Table of Contents
- Security Features
- Security Dashboard
- Security Events
- Audit Reports
- Authentication Security
- Data Protection
- Input Validation
- Security Headers
- Technical Architecture
- Related Features
Security Features
Overview
| Feature | Protection Against |
|---|---|
| CSRF Tokens | Cross-site request forgery |
| XSS Protection | Cross-site scripting |
| SQL Injection | Database injection attacks |
| Password Hashing | Credential theft |
| Rate Limiting | Brute force attacks |
| 2FA | Account takeover |
| Session Security | Session hijacking |
| Encryption | Data theft |
| Security Events | Activity tracking |
| Security Dashboard | Real-time monitoring |
| Audit Reports | Compliance verification |
Security Dashboard
Overview
The Security Dashboard provides a centralized view of your system's security posture. Access it at Admin > Security > Dashboard.
Dashboard Metrics
| Metric | Description |
|---|---|
| Security Score | Overall security health (0-100) |
| Failed Logins | Failed login attempts in last 24h |
| Blocked IPs | Currently blocked IP addresses |
| Active Sessions | Currently active user sessions |
| 2FA Adoption | Percentage of admins with 2FA enabled |
| Open Alerts | Unresolved security alerts |
| Critical Alerts | High-priority alerts needing attention |
Security Score Calculation
The security score (0-100) is calculated based on:
- 2FA Adoption Rate: Lower adoption = lower score
- Critical Events: Each critical event deducts 5 points (max 30)
- Failed Logins: >50/day = -10 points, >20/day = -5 points
Dashboard Routes
// Dashboard views
Route::get('/admin/security/dashboard', [SecurityDashboardController::class, 'index']);
Route::get('/admin/security/dashboard/metrics', [SecurityDashboardController::class, 'metrics']);
Route::get('/admin/security/dashboard/events', [SecurityDashboardController::class, 'recentEvents']);
// IP Management
Route::post('/admin/security/dashboard/block-ip', [SecurityDashboardController::class, 'blockIp']);
Route::post('/admin/security/dashboard/unblock-ip', [SecurityDashboardController::class, 'unblockIp']);
Security Events
Overview
Security Events track all security-related activities in your application, providing a comprehensive audit trail for security monitoring and compliance.
Event Types
| Event Type | Severity | Description |
|---|---|---|
login_success | Info | Successful user login |
failed_login | Warning | Failed login attempt |
logout | Info | User logout |
password_reset | Info | Password reset requested |
password_changed | Info | Password changed |
two_factor_enabled | Info | 2FA enabled |
two_factor_disabled | Warning | 2FA disabled |
two_factor_failed | Warning | 2FA verification failed |
ip_blocked | Critical | IP address blocked |
brute_force_attempt | Critical | Brute force detected |
unusual_location | Warning | Login from unusual location |
suspicious_user_agent | Warning | Suspicious user agent |
multiple_failed_logins | Warning | Multiple failed logins |
Logging Events
use App\Models\SecurityEvent;
// Log a basic event
SecurityEvent::log(
SecurityEvent::TYPE_LOGIN_SUCCESS,
$user->id,
['method' => 'password']
);
// Log a critical event
SecurityEvent::logCritical(
SecurityEvent::TYPE_BRUTE_FORCE,
null,
['attempts' => 10, 'ip' => $request->ip()]
);
// Log a warning event
SecurityEvent::logWarning(
SecurityEvent::TYPE_LOGIN_FAILED,
null,
['email' => $email]
);
// Convenience methods
SecurityEvent::logFailedLogin($email);
SecurityEvent::logSuccessfulLogin($userId);
Querying Events
// Get recent events
$events = SecurityEvent::recent(24)->get();
// Get critical events
$critical = SecurityEvent::critical()->get();
// Get events by type
$failed = SecurityEvent::ofType(SecurityEvent::TYPE_LOGIN_FAILED)->get();
// Get events from specific IP
$fromIp = SecurityEvent::fromIp('192.168.1.1')->get();
// Get events for a user
$userEvents = SecurityEvent::forUser($userId)->get();
SecurityMetricsService
use App\Services\SecurityMetricsService;
$metrics = app(SecurityMetricsService::class);
// Get various metrics
$failedLogins = $metrics->getFailedLoginAttempts(24);
$blockedIps = $metrics->getBlockedIps();
$suspiciousActivity = $metrics->getSuspiciousActivity(24);
$activeSessions = $metrics->getActiveSessionsCount();
$twoFactorRate = $metrics->getTwoFactorAdoptionRate();
$securityScore = $metrics->getSecurityScore();
// Check if IP should be blocked
if ($metrics->shouldBlockIp($ip, maxAttempts: 5, minutes: 15)) {
// Block the IP
}
Audit Reports
Overview
Security Audit Reports provide comprehensive analysis of your security posture over configurable time periods. Access at Admin > Security > Audit Reports.
Report Sections
- Summary: Overall security score, total events, critical/warning counts
- Authentication: Login success/failure rates, 2FA adoption, password changes
- Threats: Brute force attempts, blocked IPs, suspicious activity
- Compliance: 2FA compliance, inactive users, session security
- Alerts: Alert counts, resolution times, false positive rates
- Recommendations: Actionable security improvements
Generating Reports
use App\Services\SecurityAuditReportService;
$service = app(SecurityAuditReportService::class);
// Generate a 30-day report
$report = $service->generateReport(
now()->subDays(30),
now()
);
// Access report sections
$summary = $report['summary'];
$threats = $report['threats'];
$recommendations = $report['recommendations'];
Export Formats
| Format | Route | Description |
|---|---|---|
| HTML | /admin/security/audit | Interactive view |
| JSON | /admin/security/audit/export/json | Programmatic access |
| CSV | /admin/security/audit/export/csv | Spreadsheet import |
Sample Report Output
{
"period": {
"start": "2026-01-01",
"end": "2026-01-13",
"days": 13
},
"summary": {
"security_score": 85,
"total_events": 1250,
"critical_events": 5,
"warning_events": 45,
"two_factor_adoption": 75.0
},
"threats": {
"brute_force_attempts": 3,
"blocked_ips": 2,
"total_threats": 8
},
"recommendations": [
{
"priority": "high",
"category": "2FA",
"message": "Only 75% of admin users have 2FA enabled."
}
]
}
Authentication Security
Password Policy
| Requirement | Setting |
|---|---|
| Minimum length | 8 characters |
| Complexity | Mixed case, numbers recommended |
| Hashing | Bcrypt (cost 10) |
| History | Cannot reuse last 5 |
Two-Factor Authentication
See Authentication Guide for 2FA setup.
Session Security
| Setting | Value | Purpose |
|---|---|---|
session.secure | true (prod) | HTTPS only |
session.http_only | true | No JS access |
session.same_site | lax | CSRF protection |
session.lifetime | 120 min | Auto logout |
Login Security
// Rate limiting (5 attempts per minute)
RateLimiter::for('login', function (Request $request) {
return Limit::perMinute(5)->by($request->email . $request->ip());
});
Failed Login Tracking
// Log failed attempts
Event::listen(Failed::class, function ($event) {
Log::warning('Failed login attempt', [
'email' => $event->credentials['email'],
'ip' => request()->ip(),
]);
});
Data Protection
Encryption
At Rest:
// Encrypted model attributes
protected $casts = [
'ssn' => 'encrypted',
'api_key' => 'encrypted',
];
In Transit:
- HTTPS enforced in production
- TLS 1.2+ required
- HSTS header enabled
Sensitive Data Handling
| Data Type | Protection |
|---|---|
| Passwords | Bcrypt hash |
| API keys | Encrypted |
| PII | Encrypted at rest |
| Tokens | Hashed, time-limited |
File Security
// Files stored outside webroot
'disks' => [
'secure' => [
'driver' => 'local',
'root' => storage_path('app/secure'),
'visibility' => 'private',
],
],
Input Validation
Form Requests
class ClientRequest extends FormRequest
{
public function rules(): array
{
return [
'email' => 'required|email|max:255',
'company_name' => 'required|string|max:255',
'phone' => 'nullable|string|max:50',
'website' => 'nullable|url|max:255',
];
}
}
Sanitization
// Clean user input
$name = strip_tags($request->input('name'));
$html = clean($request->input('content')); // HTMLPurifier
SQL Injection Prevention
// Always use parameter binding
$users = DB::select('SELECT * FROM users WHERE email = ?', [$email]);
// Use Eloquent (auto-escaped)
$user = User::where('email', $email)->first();
XSS Prevention
{{-- Auto-escaped output --}}
{{ $user->name }}
{{-- Unescaped (use carefully) --}}
{!! $trustedHtml !!}
Security Headers
Configured Headers
The SecurityHeaders middleware (app/Http/Middleware/SecurityHeaders.php) sets the following headers on every response:
| Header | Value | Purpose |
|---|---|---|
Content-Security-Policy | Nonce-based (see below) | Controls allowed content sources |
X-Content-Type-Options | nosniff | Prevents MIME sniffing |
X-Frame-Options | SAMEORIGIN | Prevents clickjacking |
X-XSS-Protection | 1; mode=block | Legacy XSS filter |
Referrer-Policy | strict-origin-when-cross-origin | Controls referrer info |
Permissions-Policy | geolocation=(), camera=(), microphone=(), payment=() | Disables unused APIs |
Strict-Transport-Security | max-age=31536000; includeSubDomains | HSTS (production only) |
Content Security Policy (CSP)
CSP uses nonce-based script authorization instead of unsafe-inline:
// A unique nonce is generated per request
$nonce = base64_encode(random_bytes(16));
View::share('cspNonce', $nonce);
// CSP directives
"script-src 'self' 'nonce-{$nonce}' {cdn_urls} {analytics_urls}"
"style-src 'self' 'unsafe-inline' {font_urls}"
All inline <script> tags must include nonce="{{ $cspNonce }}":
<script nonce="{{ $cspNonce }}">
// Your inline JavaScript
</script>
Key points:
unsafe-evalandunsafe-inlineare NOT used for scriptsunsafe-inlineis still used for styles (required by Tailwind/Alpine)- CDN URLs (jsdelivr), analytics (Google), and font URLs are allowlisted
- Local development adds Vite dev server and WebSocket URLs
- Never use inline event handlers (
onclick=,onchange=,onkeydown=, etc.) — they are blocked by CSP. Use Alpine.js directives instead (@click,@change,@keydown, etc.). Use$elinstead ofthisand$eventinstead ofeventin Alpine expressions.
File Upload Content Validation
The ValidFileContent rule (app/Rules/ValidFileContent.php) validates file uploads by checking magic bytes:
| File Type | Magic Bytes | Extension |
|---|---|---|
%PDF | .pdf | |
| PNG | \x89PNG | .png |
| JPEG | \xFF\xD8\xFF | .jpg, .jpeg |
| GIF | GIF87a or GIF89a | .gif |
This prevents spoofed files (e.g., a PHP script renamed to .pdf) from being uploaded.
HTTPS Enforcement
// Force HTTPS in production
if (app()->environment('production')) {
URL::forceScheme('https');
}
CSRF Protection
Token Validation
<form method="POST">
@csrf
<!-- Form fields -->
</form>
API Token Authentication
// API routes use Sanctum tokens instead of CSRF
Route::middleware('auth:sanctum')->group(function () {
// API routes
});
Security Monitoring
Logging Security Events
| Event | Logged Data |
|---|---|
| Login success | User, IP, timestamp |
| Login failure | Email, IP, timestamp |
| Password change | User, timestamp |
| Permission denied | User, resource, action |
| Suspicious activity | Details, IP |
Alerting
// Alert on suspicious activity
if ($failedAttempts > 10) {
Notification::route('mail', config('security.alert_email'))
->notify(new SuspiciousActivityAlert($details));
}
Audit Trail
See Activity Logging and Audit Compliance.
Technical Architecture
Security Middleware
Location: app/Http/Middleware/
| Middleware | Purpose |
|---|---|
SecurityHeaders | Add security headers |
ValidateSignature | Verify signed URLs |
ThrottleRequests | Rate limiting |
VerifyCsrfToken | CSRF protection |
EncryptCookies | Cookie encryption |
Security Service
Location: app/Services/SecurityService.php
class SecurityService
{
public function hashPassword(string $password): string
{
return Hash::make($password);
}
public function verifyPassword(string $password, string $hash): bool
{
return Hash::check($password, $hash);
}
public function generateSecureToken(int $length = 32): string
{
return bin2hex(random_bytes($length));
}
public function encryptData(string $data): string
{
return Crypt::encryptString($data);
}
public function decryptData(string $encrypted): string
{
return Crypt::decryptString($encrypted);
}
}
Configuration
// config/security.php
return [
'password_min_length' => 8,
'session_lifetime' => 120,
'max_login_attempts' => 5,
'lockout_duration' => 60, // seconds
'require_2fa_for_admin' => true,
'alert_email' => env('SECURITY_ALERT_EMAIL'),
];
Security Checklist
Authentication
- Strong password policy
- Two-factor authentication
- Session timeout
- Login rate limiting
- Failed login logging
Data Protection
- HTTPS enforced
- Data encrypted at rest
- Secure cookie settings
- Files outside webroot
Application
- CSRF protection
- XSS prevention
- SQL injection prevention
- Security headers
- Input validation
Monitoring
- Security event logging
- Audit trail
- Alert notifications
- Regular reviews
Best Practices
For Users
- Use strong passwords with mixed characters
- Enable 2FA for your account
- Log out when using shared devices
- Report suspicious activity
For Developers
- Never trust user input - validate everything
- Use parameterized queries - no string concatenation
- Escape output - use Blade's
{{ }} - Keep dependencies updated
- Follow least privilege principle
Troubleshooting
| Issue | Solution |
|---|---|
| CSRF token mismatch | Clear browser cookies |
| Session expires quickly | Check session config |
| Can't upload files | Check file type restrictions |
| Access denied errors | Verify user permissions |
Related Features
Dependencies
| Feature | Relationship |
|---|---|
| Authentication | Login security |
| Authorization | Access control |
Complementary Features
| Feature | Description |
|---|---|
| Activity Logging | Security logs |
| Audit Compliance | Compliance |
| Admin Tools | Security tools |
See Also
- Authentication - Login security
- Authorization - Permissions
- Audit Compliance - Compliance