Security Best Practices
This guide presents security best practices for PCH-SIG administration.
Authentication
Passwords
- Minimum length: 8 characters
- Complexity: Uppercase, lowercase, numbers, symbols
- Renewal: Every 90 days
- History: Do not reuse last 5 passwords
Account management
| Action | Recommendation |
|---|---|
| Creation | One account per person |
| Sharing | Never share credentials |
| Inactivity | Disable after 90 days without login |
| Departure | Disable immediately |
Multi-factor authentication
Although not currently implemented, consider:
- TOTP application (Google Authenticator, Authy)
- SMS (less secure)
- Hardware security keys (YubiKey)
Authorizations
Principle of least privilege
- Grant only necessary permissions
- Start with restrictive access
- Add permissions if justified
Separation of duties
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Creation │────▶│ Validation │────▶│ Approval │
│ (Operator) │ │ (Manager) │ │ (Admin) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
- User who creates does not validate
- User who validates does not approve
- Critical actions require multiple people
Access review
- Frequency: Quarterly
- Checks:
- Inactive accounts
- Excessive permissions
- Obsolete roles
Data protection
Classification
| Level | Description | Examples |
|---|---|---|
| Public | Accessible to all | Documentation |
| Internal | Authenticated users | Aggregated statistics |
| Confidential | Restricted access | Beneficiary data |
| Secret | Highly restricted | Financial information |
Encryption
- In transit: HTTPS required
- At rest: Disk encryption for sensitive data
- Backups: Archive encryption
Anonymization
For exports and reports, anonymize sensitive data:
// Anonymization example
$beneficiaire = [
'id' => hash('sha256', $original['id']),
'region' => $original['region'],
'montant' => $original['montant'],
// NO name, phone, address
];
Infrastructure
Network
- Firewall: Limit exposed ports
- Segmentation: Separate environments
- VPN: For administrative access
Ports to expose
| Port | Service | Access |
|---|---|---|
| 443 | HTTPS Frontend | Public |
| 22 | SSH | VPN only |
| 5432 | PostgreSQL | Local only |
| 6379 | Redis | Local only |
Docker configuration
# Do not expose sensitive ports
services:
postgres:
# NO ports: exposed
networks:
- internal
backend:
networks:
- internal
- frontend
networks:
internal:
internal: true
frontend:
Audit and Logging
Events to log
| Category | Events |
|---|---|
| Authentication | Successful/failed logins |
| Authorization | Access denied |
| Data | Creations, modifications, deletions |
| Admin | Configuration changes |
Log retention
| Type | Duration |
|---|---|
| Security | 1 year minimum |
| Application | 90 days |
| Debug | 7 days |
Do not log
- Passwords (even hashed)
- Authentication tokens
- Complete identity document numbers
- Banking data
Backups
3-2-1 Strategy
- 3 copies of data
- 2 different media
- 1 off-site copy
Backup security
- Encrypt backups
- Store keys separately
- Test restores regularly
Secure script
#!/bin/bash
# backup-secure.sh
# Variables
BACKUP_DIR="/backups"
ENCRYPTION_KEY="/root/.backup-key"
DATE=$(date +%Y%m%d)
# Backup
pg_dump -U pch_admin pch_sig > /tmp/backup.sql
# Encryption
gpg --symmetric --cipher-algo AES256 \
--passphrase-file $ENCRYPTION_KEY \
--output $BACKUP_DIR/backup_$DATE.sql.gpg \
/tmp/backup.sql
# Cleanup
rm -f /tmp/backup.sql
Secure development
Input validation
// Always validate
$email = filter_var($input['email'], FILTER_VALIDATE_EMAIL);
$id = filter_var($input['id'], FILTER_VALIDATE_INT);
// Use Symfony constraints
use Symfony\Component\Validator\Constraints as Assert;
class User
{
#[Assert\Email]
#[Assert\NotBlank]
private string $email;
#[Assert\Length(min: 8)]
private string $password;
}
Protection against injections
// SQL - Use prepared statements
$query = $em->createQuery('SELECT m FROM Menage m WHERE m.region = :region');
$query->setParameter('region', $region);
// NEVER concatenation
// $query = "SELECT * FROM menages WHERE region = '$region'"; // DANGEROUS
XSS protection
// Escape outputs
{{ variable|e }} {# Twig escapes by default #}
// In React, use native mechanisms
<div>{variable}</div> // Escaped automatically
// NEVER
<div dangerouslySetInnerHTML={{ __html: variable }} /> // Avoid
Secrets management
Never commit
# .gitignore
.env
.env.local
config/jwt/private.pem
config/jwt/public.pem
*.key
*.pem
Environment variables
# Store secrets in environment, not in code
export DATABASE_URL="postgresql://user:password@host:5432/db"
export JWT_PASSPHRASE="secret_passphrase"
Secrets rotation
| Secret | Frequency |
|---|---|
| DB passwords | Annual |
| JWT keys | Semi-annual |
| External API keys | Per policy |
Incident response
Response plan
- Detection: Monitoring alerts
- Analysis: Assess impact
- Containment: Limit damage
- Eradication: Remove threat
- Recovery: Restore services
- Post-mortem: Analyze and improve
Emergency contacts
Maintain an up-to-date contact list:
- System administrator
- Security officer
- Development team
- Hosting support
Compromise procedure
# 1. Block access
docker stop pch_backend pch_frontend
# 2. Change passwords
docker exec pch_postgres psql -U pch_admin -c "
ALTER USER pch_admin WITH PASSWORD 'new_password';"
# 3. Regenerate tokens
docker exec pch_backend php bin/console lexik:jwt:generate-keypair --overwrite
# 4. Analyze logs
docker logs pch_backend --since "2024-01-15" > incident.log
Security checklist
Daily
- Check monitoring alerts
- Review failed login attempts
Weekly
- Check for security updates
- Analyze access logs
Monthly
- Review user access
- Test backups
- Check SSL certificates
Quarterly
- Permissions audit
- Full restore test
- Review security procedures
Annual
- Rotate system passwords
- Complete security audit
- User training
Resources
References
Tools
- Vulnerability analysis: Trivy, Snyk
- Penetration testing: OWASP ZAP
- Code audit: SonarQube