The review found one defect shape repeated across the platform: surfaces that were declared, bound, and documented, but that nothing read. An operator configuring fullUrlRecording, bodyLogging, retry.policy, validatedDnsPinning, timeout.dns, or any of ten declared metric names got a guarantee the code never delivered. Every such surface is now in exactly one of three states -- wired for real, rejected at startup, or registered in a test-enforced gap list with its reason. No silent no-ops remain. P0: - Activate the platform from bootstrap behind app.httpclient.enabled, with a single auto-configuration importing the nine child configurations. - Give the platform a strict, repository-level ENV contract: 74 leaf fields derived from the settings record tree, unknown APP_HTTPCLIENT_* rejected. - Route typed HTTP service clients through the call kernel via KernelHttpExchangeAdapter, so they stop bypassing platform policy. - Pin dynamic-target DNS resolution to the socket for the life of a call, closing the resolve-then-connect TOCTOU / rebinding window. - Actually transmit the idempotency key, and make retry eligibility depend on transmission rather than on merely holding one. - Reject reactive authentication and reactive redirect at startup instead of declaring support that does not function. - Fix the Reactor-only Stable contract row so the lane stops failing. - Stop advertising HTTP/3 on a transport that negotiates HTTP/1. P1 covers execution and retry accounting, redirect security (per-hop target guarding, sensitive-header stripping, 303 body handling), runtime rotation and transport resource ownership keyed by generation, dynamic-target hardening (subdomain matching, global-unicast classification, strict CIDR parsing), protocol intent, pool and timeout wiring, streaming and body limits, observability parity, and OAuth single-flight refresh on a bounded pool with a bounded wait. P2 covers configuration and documentation drift, the Gradle check wiring for the four hermetic lanes, and the CI gate matrix. Two test-quality defects surfaced while closing these: the HTTP/2 stream saturation test ran against cleartext HTTP/1.1 while asserting nothing about the protocol, and an OAuth contention test slept on a latch that could fire before the callers it meant to observe. Both now assert what their names claim. Verification run: :adapter:outbound:httpclient:check and :app-bootstrap:check (checkstyle, spotless, spotbugs, and the four hermetic lanes), verifyCleanArchitectureDependencies, verifyEnvKeys, verifyOneTypePerFile, verifyDependencyLocks, the documentation and gate-matrix verifiers, and the performance lane against a real TLS+ALPN HTTP/2 server. Not executed, and tracked rather than claimed: Docker/Toxiproxy fault injection, JMH, a real QUIC/HTTP3 server, a real Spring Framework 6.2 distribution (now a delegated-pending gate), live OAuth/TLS/proxy/DNS integration, and a whole-repository check. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
234 lines
8.8 KiB
Bash
234 lines
8.8 KiB
Bash
# ----------------------------------------------------------------------------
|
|
# 외부화 설정의 단일 출처. spring-dotenv 가 src/.env 에서 로드합니다
|
|
# (bootRun.workingDir = src/). 각 키의 허용값·결정 근거는 src/README.md 참조.
|
|
# ----------------------------------------------------------------------------
|
|
|
|
# ----- App identity -----
|
|
APP_NAME=ca-skeleton
|
|
SPRING_PROFILES_ACTIVE=local
|
|
|
|
# ----- Runtime safety (StartupSafetyValidator, D8) -----
|
|
APP_ERROR_DETAIL_EXPOSURE_ENABLED=false
|
|
APP_LOG_BODY_CAPTURE_ENABLED=false
|
|
APP_MULTI_INSTANCE_ENABLED=false
|
|
APP_MIGRATION_ON_STARTUP=true
|
|
APP_RATE_LIMIT_ENABLED=false
|
|
APP_RATE_LIMIT_CLIENT_IP_MODE=remote-addr-only
|
|
APP_RATE_LIMIT_PROVIDER=disabled
|
|
APP_IDEMPOTENCY_TTL=24h
|
|
APP_IDEMPOTENCY_PROVIDER=jdbc
|
|
APP_IDEMPOTENCY_REDIS_KEY_HMAC_SECRET=
|
|
APP_IDEMPOTENCY_PROCESSING_LEASE=30s
|
|
APP_IDEMPOTENCY_FAILURE_RETENTION=24h
|
|
APP_LEASE_PROVIDER=disabled
|
|
APP_LEASE_REDIS_KEY_HMAC_SECRET=
|
|
APP_LEASE_REDIS_DRIFT_BUDGET=10ms
|
|
|
|
# ----- Async executor -----
|
|
APP_ASYNC_EXECUTOR_CORE_SIZE=10
|
|
APP_ASYNC_EXECUTOR_MAX_SIZE=50
|
|
APP_ASYNC_EXECUTOR_QUEUE_CAPACITY=200
|
|
|
|
# ----- Optional integration adapters (default: all disabled) -----
|
|
APP_CACHE_CANONICAL_DEFAULT_PROVIDER=disabled
|
|
# The single global Redis switch. False means no Redis settings, secrets, client, threads or
|
|
# health contributor exist. Role selectors (cache/session/idempotency/lease/rate-limit) choose
|
|
# which capabilities compose once Redis is on; none of them turns Redis on.
|
|
APP_REDIS_ENABLED=false
|
|
APP_MESSAGING_BROKER=
|
|
APP_MESSAGING_KAFKA_BROKERS=
|
|
APP_NOTIFICATION_SLACK_PROVIDER=
|
|
APP_NOTIFICATION_EMAIL_PROVIDER=
|
|
|
|
# ----- Logging: root & app levels -----
|
|
APP_LOG_LEVEL_ROOT=INFO
|
|
APP_LOG_LEVEL_APP=DEBUG
|
|
|
|
# ----- Logging: per-package levels -----
|
|
APP_LOG_LEVEL_SPRING=INFO
|
|
APP_LOG_LEVEL_WEB=INFO
|
|
APP_LOG_LEVEL_SQL=WARN
|
|
|
|
# ----- Logging: file output + rolling -----
|
|
APP_LOG_FILE_ENABLED=false
|
|
APP_LOG_FILE_PATH=logs/ca-skeleton.json
|
|
APP_LOG_FILE_MAX_SIZE=100MB
|
|
APP_LOG_FILE_MAX_HISTORY=14
|
|
APP_LOG_FILE_TOTAL_SIZE_CAP=3GB
|
|
|
|
# ----- Logging: async appender -----
|
|
APP_LOG_ASYNC_ENABLED=true
|
|
APP_LOG_ASYNC_QUEUE_SIZE=512
|
|
APP_LOG_ASYNC_DISCARDING_THRESHOLD=20
|
|
|
|
# ----- Logging: JSON encoder -----
|
|
APP_LOG_JSON_TIMEZONE=UTC
|
|
APP_LOG_JSON_TIMESTAMP_PATTERN=yyyy-MM-dd'T'HH:mm:ss.SSSXXX
|
|
APP_LOG_JSON_INCLUDE_CALLER_DATA=false
|
|
APP_LOG_JSON_LOGGER_NAME_LENGTH=0
|
|
|
|
# ----- Logging: sampling -----
|
|
APP_LOG_SAMPLING_RATE=1.0
|
|
|
|
# ----- Distributed tracing -----
|
|
OTEL_EXPORTER_OTLP_ENDPOINT=
|
|
APP_TRACING_ENABLED=true
|
|
APP_TRACING_SAMPLE_RATE=
|
|
|
|
# ----- Privacy: user_principal pseudonymization -----
|
|
APP_PRIVACY_PSEUDONYMIZATION_SALT=__LOCAL_DEV_pseudonymization_salt
|
|
|
|
# ----- Spring Boot bootstrap -----
|
|
SPRING_BANNER_MODE=console
|
|
SPRING_MAIN_LAZY_INITIALIZATION=false
|
|
SPRING_MAIN_LOG_STARTUP_INFO=true
|
|
SPRING_THREADS_VIRTUAL_ENABLED=true
|
|
|
|
# ----- Jackson: deserialization policy -----
|
|
SPRING_JACKSON_DESER_FAIL_ON_UNKNOWN_PROPERTIES=true
|
|
SPRING_JACKSON_DESER_FAIL_ON_NULL_FOR_PRIMITIVES=true
|
|
SPRING_JACKSON_DESER_FAIL_ON_IGNORED_PROPERTIES=true
|
|
SPRING_JACKSON_DESER_READ_UNKNOWN_ENUM_VALUES_AS_NULL=false
|
|
|
|
# ----- Jackson: serialization policy -----
|
|
SPRING_JACKSON_SER_WRITE_DATES_AS_TIMESTAMPS=false
|
|
|
|
# ----- Server / Tomcat -----
|
|
APP_SERVER_PORT=8080
|
|
APP_SERVER_SHUTDOWN=graceful
|
|
APP_SERVER_SHUTDOWN_TIMEOUT=30s
|
|
APP_SERVER_TOMCAT_MAX_THREADS=200
|
|
APP_SERVER_TOMCAT_MIN_SPARE_THREADS=10
|
|
APP_SERVER_TOMCAT_ACCEPT_COUNT=100
|
|
APP_SERVER_TOMCAT_MAX_CONNECTIONS=8192
|
|
APP_SERVER_TOMCAT_CONNECTION_TIMEOUT=20s
|
|
APP_SERVER_COMPRESSION_ENABLED=true
|
|
APP_SERVER_COMPRESSION_MIN_RESPONSE_SIZE=1024
|
|
APP_SERVER_FORWARD_HEADERS_STRATEGY=framework
|
|
APP_SERVER_ERROR_INCLUDE_STACKTRACE=never
|
|
APP_SERVER_ERROR_INCLUDE_MESSAGE=never
|
|
|
|
# ----- Presentation -----
|
|
PRESENTATION_API_BASE_PATH=/api
|
|
|
|
# ----- Auth (OIDC resource server) -----
|
|
APP_SECURITY_AUTH_MODE=jwt
|
|
APP_SECURITY_JWT_ISSUER=http://localhost:8081/realms/ca-skeleton
|
|
APP_SECURITY_JWT_AUDIENCE=ca-skeleton-api
|
|
SECURITY_PUBLIC_PATHS=/api/healthcheck
|
|
APP_SESSION_COOKIE_NAME=CA_SESSION
|
|
APP_SESSION_COOKIE_SECURE=true
|
|
APP_SESSION_COOKIE_HTTP_ONLY=true
|
|
APP_SESSION_COOKIE_SAME_SITE=Lax
|
|
APP_SESSION_COOKIE_PATH=/
|
|
APP_SESSION_CSRF_COOKIE_NAME=XSRF-TOKEN
|
|
APP_SESSION_CSRF_HEADER_NAME=X-XSRF-TOKEN
|
|
|
|
# ----- CORS -----
|
|
APP_SECURITY_CORS_ENABLED=true
|
|
APP_SECURITY_CORS_ORIGINS=http://localhost:3000
|
|
APP_SECURITY_CORS_ALLOWED_METHODS=
|
|
APP_SECURITY_CORS_ALLOWED_HEADERS=*
|
|
APP_SECURITY_CORS_ALLOW_CREDENTIALS=true
|
|
APP_SECURITY_CORS_MAX_AGE=3600
|
|
|
|
# ----- Database (Postgres) -----
|
|
APP_DATASOURCE_URL=jdbc:postgresql://localhost:5433/ca_skeleton
|
|
APP_DATASOURCE_USERNAME=ca_skeleton
|
|
APP_DATASOURCE_PASSWORD=ca_skeleton
|
|
APP_DATASOURCE_DRIVER=org.postgresql.Driver
|
|
APP_DATASOURCE_DDL_AUTO=update
|
|
APP_DATASOURCE_SHOW_SQL=false
|
|
APP_DATASOURCE_FORMAT_SQL=false
|
|
APP_DATASOURCE_OPEN_IN_VIEW=false
|
|
|
|
# ----- Database: HikariCP connection pool -----
|
|
APP_DATASOURCE_POOL_MAX_SIZE=10
|
|
APP_DATASOURCE_POOL_MIN_IDLE=2
|
|
APP_DATASOURCE_CONNECTION_TIMEOUT=30000
|
|
APP_DATASOURCE_POOL_IDLE_TIMEOUT=600000
|
|
APP_DATASOURCE_POOL_MAX_LIFETIME=1800000
|
|
|
|
# ----- Management / Actuator -----
|
|
MANAGEMENT_SERVER_PORT=9001
|
|
|
|
# ----- Fileserver HTTP platform (app.fileserver-platform.*) -----
|
|
# Off by default. While false nothing below is bound: the platform auto-configuration binds this
|
|
# block itself and is not processed until the master switch is true.
|
|
APP_FILESERVER_PLATFORM_ENABLED=false
|
|
APP_FILESERVER_PLATFORM_INSTANCE_ID=local-node
|
|
APP_FILESERVER_PLATFORM_DEFAULT_NAMESPACE=default
|
|
|
|
# Storage root must be an absolute path on its own volume, never under a web or config root.
|
|
APP_FILESERVER_PLATFORM_STORAGE_ROOT=/var/lib/backend/files
|
|
APP_FILESERVER_PLATFORM_STORAGE_PUBLISH_MODE=atomic-move-preferred
|
|
APP_FILESERVER_PLATFORM_STORAGE_BUFFER_SIZE=128KB
|
|
APP_FILESERVER_PLATFORM_STORAGE_FORBIDDEN_ROOT_ANCESTORS=/app,/etc,/usr/share/nginx/html
|
|
|
|
# Shared with spring.servlet.multipart.* so the container and the policy cannot disagree.
|
|
APP_FILESERVER_PLATFORM_UPLOAD_MAX_FILE_SIZE=100MB
|
|
APP_FILESERVER_PLATFORM_UPLOAD_MAX_REQUEST_SIZE=110MB
|
|
APP_FILESERVER_PLATFORM_UPLOAD_INITIAL_RESERVATION=8MB
|
|
APP_FILESERVER_PLATFORM_UPLOAD_MAX_PARTS=16
|
|
APP_FILESERVER_PLATFORM_UPLOAD_TTL=1h
|
|
APP_FILESERVER_PLATFORM_UPLOAD_RESERVATION_TTL=24h
|
|
APP_FILESERVER_PLATFORM_UPLOAD_LEASE_DURATION=30s
|
|
APP_FILESERVER_PLATFORM_UPLOAD_REQUIRE_CONTENT_LENGTH=false
|
|
|
|
APP_FILESERVER_PLATFORM_DOWNLOAD_CACHE_CONTROL=private, no-store
|
|
APP_FILESERVER_PLATFORM_DOWNLOAD_INLINE_ALLOWED=false
|
|
APP_FILESERVER_PLATFORM_DOWNLOAD_MAX_RANGES=1
|
|
APP_FILESERVER_PLATFORM_DOWNLOAD_MAX_RANGE_BYTES=100MB
|
|
APP_FILESERVER_PLATFORM_DOWNLOAD_ZERO_COPY_ENABLED=true
|
|
APP_FILESERVER_PLATFORM_DOWNLOAD_ZERO_COPY_MINIMUM_BYTES=16MB
|
|
|
|
APP_FILESERVER_PLATFORM_TRANSFER_CORE_SIZE=8
|
|
APP_FILESERVER_PLATFORM_TRANSFER_MAX_SIZE=32
|
|
APP_FILESERVER_PLATFORM_TRANSFER_QUEUE_CAPACITY=64
|
|
APP_FILESERVER_PLATFORM_TRANSFER_AWAIT_SECONDS=300
|
|
|
|
# required | role-based | unenforced (unenforced is refused under a production profile).
|
|
APP_FILESERVER_PLATFORM_SECURITY_ACCESS_POLICY=required
|
|
APP_FILESERVER_PLATFORM_SECURITY_READ_ROLES=ROLE_FILE_READ
|
|
APP_FILESERVER_PLATFORM_SECURITY_WRITE_ROLES=ROLE_FILE_WRITE
|
|
APP_FILESERVER_PLATFORM_SECURITY_ADMIN_ROLES=ROLE_FILE_ADMIN
|
|
|
|
APP_FILESERVER_PLATFORM_VERIFICATION_TIMEOUT=5s
|
|
APP_FILESERVER_PLATFORM_VERIFICATION_REQUIRE_MEDIA_TYPE_VERDICT=false
|
|
APP_FILESERVER_PLATFORM_VERIFICATION_INLINE_SAFE_PROFILE=false
|
|
|
|
APP_FILESERVER_PLATFORM_QUOTA_INSTANCE_UPLOAD_PERMITS=16
|
|
APP_FILESERVER_PLATFORM_QUOTA_SCOPE_UPLOAD_PERMITS=4
|
|
APP_FILESERVER_PLATFORM_QUOTA_DIRECT_DOWNLOAD_PERMITS=64
|
|
APP_FILESERVER_PLATFORM_QUOTA_SOFT_HIGH_WATER=0.70
|
|
APP_FILESERVER_PLATFORM_QUOTA_HARD_HIGH_WATER=0.85
|
|
|
|
APP_FILESERVER_PLATFORM_ADMIN_ENABLED=false
|
|
APP_FILESERVER_PLATFORM_ADMIN_ORPHAN_MINIMUM_AGE=1h
|
|
|
|
APP_FILESERVER_PLATFORM_CLEANUP_ENABLED=false
|
|
APP_FILESERVER_PLATFORM_CLEANUP_INTERVAL=60s
|
|
APP_FILESERVER_PLATFORM_CLEANUP_MAX_ITEMS=100
|
|
APP_FILESERVER_PLATFORM_CLEANUP_MAX_BYTES=1GB
|
|
APP_FILESERVER_PLATFORM_CLEANUP_RETRY_BACKOFF=5m
|
|
|
|
APP_FILESERVER_PLATFORM_TUS_ENABLED=false
|
|
APP_FILESERVER_PLATFORM_HTTPBIS_DRAFT12_ENABLED=false
|
|
|
|
APP_FILESERVER_PLATFORM_NGINX_ENABLED=false
|
|
APP_FILESERVER_PLATFORM_NGINX_INTERNAL_PREFIX=/__files/
|
|
APP_FILESERVER_PLATFORM_NGINX_OBJECT_SUFFIX=.bin
|
|
APP_FILESERVER_PLATFORM_NGINX_MINIMUM_SIZE=16MB
|
|
|
|
APP_FILESERVER_PLATFORM_OBSERVABILITY_METRICS_ENABLED=true
|
|
# Secret. Required while metrics are enabled; an unkeyed digest of an enumerable id is reversible.
|
|
APP_FILESERVER_PLATFORM_OBSERVABILITY_FINGERPRINT_KEY=
|
|
|
|
# ----- HTTP Client platform (app.httpclient.*) -----
|
|
# The single switch for outbound HTTP. False means no HTTP client property is bound, and no
|
|
# transport provider, connection pool, TLS context, credential, thread or gateway is created.
|
|
# The per-client surface is indexed and per-deployment, so it is set directly in the environment
|
|
# rather than declared here; docs/httpclient/env-fields.yaml is its registry, and an
|
|
# APP_HTTPCLIENT_ variable that is not in that registry fails startup.
|
|
APP_HTTPCLIENT_ENABLED=false
|