From dd5a427d96bff24a925e5b29f6cc13d2912f54a4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 19:51:15 +0900 Subject: [PATCH 1/9] test(config): reproduce implicit repository authority --- ...nfigServerRepositoryAuthorityLiveTest.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 config-server/src/test/java/com/xtrmetl/config/ConfigServerRepositoryAuthorityLiveTest.java diff --git a/config-server/src/test/java/com/xtrmetl/config/ConfigServerRepositoryAuthorityLiveTest.java b/config-server/src/test/java/com/xtrmetl/config/ConfigServerRepositoryAuthorityLiveTest.java new file mode 100644 index 00000000..11930902 --- /dev/null +++ b/config-server/src/test/java/com/xtrmetl/config/ConfigServerRepositoryAuthorityLiveTest.java @@ -0,0 +1,34 @@ +package com.xtrmetl.config; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Verifies that the runnable Config Server never invents a remote Git repository authority. + */ +class ConfigServerRepositoryAuthorityLiveTest { + + @Test + void missingRepositoryAuthorityFailsClosedWithoutDemoRemote() throws IOException { + String applicationYaml = Files.readString(Path.of("src/main/resources/application.yml")); + + assertTrue( + applicationYaml.contains("uri: ${CONFIG_REPO_URI}"), + "Config Server must require an explicitly supplied CONFIG_REPO_URI" + ); + assertFalse( + applicationYaml.contains("your-repo/config-repo.git"), + "A demo remote must never become a runnable fallback" + ); + assertFalse( + applicationYaml.contains("CONFIG_REPO_URI:https://"), + "Missing repository authority must fail before implicit remote Git access" + ); + } +} From 43c404952afc66dd987bfaff8017f34744d27c9b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 16 Aug 2026 15:08:41 +0000 Subject: [PATCH 2/9] fix(config): reject blank Config Server repository authority Removing the demo Git default is not fail-closed: Spring Cloud Config 5.0.4 still starts with an unset or empty CONFIG_REPO_URI. Validate destination authority at default-profile startup and keep native fixtures independently startable. Co-authored-by: Seongho Bae --- AGENTS.md | 1 + ARCHITECTURE.md | 2 +- CHANGELOG.md | 1 + CLAUDE.md | 1 + .../ConfigServerRepositoryAuthority.java | 42 ++++++++ ...figServerRepositoryAuthorityValidator.java | 35 ++++++ .../src/main/resources/application.yml | 1 + ...nfigServerRepositoryAuthorityLiveTest.java | 102 ++++++++++++++---- .../ConfigServerRepositoryAuthorityTest.java | 52 +++++++++ ...erverRepositoryAuthorityValidatorTest.java | 26 +++++ ...nfigServerRepositoryConfigurationTest.java | 2 + .../config-server-repository-authority.md | 31 ++++-- 12 files changed, 263 insertions(+), 33 deletions(-) create mode 100644 config-server/src/main/java/com/xtrmetl/config/ConfigServerRepositoryAuthority.java create mode 100644 config-server/src/main/java/com/xtrmetl/config/ConfigServerRepositoryAuthorityValidator.java create mode 100644 config-server/src/test/java/com/xtrmetl/config/ConfigServerRepositoryAuthorityTest.java create mode 100644 config-server/src/test/java/com/xtrmetl/config/ConfigServerRepositoryAuthorityValidatorTest.java diff --git a/AGENTS.md b/AGENTS.md index 757f5141..12670aaa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,6 +18,7 @@ workflows, and service-level maintenance. - Root Maven aggregator: `pom.xml` - Services: `etl-service/`, `cdc-service/`, `zuul-gateway/`, `eureka-server/`, `config-server/` +- Config Server is independently runnable and is not a default production dependency. The default Git profile must fail closed without an operator-supplied `CONFIG_REPO_URI`; see `docs/doctoring/config-server-repository-authority.md`. - Shared code: `META-INF/`, common build config in root `pom.xml` - Operations/docs: `docker/`, `docs/`, `.github/`, `scripts/` diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 412d0b98..f2cfef3a 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -588,7 +588,7 @@ ETL Processing with Retry: | ETL Service | 8000 | HTTP | Internal | | CDC Service | 8001 | HTTP | Internal | | Eureka Server | 8761 | HTTP | Internal | -| Config Server | 8888 | HTTP | Internal | +| Config Server | 8888 | HTTP | Internal; optional module, not a default Compose dependency. Default Git profile requires `CONFIG_REPO_URI` and fails closed on blank, unresolved, or demo authority. Use `native` only for local fixtures. | | PostgreSQL | 5432 | TCP | Internal | | Kafka | 9092 | TCP | Internal | | Zipkin | 9412 | HTTP | Internal | diff --git a/CHANGELOG.md b/CHANGELOG.md index e8c3e4db..8bb5af7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Config Server default Git profile now rejects unset, blank, unresolved, and demo `CONFIG_REPO_URI` values at startup. Removing the example remote is not enough: Spring Cloud Config 5.0.4 still boots with an empty URI when `cloneOnStart` is false. Set `CONFIG_REPO_URI` to a reviewed Git URI before starting the default profile; use `native` only for local fixtures. - Production container builds now use digest-pinned Docker base images while retaining readable Maven/Temurin tags, preventing upstream tag movement from silently changing reviewed build inputs. - Durable `POST /api/etl/jobs` submissions now return RFC 9110 `202 Accepted`, a stable pending-job representation, `Location` status-monitor metadata, and explicit replay metadata without changing the synchronous `/api/etl/process` contract. The incomplete intake controller is fail-closed and requires explicit `xtrmetl.etl.jobs.intake-enabled=true` operator opt-in until worker execution and terminal payload clearing are implemented. - Concurrent requests using the same authenticated-principal-scoped semantic idempotency key now return immediate RFC 9457 `409 etl_idempotency_request_in_progress` responses through PostgreSQL `pg_try_advisory_xact_lock`; retries after completion still replay the committed response. diff --git a/CLAUDE.md b/CLAUDE.md index f194a399..45eb7223 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,5 +8,6 @@ Contributor and agent guidance for this repository. - Never commit secrets, credentials, `.env` files, or private keys. - Do not commit or push unless a human explicitly asks. - For workflow edits, run local YAML parsing and `actionlint` on edited files. +- Config Server default Git profile requires `CONFIG_REPO_URI` and must fail closed on blank authority. See `docs/doctoring/config-server-repository-authority.md`. If any guidance here conflicts with `AGENTS.md`, `AGENTS.md` wins. diff --git a/config-server/src/main/java/com/xtrmetl/config/ConfigServerRepositoryAuthority.java b/config-server/src/main/java/com/xtrmetl/config/ConfigServerRepositoryAuthority.java new file mode 100644 index 00000000..570da377 --- /dev/null +++ b/config-server/src/main/java/com/xtrmetl/config/ConfigServerRepositoryAuthority.java @@ -0,0 +1,42 @@ +package com.xtrmetl.config; + +/** + * Decides whether a Config Server Git URI is an explicit deployment-owned authority. + * + *

Spring Cloud Config 5.0.4 treats a non-null empty URI as valid when + * {@code cloneOnStart} is false, so a missing or blank {@code CONFIG_REPO_URI} + * would otherwise start the process and defer Git work to the first request. + * Call this check during default-profile startup, before any clone.

+ * + *

Operators: set {@code CONFIG_REPO_URI} to the reviewed Git URI, then start + * the default profile. Use the {@code native} profile only for local fixtures + * that must not depend on a remote.

+ */ +public final class ConfigServerRepositoryAuthority { + + static final String MISSING_AUTHORITY_MESSAGE = + "CONFIG_REPO_URI must name a deployment-owned Git repository; " + + "blank, unresolved, or demo authority is rejected before remote Git access"; + + private static final String UNRESOLVED_PLACEHOLDER = "${CONFIG_REPO_URI"; + private static final String DEMO_REMOTE = "your-repo/config-repo.git"; + + private ConfigServerRepositoryAuthority() { + } + + /** + * Rejects repository values that are not an operator-supplied destination. + * + * @param repositoryUri bound {@code spring.cloud.config.server.git.uri} value + * @throws IllegalStateException when the URI is missing, blank, still a + * placeholder, or the retired demo remote + */ + public static void requireExplicitRepository(String repositoryUri) { + if (repositoryUri == null + || repositoryUri.isBlank() + || repositoryUri.contains(UNRESOLVED_PLACEHOLDER) + || repositoryUri.contains(DEMO_REMOTE)) { + throw new IllegalStateException(MISSING_AUTHORITY_MESSAGE); + } + } +} diff --git a/config-server/src/main/java/com/xtrmetl/config/ConfigServerRepositoryAuthorityValidator.java b/config-server/src/main/java/com/xtrmetl/config/ConfigServerRepositoryAuthorityValidator.java new file mode 100644 index 00000000..8c4708fe --- /dev/null +++ b/config-server/src/main/java/com/xtrmetl/config/ConfigServerRepositoryAuthorityValidator.java @@ -0,0 +1,35 @@ +package com.xtrmetl.config; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +/** + * Fails default-profile startup when Git repository authority is not explicit. + * + *

The {@code native} profile is excluded so reviewed local fixtures can start + * without a remote. Do not use {@code native} as a production fallback.

+ */ +@Component +@Profile("!native") +public class ConfigServerRepositoryAuthorityValidator implements InitializingBean { + + private final String repositoryUri; + + /** + * @param repositoryUri bound Git backend URI; empty when unset + */ + public ConfigServerRepositoryAuthorityValidator( + @Value("${spring.cloud.config.server.git.uri:}") String repositoryUri) { + this.repositoryUri = repositoryUri; + } + + /** + * Rejects blank, unresolved, or demo repository authority before Git access. + */ + @Override + public void afterPropertiesSet() { + ConfigServerRepositoryAuthority.requireExplicitRepository(repositoryUri); + } +} diff --git a/config-server/src/main/resources/application.yml b/config-server/src/main/resources/application.yml index 4a5143ec..3f2fa267 100644 --- a/config-server/src/main/resources/application.yml +++ b/config-server/src/main/resources/application.yml @@ -8,6 +8,7 @@ spring: config: server: git: + # Set CONFIG_REPO_URI to a reviewed Git URI before starting the default profile. uri: ${CONFIG_REPO_URI} eureka: diff --git a/config-server/src/test/java/com/xtrmetl/config/ConfigServerRepositoryAuthorityLiveTest.java b/config-server/src/test/java/com/xtrmetl/config/ConfigServerRepositoryAuthorityLiveTest.java index 11930902..5ef6ca26 100644 --- a/config-server/src/test/java/com/xtrmetl/config/ConfigServerRepositoryAuthorityLiveTest.java +++ b/config-server/src/test/java/com/xtrmetl/config/ConfigServerRepositoryAuthorityLiveTest.java @@ -1,34 +1,94 @@ package com.xtrmetl.config; import org.junit.jupiter.api.Test; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.WebApplicationType; +import org.springframework.context.ConfigurableApplicationContext; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; - -import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; /** - * Verifies that the runnable Config Server never invents a remote Git repository authority. + * Starts the default Git-backed Config Server and proves blank repository + * authority cannot become a running process. + * + *

