비밀을 state.json(0664, 실패 시 무기한 잔존)에 기록하지 않고 runtime.php(0600, finalize 시 삭제)로 채널을 일원화한다. 정상 완료 경로에서는 finalize 가 state.json 을 삭제하지만, finalize 미호출 / unlink 실패 / 설치 중단 시에는 평문이 영구히 남았고, finalize 가드가 재호출을 410 차단해 자가 회복도 불가능했다. 삭제라는 단일 경로에 의존해 평문을 보호하던 구조 자체를 제거한다. SSE 워커는 세션에 접근할 수 없어 파일 경유가 불가피한데, 그 파일이 0664 의 state.json 이었던 것이 구조적 원인이다. 세션을 보유한 유일한 공통 시작점인 install-process.php 를 비밀 이송 지점으로 삼고, db_seed 가 소비한 즉시 제거한다. 레거시 / 실패 경로는 완료·중단·초기화·finalize 시점 redact 로 방어. 동반 수정: - 세션 config 재병합으로 무력화되던 기존 DB 비밀번호 마스킹 복구 - db_seed 후 putenv 미해제로 모든 exec 자식에 평문이 ENV 상속되던 누출 차단 - runtime read 비밀번호 폴백이 write 값으로 잘못 대체되던 문제 ( 인접) 이미 설치된 사이트는 7.0.4 업그레이드 스텝이 잔존 파일을 정리한다. 완료 증거가 있으면 삭제하고, 설치 진행 중일 수 있으면 비밀만 제거한다.
154 lines
9.6 KiB
YAML
154 lines
9.6 KiB
YAML
# audit:allow test-scenario-coverage reason: 인스톨러 비밀 수명주기의 시나리오 매트릭스 SSoT. 회귀 가드는 test_files 의 통과 테스트 + Manual 시나리오(실제 설치 2회 + storage grep) 로 커버.
|
|
|
|
feature: 인스톨러 비밀 수명주기 (state.json 평문 제거 → runtime.php 일원화)
|
|
|
|
description: |
|
|
설치 과정에서 입력받는 비밀(DB 비밀번호 2종, 관리자 비밀번호 2종) 이
|
|
storage/installer-state.json 에 평문으로 기록되어, 설치 실패/중단/finalize 미호출
|
|
환경에서 무기한 잔존하던 결함을 수정한다 (이슈 #465).
|
|
|
|
설계:
|
|
- state.json(0664, 실패 시 무기한 잔존) 에는 비밀을 아예 기록하지 않는다.
|
|
- 비밀 채널은 storage/installer/runtime.php(0600, finalize 시 삭제) 로 일원화.
|
|
SSE 워커는 세션 접근이 불가하므로 파일 경유가 불가피하며, 세션을 보유한 유일한
|
|
공통 시작점인 install-process.php 가 이송 지점이다 (SSE/폴링 공통, 재시도 포함).
|
|
- 관리자 비밀번호는 db_seed 가 소비한 즉시 runtime 에서 제거 + putenv 해제
|
|
(해제하지 않으면 이후 모든 exec() 자식이 평문을 ENV 상속).
|
|
- 레거시/실패 경로 방어: 완료·중단·초기화·finalize 시점 redact 폴백.
|
|
- 이미 설치된 사이트는 7.0.4 업그레이드 스텝이 잔존 파일을 정리.
|
|
|
|
구조적 제약:
|
|
state.config 에서 DB 비밀번호가 사라지므로 그 값을 읽던 소비처(db_cleanup /
|
|
롤백 seed truncate) 는 hydrateDbSecretsFromRuntime() 으로 자격증명을 복원한다.
|
|
두 소비처 모두 env_update 이후에만 실행되어 runtime.php 가 항상 존재한다.
|
|
|
|
axes:
|
|
installation_mode: [sse, polling]
|
|
session_state: [alive, lost_after_step3]
|
|
retry_kind: [none, resume_after_failure, drop_tables_reconsent]
|
|
runtime_state: [absent, present_with_db_only, present_with_db_and_admin]
|
|
seed_outcome: [not_reached, success, failure]
|
|
completion_path: [normal_finalize, finalize_unlink_failed, finalize_not_called, aborted, reset]
|
|
read_db: [disabled, enabled_separate_host]
|
|
legacy_state: [clean, has_plaintext_secrets_from_7_0_3_or_earlier]
|
|
upgrade_trigger: [not_applicable, state_present_installed, state_present_not_installed, state_absent, state_corrupted]
|
|
|
|
exclusions:
|
|
- { seed_outcome: not_reached, runtime_state: absent, reason: "env_update 이전 실패 — runtime 미생성이 정상" }
|
|
- { session_state: lost_after_step3, runtime_state: absent, retry_kind: resume_after_failure, reason: "세션+runtime 동시 유실은 재개 불가 400 → Step 3 재입력 (의도된 안전 실패)" }
|
|
- { upgrade_trigger: state_absent, legacy_state: has_plaintext_secrets_from_7_0_3_or_earlier, reason: "state 파일이 없으면 잔존 평문도 없음" }
|
|
- { completion_path: reset, seed_outcome: success, reason: "reset 은 완료 전 상태 초기화 — 시딩 성공 후 경로 아님" }
|
|
|
|
effects:
|
|
# 소스 차단 — state.json 에 비밀 미기록
|
|
- step3_post_writes_state_config_without_db_write_password
|
|
- step3_post_writes_state_config_without_db_read_password
|
|
- step3_post_writes_state_config_without_admin_password
|
|
- step3_post_writes_state_config_without_admin_password_confirm
|
|
- step3_post_keeps_admin_password_in_session_for_runtime_transfer
|
|
- step3_post_drops_admin_password_confirm_from_session_no_consumer_exists
|
|
- sanitize_config_for_state_preserves_non_secret_fields_like_app_name_and_db_host
|
|
- sanitize_config_for_state_drops_read_connection_fields_when_use_read_db_false
|
|
- install_process_writes_admin_password_into_runtime_php_before_state_save
|
|
- install_process_writes_state_config_through_sanitize_helper
|
|
- install_process_fails_installation_start_when_runtime_write_fails
|
|
- step3_form_prefill_never_renders_admin_password_value_even_from_legacy_state
|
|
|
|
# runtime.php 스키마 + 보존 폴백
|
|
- build_runtime_adds_admin_section_only_when_caller_supplies_password
|
|
- build_runtime_preserves_existing_admin_section_across_retries
|
|
- build_runtime_preserves_existing_db_write_password_when_state_config_lacks_it
|
|
- build_runtime_read_password_falls_back_to_existing_runtime_before_write_password
|
|
- build_runtime_prefers_caller_supplied_password_over_existing_runtime_value
|
|
- build_runtime_preserves_existing_app_key_unchanged_regression_guard
|
|
|
|
# DB 비밀번호 소비처 hydrate 폴백
|
|
- hydrate_db_secrets_fills_empty_write_password_from_runtime
|
|
- hydrate_db_secrets_fills_empty_read_password_from_runtime
|
|
- hydrate_db_secrets_does_not_override_password_already_present_in_config
|
|
- hydrate_db_secrets_returns_config_unchanged_when_runtime_absent
|
|
- db_cleanup_drop_tables_connects_successfully_with_hydrated_credentials
|
|
- rollback_seed_truncate_connects_successfully_with_hydrated_credentials
|
|
- generate_env_content_needs_no_hydration_because_merge_runtime_into_env_overwrites_db_lines
|
|
|
|
# 소비 즉시 삭제 (redact-on-consume)
|
|
- db_seed_reads_admin_password_from_runtime_php_not_state_config
|
|
- db_seed_falls_back_to_state_config_admin_password_for_legacy_installs_in_progress
|
|
- db_seed_removes_admin_section_from_runtime_php_immediately_after_success
|
|
- db_seed_redacts_legacy_state_secrets_immediately_after_success
|
|
- db_seed_redact_reloads_state_so_completed_task_markers_are_not_lost
|
|
- db_seed_unsets_installer_admin_password_env_after_success
|
|
- db_seed_unsets_installer_admin_name_email_language_env_after_success
|
|
- subsequent_exec_children_do_not_inherit_plaintext_admin_password_env
|
|
- db_seed_leaves_admin_password_intact_in_runtime_when_seeding_fails_so_retry_works
|
|
|
|
# .env 유출 차단 불변식
|
|
- merge_runtime_into_env_never_writes_admin_section_whitelist_invariant
|
|
- merge_runtime_into_env_still_writes_db_credentials_and_app_key
|
|
- installer_runtime_service_provider_ignores_unknown_admin_section
|
|
|
|
# 완료/실패 시점 폴백
|
|
- complete_flag_task_redacts_state_secrets_before_saving_completed_status
|
|
- finalize_deletes_state_json_on_success_when_DELETE_INSTALLER_AFTER_COMPLETE_set
|
|
- finalize_redacts_state_json_when_unlink_fails
|
|
- finalize_redacts_state_json_when_DELETE_INSTALLER_AFTER_COMPLETE_false
|
|
- reset_state_redacts_secrets_in_preserved_config
|
|
- abort_installation_redacts_secrets_in_preserved_config
|
|
- completed_installer_page_load_clears_install_config_from_session
|
|
- completed_installer_page_load_clears_db_test_flags_from_session
|
|
|
|
# 재시도 / 재개 안전성
|
|
- resume_after_failure_with_live_session_reinjects_admin_password_into_runtime
|
|
- resume_after_failure_with_lost_session_reuses_admin_password_from_runtime
|
|
- resume_after_db_seed_completion_passes_validation_via_completed_task_marker
|
|
- resume_with_no_session_and_no_runtime_and_no_seed_marker_returns_400_missing_admin_password
|
|
- drop_tables_reconsent_reruns_db_seed_with_password_reinjected_by_install_process
|
|
|
|
# 업그레이드 스텝 (7.0.4) — 기존 설치 사이트 정리
|
|
- upgrade_step_no_op_when_state_json_absent
|
|
- upgrade_step_deletes_state_json_when_env_installer_completed_is_true
|
|
- upgrade_step_deletes_state_json_when_g7_installed_flag_present
|
|
- upgrade_step_does_not_abort_upgrade_when_state_json_cannot_be_deleted
|
|
- upgrade_step_preserves_state_json_and_redacts_only_when_installation_not_complete
|
|
- upgrade_step_preserves_non_secret_config_fields_so_in_progress_install_can_continue
|
|
- upgrade_step_preserves_corrupted_state_json_without_destroying_it
|
|
- upgrade_step_is_idempotent_across_repeated_runs
|
|
- upgrade_step_handles_state_without_config_section
|
|
- upgrade_step_does_not_chmod_or_chown_preserving_existing_file_ownership
|
|
|
|
test_files:
|
|
- tests/Unit/Installer/InstallerStateSecretsTest.php
|
|
- tests/Unit/Installer/InstallerRuntimeHelperTest.php
|
|
- tests/Feature/Upgrade/RedactResidualInstallerStateSecretsTest.php
|
|
|
|
manual_verification:
|
|
- scenario: A_sse_full_install
|
|
description: |
|
|
SSE 모드로 실제 설치 1회 완료 → 완료 후 storage/ 하위에서 입력한 관리자
|
|
비밀번호 문자열 grep 무일치 (installer-state.json / installer/runtime.php /
|
|
logs/installation.log 전부) + 그 비밀번호로 관리자 로그인 성공.
|
|
- scenario: B_polling_full_install
|
|
description: |
|
|
폴링 모드로 동일 검증. SSE 와 동일하게 install-process.php 를 경유하므로
|
|
비밀 이송 경로가 같아야 한다.
|
|
- scenario: C_abort_midway
|
|
description: |
|
|
Step 5 진행 중 설치 중단 → 잔존 installer-state.json 에 admin_password /
|
|
db_write_password 부재 확인 (redact 폴백). 이후 재개 시 정상 진행.
|
|
- scenario: D_drop_tables_retry
|
|
description: |
|
|
기존 DB 가 있는 상태에서 drop_tables 재동의로 재설치 → db_seed 가 다시 실행되고
|
|
관리자 계정이 정상 생성됨 (install-process.php 가 비밀번호를 runtime 에 재주입).
|
|
- scenario: E_upgrade_existing_site
|
|
description: |
|
|
7.0.3 이하에서 설치되어 평문 비밀이 남은 installer-state.json 이 있는 사이트를
|
|
7.0.4 로 업그레이드 → 파일 삭제(완료 사이트) 또는 비밀만 redact 확인.
|
|
|
|
rules_layer_coverage:
|
|
- rule: upgrade-step-class-caching
|
|
coverage: Upgrade_7_0_4 는 빈 위임자, 로직은 data/7.0.4/migrations/ 의 버전 namespace 클래스에 격리
|
|
- rule: installer-pattern-no-copy-to-upgrade-step
|
|
coverage: 업그레이드 스텝은 인스톨러 파일을 require 하지 않고 비밀 키 목록/파싱/저장을 로컬 인라인 중복 구현. CLI 컨텍스트이므로 chmod/chown 미시도 (기존 inode 권한 보존)
|
|
- rule: service-provider-db-safety
|
|
coverage: InstallerRuntimeServiceProvider 무수정 — runtime['db'] / runtime['app']['key'] 만 참조하므로 신규 admin 섹션은 무시됨
|