Sécuriser une application Symfony avec Docker : Guide complet des bonnes pratiques en 2024

# Introduction La sécurité des applications web est devenue un enjeu critique dans le développement moderne. Combiner Symfony avec Docker offre une excellente

Symfony

Sécuriser une application Symfony avec Docker : Guide complet des bonnes pratiques en 2024

15/01/2024

# Introduction La sécurité des applications web est devenue un enjeu critique dans le développement moderne. Combiner Symfony avec Docker offre une excellente base pour créer des applications robustes, mais nécessite une attention particulière aux aspects sécuritaires. Dans cet article, nous allons explorer les meilleures pratiques pour sécuriser une application Symfony conteneurisée avec Docker. ## 1. Sécurisation de l'image Docker ### Utiliser des images officielles et minimales Première règle : toujours partir d'images officielles et privilégier les versions Alpine pour réduire la surface d'attaque. ```dockerfile # Mauvaise pratique FROM php:8.2 # Bonne pratique FROM php:8.2-fpm-alpine ``` ### Créer un utilisateur non-root Ne jamais exécuter l'application en tant que root : ```dockerfile FROM php:8.2-fpm-alpine # Créer un utilisateur dédié RUN addgroup -g 1000 symfony && \ adduser -D -u 1000 -G symfony symfony # Installation des dépendances RUN apk add --no-cache \ git \ zip \ unzip # Copier les fichiers avec les bons droits COPY --chown=symfony:symfony . /app WORKDIR /app # Passer à l'utilisateur non-root USER symfony EXPOSE 9000 CMD ["php-fpm"] ``` ### Scanner les vulnérabilités Intégrez Trivy dans votre pipeline CI/CD : ```yaml # .gitlab-ci.yml security:scan: stage: security image: aquasec/trivy:latest script: - trivy image --severity HIGH,CRITICAL mon-app:latest allow_failure: false ``` ## 2. Configuration sécurisée de Symfony ### Gestion des secrets avec Docker Utilisez Docker Secrets pour les données sensibles : ```yaml # docker-compose.yml version: '3.8' services: app: image: mon-app:latest secrets: - db_password - app_secret environment: DATABASE_URL: mysql://user:run/secrets/db_password@db:3306/mydb APP_SECRET_FILE: /run/secrets/app_secret secrets: db_password: external: true app_secret: external: true ``` ### Configuration du fichier .env Ne jamais commiter les fichiers .env.local en production : ```ini # .env APP_ENV=prod APP_DEBUG=0 # Ne pas mettre de valeurs sensibles ici # Utiliser des variables d'environnement Docker à la place DATABASE_URL="${DATABASE_URL}" APP_SECRET="${APP_SECRET}" ``` ### Configurer les en-têtes de sécurité Dans `config/packages/framework.yaml` : ```yaml framework: # Configuration CSP csp: enabled: true report_only: false report_uri: '/csp-report' default_src: "'self'" script_src: "'self' 'unsafe-inline'" style_src: "'self' 'unsafe-inline'" ``` Configuration Nginx pour les en-têtes de sécurité : ```nginx # nginx.conf server { listen 80; server_name example.com; root /app/public; # En-têtes de sécurité add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; # Désactiver la signature du serveur server_tokens off; location / { try_files $uri /index.php$is_args$args; } location ~ ^/index\.php(/|$) { fastcgi_pass app:9000; fastcgi_split_path_info ^(.+\.php)(/.*)$; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; fastcgi_param DOCUMENT_ROOT $realpath_root; internal; } location ~ \.php$ { return 404; } } ``` ## 3. Protection contre les attaques courantes ### Protection CSRF Symfony active la protection CSRF par défaut. Assurez-vous qu'elle est bien configurée : ```php // src/Form/UserType.php use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\OptionsResolver\OptionsResolver; class UserType extends AbstractType { public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'data_class' => User::class, 'csrf_protection' => true, // Activé par défaut 'csrf_field_name' => '_token', 'csrf_token_id' => 'user_item', ]); } } ``` ### Protection XSS Utilisez toujours l'échappement Twig : ```twig {# templates/user/profile.html.twig #} {# Bon - échappement automatique #}

{{ user.name }}