Operators: set {@code CONFIG_REPO_URI} to a deployment-owned Git URI + * before starting the default profile. Use {@code native} only for reviewed + * local fixtures that do not need a remote.

*/ class ConfigServerRepositoryAuthorityLiveTest { @Test - void missingRepositoryAuthorityFailsClosedWithoutDemoRemote() throws IOException { - String applicationYaml = Files.readString(Path.of("src/main/resources/application.yml")); - - assertTrue( - applicationYaml.contains("uri: ${CONFIG_REPO_URI}"), - "Config Server must require an explicitly supplied CONFIG_REPO_URI" - ); - assertFalse( - applicationYaml.contains("your-repo/config-repo.git"), - "A demo remote must never become a runnable fallback" - ); - assertFalse( - applicationYaml.contains("CONFIG_REPO_URI:https://"), - "Missing repository authority must fail before implicit remote Git access" - ); + void blankRepositoryUriFailsClosedBeforeGitAccess() { + SpringApplication application = new SpringApplication(ConfigServerApplication.class); + application.setWebApplicationType(WebApplicationType.SERVLET); + ConfigurableApplicationContext context = null; + try { + context = application.run( + "--server.port=0", + "--eureka.client.enabled=false", + "--eureka.client.register-with-eureka=false", + "--eureka.client.fetch-registry=false", + "--spring.cloud.config.server.git.uri=", + "--spring.cloud.config.server.git.clone-on-start=false" + ); + fail("Blank CONFIG_REPO_URI must stop Config Server before Git access"); + } catch (Exception ex) { + assertTrue( + containsAuthorityFailure(ex), + () -> "Startup must name the missing repository authority, but failed with: " + ex + ); + } finally { + if (context != null) { + context.close(); + } + } + } + + @Test + void unsetRepositoryUriFailsClosedBeforeGitAccess() { + SpringApplication application = new SpringApplication(ConfigServerApplication.class); + application.setWebApplicationType(WebApplicationType.SERVLET); + ConfigurableApplicationContext context = null; + try { + context = application.run( + "--server.port=0", + "--eureka.client.enabled=false", + "--eureka.client.register-with-eureka=false", + "--eureka.client.fetch-registry=false" + ); + fail("Unset CONFIG_REPO_URI must stop Config Server before Git access"); + } catch (Exception ex) { + assertTrue( + containsAuthorityFailure(ex) || containsUnresolvedPlaceholder(ex), + () -> "Startup must fail closed without a repository URI, but failed with: " + ex + ); + } finally { + if (context != null) { + context.close(); + } + } + } + + private static boolean containsAuthorityFailure(Throwable thrown) { + return messageChain(thrown).contains("CONFIG_REPO_URI must name a deployment-owned Git repository"); + } + + private static boolean containsUnresolvedPlaceholder(Throwable thrown) { + String messages = messageChain(thrown); + return messages.contains("Could not resolve placeholder 'CONFIG_REPO_URI'") + || messages.contains("Could not resolve placeholder 'spring.cloud.config.server.git.uri'"); + } + + private static String messageChain(Throwable thrown) { + assertNotNull(thrown); + StringBuilder messages = new StringBuilder(); + for (Throwable current = thrown; current != null; current = current.getCause()) { + if (current.getMessage() != null) { + messages.append(current.getMessage()).append('\n'); + } + } + return messages.toString(); } } diff --git a/config-server/src/test/java/com/xtrmetl/config/ConfigServerRepositoryAuthorityTest.java b/config-server/src/test/java/com/xtrmetl/config/ConfigServerRepositoryAuthorityTest.java new file mode 100644 index 00000000..15616502 --- /dev/null +++ b/config-server/src/test/java/com/xtrmetl/config/ConfigServerRepositoryAuthorityTest.java @@ -0,0 +1,52 @@ +package com.xtrmetl.config; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Exercises the destination-authority predicate against realistic operator values. + */ +class ConfigServerRepositoryAuthorityTest { + + @Test + void rejectsMissingBlankUnresolvedAndDemoRemotes() { + assertEquals( + ConfigServerRepositoryAuthority.MISSING_AUTHORITY_MESSAGE, + assertThrows( + IllegalStateException.class, + () -> ConfigServerRepositoryAuthority.requireExplicitRepository(null) + ).getMessage() + ); + assertThrows( + IllegalStateException.class, + () -> ConfigServerRepositoryAuthority.requireExplicitRepository("") + ); + assertThrows( + IllegalStateException.class, + () -> ConfigServerRepositoryAuthority.requireExplicitRepository(" ") + ); + assertThrows( + IllegalStateException.class, + () -> ConfigServerRepositoryAuthority.requireExplicitRepository("${CONFIG_REPO_URI}") + ); + assertThrows( + IllegalStateException.class, + () -> ConfigServerRepositoryAuthority.requireExplicitRepository( + "https://github.com/your-repo/config-repo.git" + ) + ); + } + + @Test + void acceptsExplicitHttpsAndFileAuthorities() { + assertDoesNotThrow(() -> ConfigServerRepositoryAuthority.requireExplicitRepository( + "https://git.example.internal/config-repo.git" + )); + assertDoesNotThrow(() -> ConfigServerRepositoryAuthority.requireExplicitRepository( + "file:/opt/reviewed-config-repo" + )); + } +} diff --git a/config-server/src/test/java/com/xtrmetl/config/ConfigServerRepositoryAuthorityValidatorTest.java b/config-server/src/test/java/com/xtrmetl/config/ConfigServerRepositoryAuthorityValidatorTest.java new file mode 100644 index 00000000..2a32d6dd --- /dev/null +++ b/config-server/src/test/java/com/xtrmetl/config/ConfigServerRepositoryAuthorityValidatorTest.java @@ -0,0 +1,26 @@ +package com.xtrmetl.config; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Verifies the startup validator delegates the same fail-closed authority check. + */ +class ConfigServerRepositoryAuthorityValidatorTest { + + @Test + void afterPropertiesSetRejectsBlankAuthority() { + ConfigServerRepositoryAuthorityValidator validator = + new ConfigServerRepositoryAuthorityValidator(""); + assertThrows(IllegalStateException.class, validator::afterPropertiesSet); + } + + @Test + void afterPropertiesSetAcceptsExplicitAuthority() { + ConfigServerRepositoryAuthorityValidator validator = + new ConfigServerRepositoryAuthorityValidator("https://git.example.internal/config-repo.git"); + assertDoesNotThrow(validator::afterPropertiesSet); + } +} diff --git a/config-server/src/test/java/com/xtrmetl/config/ConfigServerRepositoryConfigurationTest.java b/config-server/src/test/java/com/xtrmetl/config/ConfigServerRepositoryConfigurationTest.java index 882ecab8..9646fa38 100644 --- a/config-server/src/test/java/com/xtrmetl/config/ConfigServerRepositoryConfigurationTest.java +++ b/config-server/src/test/java/com/xtrmetl/config/ConfigServerRepositoryConfigurationTest.java @@ -48,6 +48,8 @@ void repositoryAuthorityHasSourceBackedDoctoring() throws IOException { assertTrue(doctoring.contains("CONFIG_REPO_URI")); assertTrue(doctoring.contains("cloneOnStart")); assertTrue(doctoring.contains("skipSslValidation")); + assertTrue(doctoring.contains("ConfigServerRepositoryAuthorityValidator")); + assertTrue(doctoring.contains("blank")); assertTrue(doctoring.contains("https://docs.spring.io/spring-cloud-config/reference/server/environment-repository/git-backend.html")); assertTrue(doctoring.contains("https://docs.spring.io/spring-cloud-config/reference/server/security.html")); assertTrue(doctoring.contains("APA 7")); diff --git a/docs/doctoring/config-server-repository-authority.md b/docs/doctoring/config-server-repository-authority.md index b9b0f568..59511ce6 100644 --- a/docs/doctoring/config-server-repository-authority.md +++ b/docs/doctoring/config-server-repository-authority.md @@ -1,19 +1,23 @@ # Config Server repository authority doctoring -**Capability status:** `active_pr` via replacement PR #322; not `implemented_on_develop` until protected integration. -**Protected baseline assessed:** `develop@d6c6665163eabe1b5eca80556c6963bafd6b2625` +**Capability status:** `active_pr` via repair of replacement PR #322; not `implemented_on_develop` until protected integration. +**Protected baseline assessed:** `develop@e8373b7193019e72b7a860c9f14d109fe7963ee7` **Component:** Spring Cloud Config 5.0.4, managed through the repository's Spring Cloud 2025.0.3 release train ## Decision boundary -mightyETL's independently runnable Config Server Git backend must have explicit deployment-owned repository authority. The deployable property `spring.cloud.config.server.git.uri` therefore uses only `${CONFIG_REPO_URI}` and has no example, demo, guessed, or implicit remote fallback. Missing repository authority must fail closed before remote Git access instead of silently selecting a destination that the operator did not approve. +mightyETL's independently runnable Config Server Git backend must have explicit deployment-owned repository authority. The deployable property `spring.cloud.config.server.git.uri` uses only `${CONFIG_REPO_URI}` and has no example, demo, guessed, or implicit remote fallback. Missing, blank, unresolved, or demo repository authority must fail closed at default-profile startup, before remote Git access. This is a narrow destination-authority decision, not a claim that Config Server is part of the default supported topology. The protected Compose profile does not establish Config Server as a required production dependency. A later product decision may promote, constrain, or retire the module, but none of those outcomes requires retaining a fake repository fallback. +Operators: export `CONFIG_REPO_URI` to the reviewed Git URI, then start the default profile. Use the `native` profile only for local fixtures that must not depend on a remote. Do not start the default profile with a blank secret. + ## Root cause and rejected alternatives The prior configuration used a syntactically valid default resembling `https://github.com/your-repo/config-repo.git`. That deferred a missing deployment decision into an outbound network attempt while providing no real ownership, provenance, credential, availability, change-control, or recovery contract. +Removing the demo default alone is not fail-closed. On Spring Cloud Config 5.0.4, `JGitEnvironmentRepository.afterPropertiesSet` accepts a non-null empty URI when `cloneOnStart` is false. Local evidence on `50ddcc0` showed both unset and blank `CONFIG_REPO_URI` starting Tomcat and exposing `/actuator` before any Git work. Spring also left `${CONFIG_REPO_URI}` unresolved instead of failing Environment bootstrap, so a YAML token change cannot be the only control. + Rejected alternatives: - invent a ContextualWisdomLab repository URL, private endpoint, username, password, token, SSH key, certificate, or trust root; @@ -21,15 +25,16 @@ Rejected alternatives: - embed credentials in `CONFIG_REPO_URI` or repository source; - enable `skipSslValidation` to make an untrusted repository reachable; - discover repository authority through mutable remote bootstrap code; -- treat repository credentials as proof that a destination is authorized. +- treat repository credentials as proof that a destination is authorized; +- treat a YAML `String.contains` test as live fail-closed evidence. -The selected remedy changes only repository selection. Config Server HTTP authentication, Git credentials, trust material, retry and timeout policy, readiness, repository support status, and service-to-service identity remain separate controls. +The selected remedy keeps the explicit `${CONFIG_REPO_URI}` token and adds `ConfigServerRepositoryAuthorityValidator` on every non-`native` profile. That bean rejects blank, unresolved `${CONFIG_REPO_URI...}`, and the retired demo remote before JGit can run. Config Server HTTP authentication, Git credentials, trust material, retry and timeout policy, readiness, repository support status, and service-to-service identity remain separate controls. ## Spring Cloud Config 5.0.4 contract Spring Cloud Config documents `spring.cloud.config.server.git.uri` as the Git environment-repository location. Local filesystem repositories can be useful for deliberately configured development or testing, while production repository location and access remain deployment decisions. mightyETL externalizes this authority through `CONFIG_REPO_URI` rather than assigning a product-owned demo default. -`cloneOnStart` can surface an invalid repository during startup instead of first request. Replacement PR #322 deliberately does not enable it because that would change startup and network-availability semantics beyond the demonstrated fake-fallback defect. If Config Server becomes a supported profile, fail-early cloning must be evaluated together with bounded connect/fetch timeouts, readiness, retry, recovery, and SLO policy. +`cloneOnStart` can surface an invalid repository during startup instead of first request. This repair still does not enable it, because fail-early cloning changes startup and network-availability semantics beyond destination authority. Blank-authority rejection is a local configuration check and does not need a clone. If Config Server becomes a supported profile, fail-early cloning must be evaluated together with bounded connect/fetch timeouts, readiness, retry, recovery, and SLO policy. TLS certificate validation remains enabled. The documented `skipSslValidation` escape hatch must not become a convenience default. Private Git services needing custom roots require explicit trust-material provenance and rotation rather than global certificate-verification bypass. @@ -37,11 +42,13 @@ Git credentials are deployment secrets, not repository authority. They must be e ## Failure, privacy, and operability semantics -With no fallback, a missing `CONFIG_REPO_URI` is a deployment-configuration failure. That failure is safer and more actionable than contacting an invented external repository and possibly serving misleading configuration. +A missing or blank `CONFIG_REPO_URI` on the default profile is a deployment-configuration failure. The process must stop with a finite message that names `CONFIG_REPO_URI`. That failure is safer and more actionable than contacting an invented external repository or serving misleading configuration from an empty URI. + +The `native` profile remains independently startable without `CONFIG_REPO_URI` so inbound-security fixtures and local filesystem backends keep working. Do not use `native` to bypass destination authority in a composed production topology. Repository URLs and provider exceptions can expose internal hostnames, usernames, paths, query parameters, or credentials. Ordinary observability should retain finite configuration and fetch outcome classifications rather than raw credential-bearing URLs or unrestricted exception text. This is purpose-bound diagnostic minimization, not blanket PII masking. -Rollback must never restore the demo remote. When an approved repository is unavailable, the safe choices are to disable or not deploy Config Server, restore the approved repository service, or supply another explicitly authorized repository through a reviewed deployment change. A deliberate local-development `file:` URI belongs in an explicit profile, not in a production fallback. +Rollback must never restore the demo remote. When an approved repository is unavailable, the safe choices are to disable or not deploy Config Server, restore the approved repository service, or supply another explicitly authorized repository through a reviewed deployment change. A deliberate local-development `file:` URI belongs in an explicit operator-supplied `CONFIG_REPO_URI` or the `native` profile, not in a production fallback. ## Standalone and modular MSA implications @@ -51,14 +58,16 @@ No central orchestrator, gateway, or sibling service acquires authority to rewri ## Evidence and replacement lineage -`ConfigServerRepositoryAuthorityLiveTest` on replacement PR #322 establishes current-base RED against the real deployable YAML. `ConfigServerRepositoryConfigurationTest` preserves the earlier behavior and doctoring contracts from PR #189. The replacement branch carries both tests plus the one-line production correction and this source-backed evidence; old PR #189 must not merge separately after exact unique-work preservation and current-head acceptance are proven. +`ConfigServerRepositoryAuthorityLiveTest` starts `ConfigServerApplication` on the default servlet profile and requires startup failure for unset and blank `CONFIG_REPO_URI`. `ConfigServerRepositoryAuthorityTest` and `ConfigServerRepositoryAuthorityValidatorTest` cover null, whitespace, unresolved placeholder, demo remote, and explicit `https` / `file:` values. `ConfigServerRepositoryConfigurationTest` keeps the YAML token and doctoring contract from PR #189 / #322. Old PR #189 must not merge separately after this unique work is accepted. Canonical PRD, TRD, Architecture, UML, Security, Threat Model, Operability, and Traceability must represent this capability as `active_pr` until protected integration. The documentation must not infer that Config Server became a shipped default component merely because repository authority was hardened. ## References (APA 7) +Spring Boot. (2026). *Externalized configuration*. https://docs.spring.io/spring-boot/reference/features/external-config.html + +Spring Cloud Config. (2026). *Config Server (Spring Cloud Config 5.0.4)*. https://docs.spring.io/spring-cloud-config/reference/server.html + Spring Cloud Config. (2026). *Git backend (Spring Cloud Config 5.0.4)*. https://docs.spring.io/spring-cloud-config/reference/server/environment-repository/git-backend.html Spring Cloud Config. (2026). *Security (Spring Cloud Config 5.0.4)*. https://docs.spring.io/spring-cloud-config/reference/server/security.html - -Spring Cloud Config. (2026). *Config Server (Spring Cloud Config 5.0.4)*. https://docs.spring.io/spring-cloud-config/reference/server.html From 4df44068a37fac6af0a83b9fb8618c89419b4713 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 20:04:38 -0700 Subject: [PATCH 3/9] test(config): reproduce repository authority edge cases --- .../ConfigServerRepositoryAuthorityTest.java | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/config-server/src/test/java/com/xtrmetl/config/ConfigServerRepositoryAuthorityTest.java b/config-server/src/test/java/com/xtrmetl/config/ConfigServerRepositoryAuthorityTest.java index 15616502..4807674a 100644 --- a/config-server/src/test/java/com/xtrmetl/config/ConfigServerRepositoryAuthorityTest.java +++ b/config-server/src/test/java/com/xtrmetl/config/ConfigServerRepositoryAuthorityTest.java @@ -32,12 +32,28 @@ void rejectsMissingBlankUnresolvedAndDemoRemotes() { IllegalStateException.class, () -> ConfigServerRepositoryAuthority.requireExplicitRepository("${CONFIG_REPO_URI}") ); - assertThrows( - IllegalStateException.class, - () -> ConfigServerRepositoryAuthority.requireExplicitRepository( - "https://github.com/your-repo/config-repo.git" - ) - ); + for (String demoRemote : new String[] { + "https://github.com/your-repo/config-repo.git", + "https://github.com/your-repo/config-repo", + "HTTPS://GITHUB.COM/YOUR-REPO/CONFIG-REPO.GIT", + "https://github.com/your-repo/config-repo/" + }) { + assertThrows( + IllegalStateException.class, + () -> ConfigServerRepositoryAuthority.requireExplicitRepository(demoRemote), + () -> "Retired demo repository must be rejected: " + demoRemote + ); + } + } + + @Test + void demoRepositoryMatchingDoesNotRejectUnrelatedPaths() { + assertDoesNotThrow(() -> ConfigServerRepositoryAuthority.requireExplicitRepository( + "https://github.com/acme/your-repo/config-repo.git" + )); + assertDoesNotThrow(() -> ConfigServerRepositoryAuthority.requireExplicitRepository( + "https://github.com/your-repo/config-repo-backup.git" + )); } @Test From 3dd4b9300733dca83f5a0e4825f39bf5cb7db1a6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 20:05:09 -0700 Subject: [PATCH 4/9] test(config): reproduce pre-Git and mixed-profile failures --- ...nfigServerRepositoryAuthorityLiveTest.java | 126 +++++++++++------- 1 file changed, 81 insertions(+), 45 deletions(-) diff --git a/config-server/src/test/java/com/xtrmetl/config/ConfigServerRepositoryAuthorityLiveTest.java b/config-server/src/test/java/com/xtrmetl/config/ConfigServerRepositoryAuthorityLiveTest.java index 5ef6ca26..2f8897c2 100644 --- a/config-server/src/test/java/com/xtrmetl/config/ConfigServerRepositoryAuthorityLiveTest.java +++ b/config-server/src/test/java/com/xtrmetl/config/ConfigServerRepositoryAuthorityLiveTest.java @@ -4,66 +4,108 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.WebApplicationType; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.env.StandardEnvironment; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; /** - * Starts the default Git-backed Config Server and proves blank repository - * authority cannot become a running process. + * Starts the Config Server and proves repository/profile authority fails closed + * before a Git backend can become active. * *

Operators: set {@code CONFIG_REPO_URI} to a deployment-owned Git URI - * before starting the default profile. Use {@code native} only for reviewed - * local fixtures that do not need a remote.

+ * before starting the default profile. Use {@code native} only as the sole + * active profile for reviewed local fixtures that do not need a remote.

*/ class ConfigServerRepositoryAuthorityLiveTest { @Test void blankRepositoryUriFailsClosedBeforeGitAccess() { - SpringApplication application = new SpringApplication(ConfigServerApplication.class); - application.setWebApplicationType(WebApplicationType.SERVLET); - ConfigurableApplicationContext context = null; - try { - context = application.run( - "--server.port=0", - "--eureka.client.enabled=false", - "--eureka.client.register-with-eureka=false", - "--eureka.client.fetch-registry=false", - "--spring.cloud.config.server.git.uri=", - "--spring.cloud.config.server.git.clone-on-start=false" - ); - fail("Blank CONFIG_REPO_URI must stop Config Server before Git access"); - } catch (Exception ex) { - assertTrue( - containsAuthorityFailure(ex), - () -> "Startup must name the missing repository authority, but failed with: " + ex - ); - } finally { - if (context != null) { - context.close(); - } - } + Exception failure = runExpectingFailure( + applicationWithoutInheritedConfigRepoUri(), + "--server.port=0", + "--eureka.client.enabled=false", + "--eureka.client.register-with-eureka=false", + "--eureka.client.fetch-registry=false", + "--spring.cloud.config.server.git.uri=", + "--spring.cloud.config.server.git.clone-on-start=true" + ); + + assertTrue( + containsAuthorityFailure(failure), + () -> "Blank authority must fail with the repository-authority message: " + failure + ); + } + + @Test + void unsetRepositoryUriFailsClosedWithoutInheritedEnvironmentAuthority() { + Exception failure = runExpectingFailure( + applicationWithoutInheritedConfigRepoUri(), + "--server.port=0", + "--eureka.client.enabled=false", + "--eureka.client.register-with-eureka=false", + "--eureka.client.fetch-registry=false" + ); + + assertTrue( + containsAuthorityFailure(failure), + () -> "Unset authority must fail with the repository-authority message: " + failure + ); } @Test - void unsetRepositoryUriFailsClosedBeforeGitAccess() { + void retiredDemoUriFailsBeforeCloneOnStartCanContactGit() { + Exception failure = runExpectingFailure( + applicationWithoutInheritedConfigRepoUri(), + "--server.port=0", + "--eureka.client.enabled=false", + "--eureka.client.register-with-eureka=false", + "--eureka.client.fetch-registry=false", + "--spring.cloud.config.server.git.uri=https://github.com/your-repo/config-repo.git", + "--spring.cloud.config.server.git.clone-on-start=true" + ); + + assertTrue( + containsAuthorityFailure(failure), + () -> "Demo URI must be rejected before JGit clone-on-start: " + failure + ); + } + + @Test + void nativeProfileCannotBeCombinedWithAnotherActiveProfile() { + Exception failure = runExpectingFailure( + applicationWithoutInheritedConfigRepoUri(), + "--server.port=0", + "--eureka.client.enabled=false", + "--eureka.client.register-with-eureka=false", + "--eureka.client.fetch-registry=false", + "--spring.profiles.active=native,default" + ); + + assertTrue( + messageChain(failure).contains("native profile must be the only active profile"), + () -> "Mixed native profile startup must fail closed: " + failure + ); + } + + private static SpringApplication applicationWithoutInheritedConfigRepoUri() { SpringApplication application = new SpringApplication(ConfigServerApplication.class); application.setWebApplicationType(WebApplicationType.SERVLET); + StandardEnvironment environment = new StandardEnvironment(); + environment.getPropertySources().remove(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME); + application.setEnvironment(environment); + return application; + } + + private static Exception runExpectingFailure(SpringApplication application, String... args) { ConfigurableApplicationContext context = null; try { - context = application.run( - "--server.port=0", - "--eureka.client.enabled=false", - "--eureka.client.register-with-eureka=false", - "--eureka.client.fetch-registry=false" - ); - fail("Unset CONFIG_REPO_URI must stop Config Server before Git access"); + context = application.run(args); + fail("Config Server startup was expected to fail closed"); + throw new AssertionError("unreachable"); } catch (Exception ex) { - assertTrue( - containsAuthorityFailure(ex) || containsUnresolvedPlaceholder(ex), - () -> "Startup must fail closed without a repository URI, but failed with: " + ex - ); + return ex; } finally { if (context != null) { context.close(); @@ -75,12 +117,6 @@ private static boolean containsAuthorityFailure(Throwable thrown) { return messageChain(thrown).contains("CONFIG_REPO_URI must name a deployment-owned Git repository"); } - private static boolean containsUnresolvedPlaceholder(Throwable thrown) { - String messages = messageChain(thrown); - return messages.contains("Could not resolve placeholder 'CONFIG_REPO_URI'") - || messages.contains("Could not resolve placeholder 'spring.cloud.config.server.git.uri'"); - } - private static String messageChain(Throwable thrown) { assertNotNull(thrown); StringBuilder messages = new StringBuilder(); From 379a3b195adee7504d31a16b0c21211bccaf3b65 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 20:05:36 -0700 Subject: [PATCH 5/9] test(config): require early authority guard registration --- ...nfigServerRepositoryConfigurationTest.java | 36 ++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/config-server/src/test/java/com/xtrmetl/config/ConfigServerRepositoryConfigurationTest.java b/config-server/src/test/java/com/xtrmetl/config/ConfigServerRepositoryConfigurationTest.java index 9646fa38..f53ec818 100644 --- a/config-server/src/test/java/com/xtrmetl/config/ConfigServerRepositoryConfigurationTest.java +++ b/config-server/src/test/java/com/xtrmetl/config/ConfigServerRepositoryConfigurationTest.java @@ -6,6 +6,7 @@ import java.nio.file.Files; import java.nio.file.Path; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -18,17 +19,34 @@ class ConfigServerRepositoryConfigurationTest { void configRepositoryMustBeExplicitAndHaveNoDemoFallback() throws IOException { String applicationYaml = Files.readString(Path.of("src/main/resources/application.yml")); - assertTrue( - applicationYaml.contains("uri: ${CONFIG_REPO_URI}"), - "Config Server must require an operator-supplied CONFIG_REPO_URI" + long exactAuthorityTokens = applicationYaml.lines() + .map(String::trim) + .filter("uri: ${CONFIG_REPO_URI}"::equals) + .count(); + assertEquals( + 1L, + exactAuthorityTokens, + "Config Server must use exactly one CONFIG_REPO_URI token without a fallback" + ); + assertFalse( + applicationYaml.contains("${CONFIG_REPO_URI:"), + "CONFIG_REPO_URI must not acquire an implicit default value" ); assertFalse( applicationYaml.contains("your-repo/config-repo.git"), "A demo Git repository must never be a supported runtime fallback" ); - assertFalse( - applicationYaml.contains("CONFIG_REPO_URI:https://"), - "Missing repository authority must fail closed before remote Git access" + } + + @Test + void environmentPostProcessorIsRegisteredBeforeConfigServerBeans() throws IOException { + Path factoriesPath = Path.of("src/main/resources/META-INF/spring.factories"); + assertTrue(Files.exists(factoriesPath), "Repository authority must run as an EnvironmentPostProcessor"); + String factories = Files.readString(factoriesPath); + assertTrue( + factories.contains("org.springframework.boot.env.EnvironmentPostProcessor=\\\n" + + "com.xtrmetl.config.ConfigServerRepositoryAuthorityEnvironmentPostProcessor"), + "EnvironmentPostProcessor registration must bind the repository-authority guard" ); } @@ -48,10 +66,12 @@ void repositoryAuthorityHasSourceBackedDoctoring() throws IOException { assertTrue(doctoring.contains("CONFIG_REPO_URI")); assertTrue(doctoring.contains("cloneOnStart")); assertTrue(doctoring.contains("skipSslValidation")); - assertTrue(doctoring.contains("ConfigServerRepositoryAuthorityValidator")); - assertTrue(doctoring.contains("blank")); + assertTrue(doctoring.contains("ConfigServerRepositoryAuthorityEnvironmentPostProcessor")); + assertTrue(doctoring.contains("unset or blank `CONFIG_REPO_URI`")); assertTrue(doctoring.contains("https://docs.spring.io/spring-cloud-config/reference/server/environment-repository/git-backend.html")); assertTrue(doctoring.contains("https://docs.spring.io/spring-cloud-config/reference/server/security.html")); assertTrue(doctoring.contains("APA 7")); + assertTrue(doctoring.contains("n.d.")); + assertTrue(doctoring.contains("Retrieved August 28, 2026")); } } From 4cf628aa946962daaad134ce45d21df79326236b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 20:06:08 -0700 Subject: [PATCH 6/9] fix(config): normalize repository authority validation --- .../ConfigServerRepositoryAuthority.java | 71 ++++++++++++++++--- 1 file changed, 63 insertions(+), 8 deletions(-) diff --git a/config-server/src/main/java/com/xtrmetl/config/ConfigServerRepositoryAuthority.java b/config-server/src/main/java/com/xtrmetl/config/ConfigServerRepositoryAuthority.java index 570da377..8fe10549 100644 --- a/config-server/src/main/java/com/xtrmetl/config/ConfigServerRepositoryAuthority.java +++ b/config-server/src/main/java/com/xtrmetl/config/ConfigServerRepositoryAuthority.java @@ -1,25 +1,32 @@ package com.xtrmetl.config; +import java.net.URI; +import java.util.Locale; + /** * Decides whether a Config Server Git URI is an explicit deployment-owned authority. * *

Spring Cloud Config 5.0.4 treats a non-null empty URI as valid when * {@code cloneOnStart} is false, so a missing or blank {@code CONFIG_REPO_URI} * would otherwise start the process and defer Git work to the first request. - * Call this check during default-profile startup, before any clone.

+ * Call this check during environment preparation, before Config Server creates + * its Git repository beans.

* *

Operators: set {@code CONFIG_REPO_URI} to the reviewed Git URI, then start - * the default profile. Use the {@code native} profile only for local fixtures - * that must not depend on a remote.

+ * the default profile. Use the {@code native} profile only as the sole active + * profile for local fixtures that must not depend on a remote.

*/ public final class ConfigServerRepositoryAuthority { static final String MISSING_AUTHORITY_MESSAGE = "CONFIG_REPO_URI must name a deployment-owned Git repository; " + "blank, unresolved, or demo authority is rejected before remote Git access"; + static final String MIXED_NATIVE_PROFILE_MESSAGE = + "native profile must be the only active profile when Config Server uses local fixtures"; private static final String UNRESOLVED_PLACEHOLDER = "${CONFIG_REPO_URI"; - private static final String DEMO_REMOTE = "your-repo/config-repo.git"; + private static final String RETIRED_DEMO_HOST = "github.com"; + private static final String RETIRED_DEMO_PATH = "/your-repo/config-repo"; private ConfigServerRepositoryAuthority() { } @@ -32,11 +39,59 @@ private ConfigServerRepositoryAuthority() { * placeholder, or the retired demo remote */ public static void requireExplicitRepository(String repositoryUri) { - if (repositoryUri == null - || repositoryUri.isBlank() - || repositoryUri.contains(UNRESOLVED_PLACEHOLDER) - || repositoryUri.contains(DEMO_REMOTE)) { + if (repositoryUri == null || repositoryUri.isBlank()) { + throw new IllegalStateException(MISSING_AUTHORITY_MESSAGE); + } + String trimmed = repositoryUri.trim(); + if (trimmed.contains(UNRESOLVED_PLACEHOLDER) || isRetiredDemoRemote(trimmed)) { throw new IllegalStateException(MISSING_AUTHORITY_MESSAGE); } } + + /** + * Ensures the local-fixture {@code native} profile cannot be composed with + * another active profile to bypass Git repository authority validation. + * + * @param activeProfiles explicitly active Spring profiles + * @return {@code true} when standalone native mode is active + * @throws IllegalStateException when native is combined with another profile + */ + public static boolean requireSafeProfileComposition(String... activeProfiles) { + boolean nativeActive = false; + int activeCount = 0; + for (String profile : activeProfiles) { + if (profile == null || profile.isBlank()) { + continue; + } + activeCount++; + if ("native".equalsIgnoreCase(profile.trim())) { + nativeActive = true; + } + } + if (nativeActive && activeCount != 1) { + throw new IllegalStateException(MIXED_NATIVE_PROFILE_MESSAGE); + } + return nativeActive; + } + + private static boolean isRetiredDemoRemote(String repositoryUri) { + try { + URI uri = URI.create(repositoryUri); + if (!RETIRED_DEMO_HOST.equalsIgnoreCase(uri.getHost())) { + return false; + } + String path = uri.getPath(); + if (path == null) { + return false; + } + String normalizedPath = path.toLowerCase(Locale.ROOT); + while (normalizedPath.endsWith("/") && normalizedPath.length() > 1) { + normalizedPath = normalizedPath.substring(0, normalizedPath.length() - 1); + } + return RETIRED_DEMO_PATH.equals(normalizedPath) + || (RETIRED_DEMO_PATH + ".git").equals(normalizedPath); + } catch (IllegalArgumentException ignored) { + return false; + } + } } From d12f91b78d53c205ae91fca3c12f5c97345b0bcc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 20:06:26 -0700 Subject: [PATCH 7/9] fix(config): validate repository authority before Git bean creation --- ...toryAuthorityEnvironmentPostProcessor.java | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 config-server/src/main/java/com/xtrmetl/config/ConfigServerRepositoryAuthorityEnvironmentPostProcessor.java diff --git a/config-server/src/main/java/com/xtrmetl/config/ConfigServerRepositoryAuthorityEnvironmentPostProcessor.java b/config-server/src/main/java/com/xtrmetl/config/ConfigServerRepositoryAuthorityEnvironmentPostProcessor.java new file mode 100644 index 00000000..d1ac9c7a --- /dev/null +++ b/config-server/src/main/java/com/xtrmetl/config/ConfigServerRepositoryAuthorityEnvironmentPostProcessor.java @@ -0,0 +1,63 @@ +package com.xtrmetl.config; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.core.Ordered; +import org.springframework.core.env.ConfigurableEnvironment; + +/** + * Validates Config Server repository authority after config data is loaded and + * before the application context creates Spring Cloud Config Git components. + * + *

This boundary intentionally runs before bean initialization so + * {@code clone-on-start=true} cannot contact an unreviewed or retired remote. + * Standalone {@code native} mode remains available for reviewed local fixtures, + * but native cannot be combined with another active profile.

+ */ +public final class ConfigServerRepositoryAuthorityEnvironmentPostProcessor + implements EnvironmentPostProcessor, Ordered { + + private static final String REPOSITORY_URI_PROPERTY = + "spring.cloud.config.server.git.uri"; + + /** + * Runs immediately after Spring Boot has loaded normal ConfigData. + * + * @return the processor order + */ + @Override + public int getOrder() { + return ConfigDataEnvironmentPostProcessor.ORDER + 1; + } + + /** + * Enforces profile composition and Git destination authority before context refresh. + * + * @param environment prepared Spring environment + * @param application application being started + */ + @Override + public void postProcessEnvironment( + ConfigurableEnvironment environment, + SpringApplication application + ) { + boolean nativeOnly = ConfigServerRepositoryAuthority.requireSafeProfileComposition( + environment.getActiveProfiles() + ); + if (nativeOnly) { + return; + } + + String repositoryUri; + try { + repositoryUri = environment.getProperty(REPOSITORY_URI_PROPERTY); + } catch (IllegalArgumentException unresolvedPlaceholder) { + throw new IllegalStateException( + ConfigServerRepositoryAuthority.MISSING_AUTHORITY_MESSAGE, + unresolvedPlaceholder + ); + } + ConfigServerRepositoryAuthority.requireExplicitRepository(repositoryUri); + } +} From 7df95c0382e0037db709ad53b82f3a9c854d5bdf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 20:06:36 -0700 Subject: [PATCH 8/9] fix(config): register early repository authority guard --- config-server/src/main/resources/META-INF/spring.factories | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 config-server/src/main/resources/META-INF/spring.factories diff --git a/config-server/src/main/resources/META-INF/spring.factories b/config-server/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..19c74c50 --- /dev/null +++ b/config-server/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.env.EnvironmentPostProcessor=\ +com.xtrmetl.config.ConfigServerRepositoryAuthorityEnvironmentPostProcessor From 6ae22e91fc8781f3b87e4fa676943720a2d66a8e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 27 Aug 2026 20:07:27 -0700 Subject: [PATCH 9/9] docs(config): correct repository authority evidence and citations --- .../config-server-repository-authority.md | 51 ++++++++++--------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/docs/doctoring/config-server-repository-authority.md b/docs/doctoring/config-server-repository-authority.md index 59511ce6..1f87de3c 100644 --- a/docs/doctoring/config-server-repository-authority.md +++ b/docs/doctoring/config-server-repository-authority.md @@ -1,22 +1,22 @@ # Config Server repository authority doctoring -**Capability status:** `active_pr` via repair of replacement PR #322; not `implemented_on_develop` until protected integration. +**Capability status:** `active_pr` via replacement PR #327; not `implemented_on_develop` until protected integration. **Protected baseline assessed:** `develop@e8373b7193019e72b7a860c9f14d109fe7963ee7` **Component:** Spring Cloud Config 5.0.4, managed through the repository's Spring Cloud 2025.0.3 release train ## Decision boundary -mightyETL's independently runnable Config Server Git backend must have explicit deployment-owned repository authority. The deployable property `spring.cloud.config.server.git.uri` uses only `${CONFIG_REPO_URI}` and has no example, demo, guessed, or implicit remote fallback. Missing, blank, unresolved, or demo repository authority must fail closed at default-profile startup, before remote Git access. +mightyETL's independently runnable Config Server Git backend must have explicit deployment-owned repository authority. The deployable property `spring.cloud.config.server.git.uri` uses only `${CONFIG_REPO_URI}` and has no example, demo, guessed, or implicit remote fallback. Missing, blank, unresolved, or retired demo repository authority must fail closed during environment preparation, before Spring Cloud Config creates Git repository beans or `cloneOnStart` can contact a remote. This is a narrow destination-authority decision, not a claim that Config Server is part of the default supported topology. The protected Compose profile does not establish Config Server as a required production dependency. A later product decision may promote, constrain, or retire the module, but none of those outcomes requires retaining a fake repository fallback. -Operators: export `CONFIG_REPO_URI` to the reviewed Git URI, then start the default profile. Use the `native` profile only for local fixtures that must not depend on a remote. Do not start the default profile with a blank secret. +Operators: export `CONFIG_REPO_URI` to the reviewed Git URI, then start the default profile. Use the `native` profile only as the sole active profile for reviewed local fixtures that must not depend on a remote. Do not start the default profile with an unset or blank `CONFIG_REPO_URI`. Git credentials are separate deployment secrets and must not be embedded in this URI. ## Root cause and rejected alternatives The prior configuration used a syntactically valid default resembling `https://github.com/your-repo/config-repo.git`. That deferred a missing deployment decision into an outbound network attempt while providing no real ownership, provenance, credential, availability, change-control, or recovery contract. -Removing the demo default alone is not fail-closed. On Spring Cloud Config 5.0.4, `JGitEnvironmentRepository.afterPropertiesSet` accepts a non-null empty URI when `cloneOnStart` is false. Local evidence on `50ddcc0` showed both unset and blank `CONFIG_REPO_URI` starting Tomcat and exposing `/actuator` before any Git work. Spring also left `${CONFIG_REPO_URI}` unresolved instead of failing Environment bootstrap, so a YAML token change cannot be the only control. +Removing the demo default alone is not fail-closed. Local evidence showed that Spring Cloud Config can defer Git work when `cloneOnStart` is false, while a bean-level validator is not an ordering guarantee against JGit initialization when `cloneOnStart` is true. A YAML token change and an `InitializingBean` therefore cannot be the only controls. Rejected alternatives: @@ -26,15 +26,18 @@ Rejected alternatives: - enable `skipSslValidation` to make an untrusted repository reachable; - discover repository authority through mutable remote bootstrap code; - treat repository credentials as proof that a destination is authorized; -- treat a YAML `String.contains` test as live fail-closed evidence. +- treat a YAML `String.contains` test as live fail-closed evidence; +- use `native` together with another active profile to bypass Git destination authority. -The selected remedy keeps the explicit `${CONFIG_REPO_URI}` token and adds `ConfigServerRepositoryAuthorityValidator` on every non-`native` profile. That bean rejects blank, unresolved `${CONFIG_REPO_URI...}`, and the retired demo remote before JGit can run. Config Server HTTP authentication, Git credentials, trust material, retry and timeout policy, readiness, repository support status, and service-to-service identity remain separate controls. +The selected remedy keeps the exact `${CONFIG_REPO_URI}` token and registers `ConfigServerRepositoryAuthorityEnvironmentPostProcessor` through `META-INF/spring.factories`. It runs immediately after Spring Boot's `ConfigDataEnvironmentPostProcessor`, when ordinary config data has been loaded but before the application context creates Spring Cloud Config Git beans. It rejects blank or unresolved values, the retired `github.com/your-repo/config-repo[.git]` destination case-insensitively, and mixed `native` profile compositions. `ConfigServerRepositoryAuthorityValidator` remains defense in depth for non-native bean initialization. -## Spring Cloud Config 5.0.4 contract +## Spring Boot and Spring Cloud Config contract -Spring Cloud Config documents `spring.cloud.config.server.git.uri` as the Git environment-repository location. Local filesystem repositories can be useful for deliberately configured development or testing, while production repository location and access remain deployment decisions. mightyETL externalizes this authority through `CONFIG_REPO_URI` rather than assigning a product-owned demo default. +Spring Boot 3.5 documents `EnvironmentPostProcessor` as the extension point for changing or validating the prepared `Environment` before the application context is refreshed, with registration through `META-INF/spring.factories`. `ConfigDataEnvironmentPostProcessor.ORDER` is public, so the repository-authority guard can run at `ORDER + 1`, after normal config data has been applied and before context refresh. -`cloneOnStart` can surface an invalid repository during startup instead of first request. This repair still does not enable it, because fail-early cloning changes startup and network-availability semantics beyond destination authority. Blank-authority rejection is a local configuration check and does not need a clone. If Config Server becomes a supported profile, fail-early cloning must be evaluated together with bounded connect/fetch timeouts, readiness, retry, recovery, and SLO policy. +Spring Cloud Config documents `spring.cloud.config.server.git.uri` as the Git environment-repository location. A deliberately configured `file:` repository can be appropriate for development or testing, while production repository location and access remain deployment decisions. mightyETL externalizes this authority through `CONFIG_REPO_URI` rather than assigning a product-owned demo default. + +`cloneOnStart` can cause Git access during startup. The authority guard therefore runs before Config Server Git beans rather than relying on a bean whose relative initialization order is not a security boundary. The repair does not itself enable cloning or redefine retry, timeout, readiness, recovery, or SLO policy. TLS certificate validation remains enabled. The documented `skipSslValidation` escape hatch must not become a convenience default. Private Git services needing custom roots require explicit trust-material provenance and rotation rather than global certificate-verification bypass. @@ -42,32 +45,30 @@ Git credentials are deployment secrets, not repository authority. They must be e ## Failure, privacy, and operability semantics -A missing or blank `CONFIG_REPO_URI` on the default profile is a deployment-configuration failure. The process must stop with a finite message that names `CONFIG_REPO_URI`. That failure is safer and more actionable than contacting an invented external repository or serving misleading configuration from an empty URI. - -The `native` profile remains independently startable without `CONFIG_REPO_URI` so inbound-security fixtures and local filesystem backends keep working. Do not use `native` to bypass destination authority in a composed production topology. - -Repository URLs and provider exceptions can expose internal hostnames, usernames, paths, query parameters, or credentials. Ordinary observability should retain finite configuration and fetch outcome classifications rather than raw credential-bearing URLs or unrestricted exception text. This is purpose-bound diagnostic minimization, not blanket PII masking. +A missing or blank `CONFIG_REPO_URI` on a non-native profile is a deployment-configuration failure. The process must stop with a finite message that names `CONFIG_REPO_URI`. The retired demo GitHub path must also stop locally even when `cloneOnStart=true`, so a configuration defect cannot become an outbound Git request. -Rollback must never restore the demo remote. When an approved repository is unavailable, the safe choices are to disable or not deploy Config Server, restore the approved repository service, or supply another explicitly authorized repository through a reviewed deployment change. A deliberate local-development `file:` URI belongs in an explicit operator-supplied `CONFIG_REPO_URI` or the `native` profile, not in a production fallback. +Standalone `native` remains independently startable without `CONFIG_REPO_URI` for reviewed local filesystem fixtures. If `native` is combined with any other active profile, startup fails closed with a finite profile-composition message. Do not use `native` to bypass destination authority in a composed production topology. -## Standalone and modular MSA implications +Repository URLs and provider exceptions can expose internal hostnames, usernames, paths, query parameters, or credentials. Ordinary observability should retain finite configuration and fetch outcome classifications rather than raw credential-bearing URLs or unrestricted exception text. -The Config Server module remains independently buildable. ETL, CDC, gateway, and discovery services must not silently become dependent on it merely because the module exists. A composed MSA profile that adopts Config Server needs explicit dependency and failure-domain documentation, inbound authentication, repository authority, trust and credential provenance, readiness, rollback, and tests proving promised standalone modes remain available. - -No central orchestrator, gateway, or sibling service acquires authority to rewrite Config Server destination configuration at runtime without a versioned, authenticated deployment contract. +Rollback must never restore the demo remote. When an approved repository is unavailable, the safe choices are to disable or not deploy Config Server, restore the approved repository service, or supply another explicitly authorized repository through a reviewed deployment change. ## Evidence and replacement lineage -`ConfigServerRepositoryAuthorityLiveTest` starts `ConfigServerApplication` on the default servlet profile and requires startup failure for unset and blank `CONFIG_REPO_URI`. `ConfigServerRepositoryAuthorityTest` and `ConfigServerRepositoryAuthorityValidatorTest` cover null, whitespace, unresolved placeholder, demo remote, and explicit `https` / `file:` values. `ConfigServerRepositoryConfigurationTest` keeps the YAML token and doctoring contract from PR #189 / #322. Old PR #189 must not merge separately after this unique work is accepted. +`ConfigServerRepositoryAuthorityLiveTest` isolates inherited `CONFIG_REPO_URI`, asserts finite authority failure for unset and blank values, exercises `clone-on-start=true` against the retired demo URI, and rejects mixed `native` profile startup. `ConfigServerRepositoryAuthorityTest` covers null, whitespace, unresolved placeholder, exact retired GitHub destinations with case and `.git` variants, non-matching paths, and explicit `https` / `file:` values. `ConfigServerRepositoryConfigurationTest` requires the exact YAML token with no default colon and the registered early environment guard. -Canonical PRD, TRD, Architecture, UML, Security, Threat Model, Operability, and Traceability must represent this capability as `active_pr` until protected integration. The documentation must not infer that Config Server became a shipped default component merely because repository authority was hardened. +Canonical PRD, TRD, Architecture, Security, Operability, and Traceability must represent this capability as `active_pr` until protected integration. The documentation must not infer that Config Server became a shipped default component merely because repository authority was hardened. ## References (APA 7) -Spring Boot. (2026). *Externalized configuration*. https://docs.spring.io/spring-boot/reference/features/external-config.html +Spring Boot. (n.d.). *Externalized configuration*. In *Spring Boot reference documentation*. Retrieved August 28, 2026, from https://docs.spring.io/spring-boot/reference/features/external-config.html + +Spring Boot. (n.d.). *EnvironmentPostProcessor*. In *Spring Boot 3.5 API*. Retrieved August 28, 2026, from https://docs.spring.io/spring-boot/3.5/api/java/org/springframework/boot/env/EnvironmentPostProcessor.html + +Spring Boot. (n.d.). *ConfigDataEnvironmentPostProcessor*. In *Spring Boot 3.5 API*. Retrieved August 28, 2026, from https://docs.spring.io/spring-boot/3.5/api/java/org/springframework/boot/context/config/ConfigDataEnvironmentPostProcessor.html -Spring Cloud Config. (2026). *Config Server (Spring Cloud Config 5.0.4)*. https://docs.spring.io/spring-cloud-config/reference/server.html +Spring Cloud Config. (n.d.). *Git backend*. Retrieved August 28, 2026, from https://docs.spring.io/spring-cloud-config/reference/server/environment-repository/git-backend.html -Spring Cloud Config. (2026). *Git backend (Spring Cloud Config 5.0.4)*. https://docs.spring.io/spring-cloud-config/reference/server/environment-repository/git-backend.html +Spring Cloud Config. (n.d.). *Security*. Retrieved August 28, 2026, from https://docs.spring.io/spring-cloud-config/reference/server/security.html -Spring Cloud Config. (2026). *Security (Spring Cloud Config 5.0.4)*. https://docs.spring.io/spring-cloud-config/reference/server/security.html +Spring Cloud Config. (2026, June 11). *Spring Cloud Config 5.0.4* [Software release]. GitHub. https://github.com/spring-cloud/spring-cloud-config/releases/tag/v5.0.4