Add S3/MinIO storage, nelmio security and CSP config

- Flysystem S3 adapter configured for MinIO
- Vich uploader switched to Flysystem S3 storage
- Liip imagine loader/resolver on S3
- S3 client service with path style endpoint for MinIO
- Nelmio security: CSP, clickjacking, permissions policy, external redirects
- CSP dev: allow Vite HMR (localhost:5173)
- CSP prod: nonce scripts, restricted form-action and connect-src
- composer: flysystem-bundle, flysystem-aws-s3-v3, nelmio/security-bundle

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-03-18 21:10:45 +01:00
parent e3de0da1bf
commit 2d02ba4cbb
13 changed files with 1664 additions and 3 deletions

View File

@@ -1581,6 +1581,207 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* db_driver?: scalar|Param|null, // Default: null
* }>,
* }
* @psalm-type FlysystemConfig = array{
* storages?: array<string, array{ // Default: []
* adapter: scalar|Param|null,
* options?: list<mixed>,
* visibility?: scalar|Param|null, // Default: null
* directory_visibility?: scalar|Param|null, // Default: null
* retain_visibility?: bool|Param|null, // Default: null
* case_sensitive?: bool|Param, // Default: true
* disable_asserts?: bool|Param, // Default: false
* public_url?: list<scalar|Param|null>,
* path_normalizer?: scalar|Param|null, // Default: null
* public_url_generator?: scalar|Param|null, // Default: null
* temporary_url_generator?: scalar|Param|null, // Default: null
* read_only?: bool|Param, // Default: false
* }>,
* }
* @psalm-type NelmioSecurityConfig = array{
* signed_cookie?: array{
* names?: list<scalar|Param|null>,
* secret?: scalar|Param|null, // Default: "%kernel.secret%"
* hash_algo?: scalar|Param|null,
* legacy_hash_algo?: scalar|Param|null, // Fallback algorithm to allow for frictionless hash algorithm upgrades. Use with caution and as a temporary measure as it allows for downgrade attacks. // Default: null
* separator?: scalar|Param|null, // Default: "."
* },
* clickjacking?: array{
* hosts?: list<scalar|Param|null>,
* paths?: array<string, array{ // Default: {"^/.*":{"header":"DENY"}}
* header?: scalar|Param|null, // Default: "DENY"
* }>,
* content_types?: list<scalar|Param|null>,
* },
* external_redirects?: array{
* abort?: bool|Param, // Default: false
* override?: scalar|Param|null, // Default: null
* forward_as?: scalar|Param|null, // Default: null
* log?: bool|Param, // Default: false
* allow_list?: list<scalar|Param|null>,
* },
* flexible_ssl?: bool|array{
* enabled?: bool|Param, // Default: false
* cookie_name?: scalar|Param|null, // Default: "auth"
* unsecured_logout?: bool|Param, // Default: false
* },
* forced_ssl?: bool|array{
* enabled?: bool|Param, // Default: false
* hsts_max_age?: scalar|Param|null, // Default: null
* hsts_subdomains?: bool|Param, // Default: false
* hsts_preload?: bool|Param, // Default: false
* allow_list?: list<scalar|Param|null>,
* hosts?: list<scalar|Param|null>,
* redirect_status_code?: scalar|Param|null, // Default: 302
* },
* content_type?: array{
* nosniff?: bool|Param, // Default: false
* },
* xss_protection?: array{ // Deprecated: The "xss_protection" option is deprecated, use Content Security Policy without allowing "unsafe-inline" scripts instead.
* enabled?: bool|Param, // Default: false
* mode_block?: bool|Param, // Default: false
* report_uri?: scalar|Param|null, // Default: null
* },
* csp?: bool|array{
* enabled?: bool|Param, // Default: true
* request_matcher?: scalar|Param|null, // Default: null
* hosts?: list<scalar|Param|null>,
* content_types?: list<scalar|Param|null>,
* report_endpoint?: array{
* log_channel?: scalar|Param|null, // Default: null
* log_formatter?: scalar|Param|null, // Default: "nelmio_security.csp_report.log_formatter"
* log_level?: "alert"|"critical"|"debug"|"emergency"|"error"|"info"|"notice"|"warning"|Param, // Default: "notice"
* filters?: array{
* domains?: bool|Param, // Default: true
* schemes?: bool|Param, // Default: true
* browser_bugs?: bool|Param, // Default: true
* injected_scripts?: bool|Param, // Default: true
* },
* dismiss?: list<list<"default-src"|"base-uri"|"block-all-mixed-content"|"child-src"|"connect-src"|"font-src"|"form-action"|"frame-ancestors"|"frame-src"|"img-src"|"manifest-src"|"media-src"|"object-src"|"plugin-types"|"script-src"|"style-src"|"upgrade-insecure-requests"|"report-uri"|"worker-src"|"prefetch-src"|"report-to"|"*"|Param>>,
* },
* compat_headers?: bool|Param, // Default: true
* report_logger_service?: scalar|Param|null, // Default: "logger"
* hash?: array{
* algorithm?: "sha256"|"sha384"|"sha512"|Param, // The algorithm to use for hashes // Default: "sha256"
* },
* report?: array{
* level1_fallback?: bool|Param, // Provides CSP Level 1 fallback when using hash or nonce (CSP level 2) by adding 'unsafe-inline' source. See https://www.w3.org/TR/CSP2/#directive-script-src and https://www.w3.org/TR/CSP2/#directive-style-src // Default: true
* browser_adaptive?: bool|array{ // Do not send directives that browser do not support
* enabled?: bool|Param, // Default: false
* parser?: scalar|Param|null, // Default: "nelmio_security.ua_parser.ua_php"
* },
* default-src?: list<scalar|Param|null>,
* base-uri?: list<scalar|Param|null>,
* block-all-mixed-content?: bool|Param, // Default: false
* child-src?: list<scalar|Param|null>,
* connect-src?: list<scalar|Param|null>,
* font-src?: list<scalar|Param|null>,
* form-action?: list<scalar|Param|null>,
* frame-ancestors?: list<scalar|Param|null>,
* frame-src?: list<scalar|Param|null>,
* img-src?: list<scalar|Param|null>,
* manifest-src?: list<scalar|Param|null>,
* media-src?: list<scalar|Param|null>,
* object-src?: list<scalar|Param|null>,
* plugin-types?: list<scalar|Param|null>,
* script-src?: list<scalar|Param|null>,
* style-src?: list<scalar|Param|null>,
* upgrade-insecure-requests?: bool|Param, // Default: false
* report-uri?: list<scalar|Param|null>,
* worker-src?: list<scalar|Param|null>,
* prefetch-src?: list<scalar|Param|null>,
* report-to?: scalar|Param|null,
* },
* enforce?: array{
* level1_fallback?: bool|Param, // Provides CSP Level 1 fallback when using hash or nonce (CSP level 2) by adding 'unsafe-inline' source. See https://www.w3.org/TR/CSP2/#directive-script-src and https://www.w3.org/TR/CSP2/#directive-style-src // Default: true
* browser_adaptive?: bool|array{ // Do not send directives that browser do not support
* enabled?: bool|Param, // Default: false
* parser?: scalar|Param|null, // Default: "nelmio_security.ua_parser.ua_php"
* },
* default-src?: list<scalar|Param|null>,
* base-uri?: list<scalar|Param|null>,
* block-all-mixed-content?: bool|Param, // Default: false
* child-src?: list<scalar|Param|null>,
* connect-src?: list<scalar|Param|null>,
* font-src?: list<scalar|Param|null>,
* form-action?: list<scalar|Param|null>,
* frame-ancestors?: list<scalar|Param|null>,
* frame-src?: list<scalar|Param|null>,
* img-src?: list<scalar|Param|null>,
* manifest-src?: list<scalar|Param|null>,
* media-src?: list<scalar|Param|null>,
* object-src?: list<scalar|Param|null>,
* plugin-types?: list<scalar|Param|null>,
* script-src?: list<scalar|Param|null>,
* style-src?: list<scalar|Param|null>,
* upgrade-insecure-requests?: bool|Param, // Default: false
* report-uri?: list<scalar|Param|null>,
* worker-src?: list<scalar|Param|null>,
* prefetch-src?: list<scalar|Param|null>,
* report-to?: scalar|Param|null,
* },
* },
* referrer_policy?: bool|array{
* enabled?: bool|Param, // Default: false
* policies?: list<scalar|Param|null>,
* },
* permissions_policy?: bool|array{
* enabled?: bool|Param, // Default: false
* policies?: array{
* accelerometer?: mixed, // Default: null
* ambient_light_sensor?: mixed, // Default: null
* attribution_reporting?: mixed, // Default: null
* autoplay?: mixed, // Default: null
* bluetooth?: mixed, // Default: null
* browsing_topics?: mixed, // Default: null
* camera?: mixed, // Default: null
* captured_surface_control?: mixed, // Default: null
* compute_pressure?: mixed, // Default: null
* cross_origin_isolated?: mixed, // Default: null
* deferred_fetch?: mixed, // Default: null
* deferred_fetch_minimal?: mixed, // Default: null
* display_capture?: mixed, // Default: null
* encrypted_media?: mixed, // Default: null
* fullscreen?: mixed, // Default: null
* gamepad?: mixed, // Default: null
* geolocation?: mixed, // Default: null
* gyroscope?: mixed, // Default: null
* hid?: mixed, // Default: null
* identity_credentials_get?: mixed, // Default: null
* idle_detection?: mixed, // Default: null
* interest_cohort?: mixed, // Default: null
* language_detector?: mixed, // Default: null
* local_fonts?: mixed, // Default: null
* magnetometer?: mixed, // Default: null
* microphone?: mixed, // Default: null
* midi?: mixed, // Default: null
* otp_credentials?: mixed, // Default: null
* payment?: mixed, // Default: null
* picture_in_picture?: mixed, // Default: null
* publickey_credentials_create?: mixed, // Default: null
* publickey_credentials_get?: mixed, // Default: null
* screen_wake_lock?: mixed, // Default: null
* serial?: mixed, // Default: null
* speaker_selection?: mixed, // Default: null
* storage_access?: mixed, // Default: null
* summarizer?: mixed, // Default: null
* translator?: mixed, // Default: null
* usb?: mixed, // Default: null
* web_share?: mixed, // Default: null
* window_management?: mixed, // Default: null
* xr_spatial_tracking?: mixed, // Default: null
* },
* },
* cross_origin_isolation?: bool|array{
* enabled?: bool|Param, // Default: false
* paths?: array<string, array{ // Default: []
* coep?: "unsafe-none"|"require-corp"|"credentialless"|Param, // Cross-Origin-Embedder-Policy (COEP) header value
* coop?: "unsafe-none"|"same-origin-allow-popups"|"same-origin"|"noopener-allow-popups"|Param, // Cross-Origin-Opener-Policy (COOP) header value
* corp?: "same-site"|"same-origin"|"cross-origin"|Param, // Cross-Origin-Resource-Policy (CORP) header value
* report_only?: bool|Param, // Use Report-Only headers instead of enforcing (applies to COEP and COOP only) // Default: false
* report_to?: scalar|Param|null, // Reporting endpoint name for violations (requires Reporting API configuration, applies to COEP and COOP only) // Default: null
* }>,
* },
* }
* @psalm-type ConfigType = array{
* imports?: ImportsConfig,
* parameters?: ParametersConfig,
@@ -1595,6 +1796,8 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* endroid_qr_code?: EndroidQrCodeConfig,
* liip_imagine?: LiipImagineConfig,
* vich_uploader?: VichUploaderConfig,
* flysystem?: FlysystemConfig,
* nelmio_security?: NelmioSecurityConfig,
* "when@dev"?: array{
* imports?: ImportsConfig,
* parameters?: ParametersConfig,
@@ -1612,6 +1815,8 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* endroid_qr_code?: EndroidQrCodeConfig,
* liip_imagine?: LiipImagineConfig,
* vich_uploader?: VichUploaderConfig,
* flysystem?: FlysystemConfig,
* nelmio_security?: NelmioSecurityConfig,
* },
* "when@prod"?: array{
* imports?: ImportsConfig,
@@ -1627,6 +1832,8 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* endroid_qr_code?: EndroidQrCodeConfig,
* liip_imagine?: LiipImagineConfig,
* vich_uploader?: VichUploaderConfig,
* flysystem?: FlysystemConfig,
* nelmio_security?: NelmioSecurityConfig,
* },
* "when@test"?: array{
* imports?: ImportsConfig,
@@ -1643,6 +1850,8 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* endroid_qr_code?: EndroidQrCodeConfig,
* liip_imagine?: LiipImagineConfig,
* vich_uploader?: VichUploaderConfig,
* flysystem?: FlysystemConfig,
* nelmio_security?: NelmioSecurityConfig,
* },
* ...<string, ExtensionType|array{ // extra keys must follow the when@%env% pattern or match an extension alias
* imports?: ImportsConfig,