{# Bon - échappement explicite #}

{{ user.description|e }}

{# Mauvais - risque XSS #}

{{ user.html|raw }}

{# Bon - sanitisation HTML #}

{{ user.html|sanitize_html }}

``` ### Protection contre l'injection SQL Utilisez toujours Doctrine avec des requêtes préparées : ```php // src/Repository/UserRepository.php namespace App\Repository; use App\Entity\User; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; class UserRepository extends ServiceEntityRepository { // Bon - Requête préparée avec paramètres public function findByEmail(string $email): ?User { return $this->createQueryBuilder('u') ->where('u.email = :email') ->setParameter('email', $email) ->getQuery() ->getOneOrNullResult(); } // Mauvais - Concaténation directe (NE JAMAIS FAIRE) // public function dangerousFind(string $email): ?User // { // return $this->createQueryBuilder('u') // ->where('u.email = "' . $email . '"') // DANGER! // ->getQuery() // ->getOneOrNullResult(); // } } ``` ## 4. Authentification et autorisation renforcées ### Configuration du Security Bundle ```yaml # config/packages/security.yaml security: password_hashers: Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: algorithm: auto cost: 15 # Augmenter le coût pour plus de sécurité providers: app_user_provider: entity: class: App\Entity\User property: email firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false main: lazy: true provider: app_user_provider custom_authenticator: App\Security\LoginFormAuthenticator logout: path: app_logout invalidate_session: true clear_site_data: - cache - cookies - storage remember_me: secret: '%kernel.secret%' lifetime: 604800 path: / always_remember_me: false secure: true # Uniquement en HTTPS httponly: true # Protection XSS samesite: lax # Protection CSRF # Limitation des tentatives de connexion login_throttling: max_attempts: 5 interval: '15 minutes' access_control: - { path: ^/admin, roles: ROLE_ADMIN } - { path: ^/api, roles: ROLE_USER } role_hierarchy: ROLE_ADMIN: ROLE_USER ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH] ``` ### Implémenter l'authentification à deux facteurs ```bash composer require scheb/2fa-bundle ``` ```php // src/Entity/User.php use Scheb\TwoFactorBundle\Model\Totp\TotpConfiguration; use Scheb\TwoFactorBundle\Model\Totp\TotpConfigurationInterface; use Scheb\TwoFactorBundle\Model\Totp\TwoFactorInterface; class User implements TwoFactorInterface { private ?string $totpSecret = null; public function isTotpAuthenticationEnabled(): bool { return null !== $this->totpSecret; } public function getTotpAuthenticationUsername(): string { return $this->email; } public function getTotpAuthenticationConfiguration(): ?TotpConfigurationInterface { return new TotpConfiguration($this->totpSecret, TotpConfiguration::ALGORITHM_SHA1, 30, 6); } public function setTotpSecret(?string $totpSecret): self { $this->totpSecret = $totpSecret; return $this; } } ``` ## 5. Sécurisation du réseau Docker ### Configuration réseau isolée ```yaml # docker-compose.yml version: '3.8' services: nginx: image: nginx:alpine ports: - "80:80" - "443:443" networks: - frontend depends_on: - app app: build: context: . dockerfile: Dockerfile networks: - frontend - backend depends_on: - db - redis db: image: mysql:8.0 networks: - backend environment: MYSQL_ROOT_PASSWORD_FILE: /run/secrets/db_root_password secrets: - db_root_password volumes: - db_data:/var/lib/mysql redis: image: redis:alpine networks: - backend command: redis-server --requirepass ${REDIS_PASSWORD} networks: frontend: driver: bridge backend: driver: bridge internal: true # Pas d'accès externe volumes: db_data: secrets: db_root_password: external: true ``` ## 6. Logging et monitoring de sécurité ### Configuration Monolog pour la sécurité ```yaml # config/packages/prod/monolog.yaml monolog: handlers: security: type: stream path: "%kernel.logs_dir%/security.log" level: info channels: ["security"] authentication_failures: type: stream path: "%kernel.logs_dir%/auth_failures.log" level: warning channels: ["security"] ``` ### Créer un listener pour les événements de sécurité ```php // src/EventListener/SecurityEventListener.php namespace App\EventListener; use Psr\Log\LoggerInterface; use Symfony\Component\EventDispatcher\Attribute\AsEventListener; use Symfony\Component\Security\Http\Event\LoginFailureEvent; use Symfony\Component\Security\Http\Event\LoginSuccessEvent; #[AsEventListener(event: LoginSuccessEvent::class)] #[AsEventListener(event: LoginFailureEvent::class)] class SecurityEventListener { public function __construct(private LoggerInterface $securityLogger) { } public function onLoginSuccess(LoginSuccessEvent $event): void { $user = $event->getUser(); $request = $event->getRequest(); $this->securityLogger->info('Successful login', [ 'user' => $user->getUserIdentifier(), 'ip' => $request->getClientIp(), 'user_agent' => $request->headers->get('User-Agent'), ]); } public function onLoginFailure(LoginFailureEvent $event): void { $request = $event->getRequest(); $exception = $event->getException(); $this->securityLogger->warning('Failed login attempt', [ 'username' => $request->request->get('_username'), 'ip' => $request->getClientIp(), 'reason' => $exception->getMessage(), 'user_agent' => $request->headers->get('User-Agent'), ]); } } ``` ## 7. Pipeline CI/CD sécurisé ### Configuration GitLab CI complète ```yaml # .gitlab-ci.yml stages: - build - security - test - deploy variables: DOCKER_DRIVER: overlay2 DOCKER_TLS_CERTDIR: "/certs" build: stage: build image: docker:latest services: - docker:dind script: - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA . - docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA $CI_REGISTRY_IMAGE:latest - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA - docker push $CI_REGISTRY_IMAGE:latest security:container-scan: stage: security image: aquasec/trivy:latest script: - trivy image --severity HIGH,CRITICAL --exit-code 1 $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA allow_failure: false security:dependency-scan: stage: security image: composer:latest script: - composer install --no-scripts - composer audit allow_failure: false security:sast: stage: security image: php:8.2-cli script: - curl -L https://github.com/phpstan/phpstan/releases/latest/download/phpstan.phar -o phpstan.phar - php phpstan.phar analyse src --level=8 allow_failure: false test: stage: test image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA script: - php bin/phpunit coverage: '/^\s*Lines:\s*(\d+\.\d+%)/' deploy:production: stage: deploy image: alpine:latest before_script: - apk add --no-cache openssh-client - eval $(ssh-agent -s) - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - - mkdir -p ~/.ssh - chmod 700 ~/.ssh script: - ssh -o StrictHostKeyChecking=no $DEPLOY_USER@$DEPLOY_HOST "docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" - ssh -o StrictHostKeyChecking=no $DEPLOY_USER@$DEPLOY_HOST "docker-compose up -d" only: - main when: manual ``` ## 8. Checklist de sécurité finale ### Avant chaque déploiement - [ ] Toutes les dépendances sont à jour (`composer audit`) - [ ] Les images Docker sont scannées (Trivy) - [ ] APP_ENV=prod et APP_DEBUG=0 - [ ] Tous les secrets sont externalisés - [ ] HTTPS activé avec certificat valide - [ ] En-têtes de sécurité configurés - [ ] Rate limiting activé - [ ] Logs de sécurité configurés - [ ] Backups automatiques en place - [ ] WAF (Web Application Firewall) configuré si possible ### Configuration de sauvegarde ```bash #!/bin/bash # backup.sh DATE=$(date +%Y%m%d_%H%M%S) BACKUP_DIR="/backups" # Backup de la base de données docker exec mysql mysqldump -u root -p${MYSQL_ROOT_PASSWORD} \ --all-databases --single-transaction --quick --lock-tables=false \ > "${BACKUP_DIR}/db_backup_${DATE}.sql" # Chiffrement du backup gpg --encrypt --recipient admin@example.com "${BACKUP_DIR}/db_backup_${DATE}.sql" # Suppression du fichier non chiffré rm "${BACKUP_DIR}/db_backup_${DATE}.sql" # Rotation des backups (garder 30 jours) find ${BACKUP_DIR} -name "db_backup_*.sql.gpg" -mtime +30 -delete # Upload vers stockage distant (S3, etc.) aws s3 cp "${BACKUP_DIR}/db_backup_${DATE}.sql.gpg" \ s3://my-secure-backups/mysql/ --sse AES256 ``` ## Conclusion La sécurisation d'une application Symfony avec Docker est un processus continu qui nécessite une attention constante. En suivant ces bonnes pratiques, vous établissez une base solide pour protéger votre application contre les menaces courantes. ### Points clés à retenir : 1. **Conteneurs sécurisés** : Utilisez des images minimales, scannez les vulnérabilités et n'exécutez jamais en root 2. **Secrets externalisés** : Jamais de credentials dans le code ou les images 3. **Défense en profondeur** : Multipliez les couches de sécurité (réseau, application, données) 4. **Monitoring actif** : Loggez les événements de sécurité et mettez en place des alertes 5. **CI/CD sécurisé** : Intégrez les tests de sécurité dans votre pipeline La sécurité n'est pas une destination mais un voyage. Restez informé des dernières vulnérabilités, mettez régulièrement à jour vos dépendances et effectuez des audits de sécurité périodiques. ### Ressources supplémentaires : - [Symfony Security Documentation](https://symfony.com/doc/current/security.html) - [OWASP Top 10](https://owasp.org/www-project-top-ten/) - [Docker Security Best Practices](https://docs.docker.com/engine/security/) - [CIS Docker Benchmark](https://www.cisecurity.org/benchmark/docker) N'hésitez pas à partager vos propres pratiques de sécurité dans les commentaires !