From 5885324aa6e7e52f4594effa03bf459be0bb0c0e Mon Sep 17 00:00:00 2001 From: Nene7ko_ <1604009816@qq.com> Date: Mon, 10 Aug 2026 20:18:31 +0800 Subject: [PATCH 1/2] test(stack): prove secure provider registration --- README.md | 8 + components.json | 6 +- compose.yaml | 31 +++- go.mod | 2 +- go.sum | 4 +- scripts/write-ci-env.sh | 5 + tests/backend/invoke_record_test.go | 8 + tests/backend/nacos_tls_fixture_test.go | 200 ++++++++++++++++++++++++ 8 files changed, 255 insertions(+), 9 deletions(-) create mode 100644 tests/backend/nacos_tls_fixture_test.go diff --git a/README.md b/README.md index b8d5c15..ac50c5d 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,14 @@ queryable committed Ledger lineage. Browser success additionally requires the exact production Console build to pass its Playwright suite against that same assembly. +Backend runtime registration uses an ephemeral private PKI generated by the +acceptance process. Runtime A publishes through an authenticated TLS Nacos +endpoint; Runtime B and its replacement publish through an mTLS endpoint. The +Router observes those leases before managed invocation. Negative containers +with a wrong CA, wrong server name, or missing mTLS client identity must fail +before publication and leave no routable instance. Test keys exist only in the +caller-owned runtime directory and are never committed or recorded in Ledger. + The `Core integration` reusable workflow accepts a full Core commit SHA, renders a temporary immutable manifest, and runs both backend and browser acceptance. Core calls it after every merge to `main`; the tracked Stack diff --git a/components.json b/components.json index bf96e28..2b3df40 100644 --- a/components.json +++ b/components.json @@ -1,10 +1,10 @@ { "schemaVersion": "1", - "contractIdentity": "NeKiro/contracts@a737184be63533a578355f98d1b8b94648e05910", + "contractIdentity": "NeKiro/contracts@8c385babc9da4c383a0aaa9efe1daeb0b72dd812", "components": { "core": { "repository": "NeKiro-project/NeKiro", - "commitSha": "a737184be63533a578355f98d1b8b94648e05910" + "commitSha": "8c385babc9da4c383a0aaa9efe1daeb0b72dd812" }, "console": { "repository": "NeKiro-project/NeKiro-Console", @@ -16,7 +16,7 @@ }, "samples": { "repository": "NeKiro-project/NeKiro-Samples", - "commitSha": "9f26d5e674a79b2d89685b05ca4ed02a2c5ba37d" + "commitSha": "1dcfae990564febdb545c4d02408f3f2991aa5a0" }, "transportGo": { "repository": "NeKiro-project/nekiro-a2a-transport-go", diff --git a/compose.yaml b/compose.yaml index ff22b26..73da106 100644 --- a/compose.yaml +++ b/compose.yaml @@ -228,7 +228,7 @@ services: RUNTIME_B_CARD_DIGEST: "${RUNTIME_B_CARD_DIGEST:-}" RUNTIME_B_CANONICAL_ENDPOINT: "${RUNTIME_B_CANONICAL_ENDPOINT:-}" RUNTIME_B_AUDIENCE: "${RUNTIME_B_AUDIENCE:-}" - RUNTIME_B_NACOS_API_ORIGIN: "http://nacos:8848/nacos" + RUNTIME_B_NACOS_API_ORIGIN: "https://host.docker.internal:${NEKIRO_E2E_NACOS_MTLS_PORT:?NEKIRO_E2E_NACOS_MTLS_PORT must be set and non-empty}/nacos" RUNTIME_B_NACOS_NAMESPACE_ID: "nekiro" RUNTIME_B_NACOS_GROUP_NAME: "NEKIRO" RUNTIME_B_NACOS_SERVICE_NAME: "runtime-b" @@ -242,6 +242,10 @@ services: RUNTIME_B_NACOS_IP_DELETE_TIMEOUT_MS: "10000" RUNTIME_B_NACOS_REQUEST_TIMEOUT_MS: "3000" RUNTIME_B_NACOS_AUTH_MODE: "none" + RUNTIME_B_NACOS_TLS_CA_FILE: "/var/run/nekiro-nacos-tls/ca.pem" + RUNTIME_B_NACOS_TLS_SERVER_NAME: "nacos.internal" + RUNTIME_B_NACOS_TLS_CLIENT_CERT_FILE: "/var/run/nekiro-nacos-tls/client.pem" + RUNTIME_B_NACOS_TLS_CLIENT_KEY_FILE: "/var/run/nekiro-nacos-tls/client-key.pem" RUNTIME_B_ROUTER_URL: "http://a2a-router:8081" RUNTIME_B_ROUTER_TOKEN: "${RUNTIME_B_ROUTER_TOKEN:?RUNTIME_B_ROUTER_TOKEN must be set and non-empty}" RUNTIME_B_TARGET_AGENT_ID: "runtime-a" @@ -256,9 +260,14 @@ services: depends_on: a2a-router: condition: service_healthy + extra_hosts: + - "host.docker.internal:host-gateway" + volumes: + - "${NEKIRO_E2E_TLS_ROOT:?NEKIRO_E2E_TLS_ROOT must be set and non-empty}:/var/run/nekiro-nacos-tls:ro" networks: platform-internal: ipv4_address: 172.28.0.12 + local-access: healthcheck: test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8092/readyz"] interval: 5s @@ -279,7 +288,7 @@ services: RUNTIME_B_CARD_DIGEST: "${RUNTIME_B_CARD_DIGEST:-}" RUNTIME_B_CANONICAL_ENDPOINT: "${RUNTIME_B_CANONICAL_ENDPOINT:-}" RUNTIME_B_AUDIENCE: "${RUNTIME_B_AUDIENCE:-}" - RUNTIME_B_NACOS_API_ORIGIN: "http://nacos:8848/nacos" + RUNTIME_B_NACOS_API_ORIGIN: "https://host.docker.internal:${NEKIRO_E2E_NACOS_MTLS_PORT:?NEKIRO_E2E_NACOS_MTLS_PORT must be set and non-empty}/nacos" RUNTIME_B_NACOS_NAMESPACE_ID: "nekiro" RUNTIME_B_NACOS_GROUP_NAME: "NEKIRO" RUNTIME_B_NACOS_SERVICE_NAME: "runtime-b" @@ -293,6 +302,10 @@ services: RUNTIME_B_NACOS_IP_DELETE_TIMEOUT_MS: "10000" RUNTIME_B_NACOS_REQUEST_TIMEOUT_MS: "3000" RUNTIME_B_NACOS_AUTH_MODE: "none" + RUNTIME_B_NACOS_TLS_CA_FILE: "/var/run/nekiro-nacos-tls/ca.pem" + RUNTIME_B_NACOS_TLS_SERVER_NAME: "nacos.internal" + RUNTIME_B_NACOS_TLS_CLIENT_CERT_FILE: "/var/run/nekiro-nacos-tls/client.pem" + RUNTIME_B_NACOS_TLS_CLIENT_KEY_FILE: "/var/run/nekiro-nacos-tls/client-key.pem" RUNTIME_B_ROUTER_URL: "http://a2a-router:8081" RUNTIME_B_ROUTER_TOKEN: "${RUNTIME_B_ROUTER_TOKEN:?RUNTIME_B_ROUTER_TOKEN must be set and non-empty}" RUNTIME_B_TARGET_AGENT_ID: "runtime-a" @@ -307,9 +320,14 @@ services: depends_on: a2a-router: condition: service_healthy + extra_hosts: + - "host.docker.internal:host-gateway" + volumes: + - "${NEKIRO_E2E_TLS_ROOT:?NEKIRO_E2E_TLS_ROOT must be set and non-empty}:/var/run/nekiro-nacos-tls:ro" networks: platform-internal: ipv4_address: 172.28.0.13 + local-access: healthcheck: test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8092/readyz"] interval: 5s @@ -363,7 +381,7 @@ services: RUNTIME_A_CARD_DIGEST: "${RUNTIME_A_CARD_DIGEST:-}" RUNTIME_A_CANONICAL_ENDPOINT: "${RUNTIME_A_CANONICAL_ENDPOINT:-}" RUNTIME_A_AUDIENCE: "${RUNTIME_A_AUDIENCE:-}" - RUNTIME_A_NACOS_API_ORIGIN: "http://nacos:8848/nacos" + RUNTIME_A_NACOS_API_ORIGIN: "https://host.docker.internal:${NEKIRO_E2E_NACOS_TLS_PORT:?NEKIRO_E2E_NACOS_TLS_PORT must be set and non-empty}/nacos" RUNTIME_A_NACOS_NAMESPACE_ID: "nekiro" RUNTIME_A_NACOS_GROUP_NAME: "NEKIRO" RUNTIME_A_NACOS_SERVICE_NAME: "runtime-a" @@ -377,6 +395,8 @@ services: RUNTIME_A_NACOS_IP_DELETE_TIMEOUT_MS: "10000" RUNTIME_A_NACOS_REQUEST_TIMEOUT_MS: "3000" RUNTIME_A_NACOS_AUTH_MODE: "none" + RUNTIME_A_NACOS_TLS_CA_FILE: "/var/run/nekiro-nacos-tls/ca.pem" + RUNTIME_A_NACOS_TLS_SERVER_NAME: "nacos.internal" RUNTIME_A_ROUTER_URL: "http://a2a-router:8081" RUNTIME_A_ROUTER_TOKEN: "${RUNTIME_A_ROUTER_TOKEN:?RUNTIME_A_ROUTER_TOKEN must be set and non-empty}" RUNTIME_A_TARGET_AGENT_ID: "runtime-b" @@ -393,9 +413,14 @@ services: condition: service_healthy runtime-b: condition: service_healthy + extra_hosts: + - "host.docker.internal:host-gateway" + volumes: + - "${NEKIRO_E2E_TLS_ROOT:?NEKIRO_E2E_TLS_ROOT must be set and non-empty}:/var/run/nekiro-nacos-tls:ro" networks: platform-internal: ipv4_address: 172.28.0.14 + local-access: healthcheck: test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8091/readyz"] interval: 5s diff --git a/go.mod b/go.mod index 72b5137..1ff6e7a 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/NeKiro-project/NeKiro-Stack go 1.26.0 require ( - github.com/NeKiro-project/NeKiro v0.0.0-20260810073448-a737184be635 + github.com/NeKiro-project/NeKiro v0.0.0-20260810110658-8c385babc9da github.com/jackc/pgx/v5 v5.10.0 ) diff --git a/go.sum b/go.sum index 4bde0bf..6cec3fb 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= -github.com/NeKiro-project/NeKiro v0.0.0-20260810073448-a737184be635 h1:lNVDKPv040qm4EGN26vet3feeSyvfDmTS51ZUWJnjdI= -github.com/NeKiro-project/NeKiro v0.0.0-20260810073448-a737184be635/go.mod h1:JCIEeiLu52WC/Q5QlcAKmWKRtW7CNLkZ3lV3BAn92Oo= +github.com/NeKiro-project/NeKiro v0.0.0-20260810110658-8c385babc9da h1:jZSoKwcJe8m7dHg5OlJ06R4zf6Rm+OeYWuF2DHpZ5Yc= +github.com/NeKiro-project/NeKiro v0.0.0-20260810110658-8c385babc9da/go.mod h1:JCIEeiLu52WC/Q5QlcAKmWKRtW7CNLkZ3lV3BAn92Oo= github.com/a2aproject/a2a-go v0.3.15 h1:h5YpCiPq3jxQ5rIns7oDjPag3ivP8u817AzdA4F+NiI= github.com/a2aproject/a2a-go v0.3.15/go.mod h1:I7Cm+a1oL+UT6zMoP+roaRE5vdfUa1iQGVN8aSOuZ0I= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/scripts/write-ci-env.sh b/scripts/write-ci-env.sh index f7d5aad..bb8cc93 100755 --- a/scripts/write-ci-env.sh +++ b/scripts/write-ci-env.sh @@ -17,7 +17,9 @@ fi write_common() { config_root="$stack_root/.runtime-config/$compose_project" + tls_root="$stack_root/.runtime-tls/$compose_project" mkdir -p "$config_root" + mkdir -p "$tls_root" printf '%s\n' '{"schemaVersion":"1","revision":"stack-bootstrap-1","targets":[]}' >"$config_root/cfg-v1-cm91dGVyL2luc3RhbmNlLWRpcmVjdG9yeQ.value" cat >>"$output" <<'EOF' POSTGRES_PORT=55432 @@ -56,8 +58,11 @@ NEKIRO_ROUTER_NACOS_REQUEST_TIMEOUT_MS=3000 NEKIRO_ROUTER_NACOS_GRPC_REQUEST_TIMEOUT_MS=3000 NEKIRO_ROUTER_NACOS_PENDING_CHANGES=64 NEKIRO_ROUTER_NACOS_MAX_OBSERVATIONS=1024 +NEKIRO_E2E_NACOS_TLS_PORT=19443 +NEKIRO_E2E_NACOS_MTLS_PORT=19444 EOF printf 'NEKIRO_ROUTER_CONFIG_CENTER_ROOT=%s\n' "$config_root" >>"$output" + printf 'NEKIRO_E2E_TLS_ROOT=%s\n' "$tls_root" >>"$output" printf 'NEKIRO_E2E_COMPOSE_FILE=%s/compose.yaml\n' "$stack_root" >>"$output" printf 'NEKIRO_E2E_COMPOSE_PROJECT=%s\n' "$compose_project" >>"$output" } diff --git a/tests/backend/invoke_record_test.go b/tests/backend/invoke_record_test.go index e6fa36c..f1aef2f 100644 --- a/tests/backend/invoke_record_test.go +++ b/tests/backend/invoke_record_test.go @@ -59,6 +59,9 @@ type acceptanceEnv struct { publicAgentIDs map[string]string credentialForbidden []string forbidden []string + tlsRoot string + tlsPort string + mtlsPort string } func (env *acceptanceEnv) forbid(values ...string) { @@ -86,6 +89,7 @@ type httpResult struct { func TestInvokeToRecordAcceptance(t *testing.T) { env := loadAcceptanceEnv(t) + startSecureNacosFixture(t, &env) env.credentialForbidden = []string{ "acceptance-owner-token", "acceptance-user-token", "acceptance-other-token", "router-internal-token", "control-plane-internal-token", "runtime-a-router-token", @@ -278,6 +282,9 @@ func loadAcceptanceEnv(t *testing.T) acceptanceEnv { userToken: requiredEnv(t, "NEKIRO_E2E_USER_TOKEN"), otherToken: requiredEnv(t, "NEKIRO_E2E_OTHER_TOKEN"), databaseURL: requiredEnv(t, "NEKIRO_E2E_DATABASE_URL"), + tlsRoot: requiredEnv(t, "NEKIRO_E2E_TLS_ROOT"), + tlsPort: requiredEnv(t, "NEKIRO_E2E_NACOS_TLS_PORT"), + mtlsPort: requiredEnv(t, "NEKIRO_E2E_NACOS_MTLS_PORT"), composeFile: composeFile, composeProject: requiredEnv(t, "NEKIRO_E2E_COMPOSE_PROJECT"), challengeTTL: time.Duration(ttlSeconds) * time.Second, @@ -312,6 +319,7 @@ func assertNacosRegistrations(t *testing.T, client *http.Client, env acceptanceE func startRegisteredRuntimes(t *testing.T, env acceptanceEnv) { t.Helper() + assertSecureRegistrationFailureMatrix(t, env) command := composeCommand( t.Context(), env, "--profile", "runtime-registration", "up", "--detach", "--no-deps", "--force-recreate", diff --git a/tests/backend/nacos_tls_fixture_test.go b/tests/backend/nacos_tls_fixture_test.go new file mode 100644 index 0000000..258c65c --- /dev/null +++ b/tests/backend/nacos_tls_fixture_test.go @@ -0,0 +1,200 @@ +//go:build e2e + +package invokerecord_test + +import ( + "context" + "crypto/ed25519" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/json" + "encoding/pem" + "errors" + "math/big" + "net" + "net/http" + "net/http/httputil" + "net/url" + "os" + "path/filepath" + "strings" + "testing" + "time" +) + +type secureNacosMaterial struct { + server tls.Certificate + caPool *x509.CertPool +} + +func startSecureNacosFixture(t *testing.T, env *acceptanceEnv) { + t.Helper() + material := writeSecureNacosMaterial(t, env.tlsRoot) + target, err := url.Parse(env.nacosURL) + if err != nil { + t.Fatal(err) + } + target.Path = "" + target.RawPath = "" + start := func(port string, requireClient bool) { + listener, err := net.Listen("tcp", "0.0.0.0:"+port) + if err != nil { + t.Fatalf("listen secure Nacos fixture: %v", err) + } + proxy := httputil.NewSingleHostReverseProxy(target) + proxy.ErrorHandler = func(writer http.ResponseWriter, _ *http.Request, _ error) { + http.Error(writer, "Nacos fixture upstream unavailable", http.StatusBadGateway) + } + serverTLS := &tls.Config{MinVersion: tls.VersionTLS12, Certificates: []tls.Certificate{material.server}} + if requireClient { + serverTLS.ClientAuth = tls.RequireAndVerifyClientCert + serverTLS.ClientCAs = material.caPool + } + server := &http.Server{Handler: proxy, TLSConfig: serverTLS} + tlsListener := tls.NewListener(listener, serverTLS) + go func() { _ = server.Serve(tlsListener) }() + t.Cleanup(func() { + _ = server.Close() + }) + } + start(env.tlsPort, false) + start(env.mtlsPort, true) +} + +func assertSecureRegistrationFailureMatrix(t *testing.T, env acceptanceEnv) { + t.Helper() + for name, overrides := range map[string][]string{ + "wrong CA": { + "RUNTIME_B_NACOS_TLS_CA_FILE=/var/run/nekiro-nacos-tls/wrong-ca.pem", + "RUNTIME_B_NACOS_TLS_CLIENT_CERT_FILE=/var/run/nekiro-nacos-tls/client.pem", + "RUNTIME_B_NACOS_TLS_CLIENT_KEY_FILE=/var/run/nekiro-nacos-tls/client-key.pem", + }, + "wrong server name": { + "RUNTIME_B_NACOS_TLS_CA_FILE=/var/run/nekiro-nacos-tls/ca.pem", + "RUNTIME_B_NACOS_TLS_SERVER_NAME=other.internal", + "RUNTIME_B_NACOS_TLS_CLIENT_CERT_FILE=/var/run/nekiro-nacos-tls/client.pem", + "RUNTIME_B_NACOS_TLS_CLIENT_KEY_FILE=/var/run/nekiro-nacos-tls/client-key.pem", + }, + "missing mTLS client": { + "RUNTIME_B_NACOS_TLS_CA_FILE=/var/run/nekiro-nacos-tls/ca.pem", + "RUNTIME_B_NACOS_TLS_SERVER_NAME=nacos.internal", + "RUNTIME_B_NACOS_TLS_CLIENT_CERT_FILE=", + "RUNTIME_B_NACOS_TLS_CLIENT_KEY_FILE=", + }, + } { + t.Run(name, func(t *testing.T) { + service := "runtime-b-negative-" + strings.ReplaceAll(strings.ToLower(name), " ", "-") + args := []string{"--profile", "runtime-registration", "run", "--rm", "--no-deps"} + for _, override := range overrides { + args = append(args, "-e", override) + } + args = append(args, + "-e", "RUNTIME_B_INSTANCE_ID="+service, + "-e", "RUNTIME_B_NACOS_SERVICE_NAME="+service, + "runtime-b-directory", + ) + ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second) + output, err := composeCommand(ctx, env, args...).CombinedOutput() + cancel() + if errors.Is(ctx.Err(), context.DeadlineExceeded) { + t.Fatalf("invalid secure registration remained running instead of failing closed: %s", output) + } + if err == nil { + t.Fatalf("invalid secure registration unexpectedly succeeded: %s", output) + } + outputText := string(output) + for _, forbidden := range []string{"/var/run/nekiro-nacos-tls", "PRIVATE KEY", "BEGIN CERTIFICATE", "wrong-ca.pem"} { + if strings.Contains(outputText, forbidden) { + t.Fatalf("secure registration failure leaked %q: %s", forbidden, outputText) + } + } + assertNoNacosInstance(t, env, service) + }) + } +} + +func assertNoNacosInstance(t *testing.T, env acceptanceEnv, service string) { + t.Helper() + endpoint := env.nacosURL + "/v1/ns/instance/list?serviceName=" + url.QueryEscape(service) + "&groupName=NEKIRO&clusters=DEFAULT&namespaceId=nekiro&healthyOnly=false" + result := doRequest(t, &http.Client{Timeout: 5 * time.Second}, endpoint, http.MethodGet, "", "", nil) + if result.status == http.StatusOK { + var response struct { + Hosts []json.RawMessage `json:"hosts"` + } + if err := json.Unmarshal(result.body, &response); err != nil { + t.Fatalf("decode failed-registration Nacos response: %v body=%s", err, result.body) + } + if len(response.Hosts) != 0 { + t.Fatalf("failed registration left a Nacos instance: %s", result.body) + } + } +} + +func writeSecureNacosMaterial(t *testing.T, directory string) secureNacosMaterial { + t.Helper() + if err := os.MkdirAll(directory, 0o700); err != nil { + t.Fatal(err) + } + caPublic, caPrivate, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + now := time.Now() + caTemplate := &x509.Certificate{SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "NeKiro E2E Nacos CA"}, NotBefore: now.Add(-time.Hour), NotAfter: now.Add(time.Hour), IsCA: true, BasicConstraintsValid: true, KeyUsage: x509.KeyUsageCertSign} + caDER, err := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, caPublic, caPrivate) + if err != nil { + t.Fatal(err) + } + caCertificate, err := x509.ParseCertificate(caDER) + if err != nil { + t.Fatal(err) + } + write := func(name string, content []byte) string { + path := filepath.Join(directory, name) + if err := os.WriteFile(path, content, 0o600); err != nil { + t.Fatal(err) + } + return path + } + write("ca.pem", pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caDER})) + issue := func(name string, serial int64, usage x509.ExtKeyUsage, dnsNames []string) (tls.Certificate, string, string) { + public, private, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + template := &x509.Certificate{SerialNumber: big.NewInt(serial), Subject: pkix.Name{CommonName: name}, DNSNames: dnsNames, NotBefore: now.Add(-time.Hour), NotAfter: now.Add(time.Hour), KeyUsage: x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{usage}} + der, err := x509.CreateCertificate(rand.Reader, template, caCertificate, public, caPrivate) + if err != nil { + t.Fatal(err) + } + certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}) + keyDER, err := x509.MarshalPKCS8PrivateKey(private) + if err != nil { + t.Fatal(err) + } + keyPEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER}) + certFile, keyFile := write(name+".pem", certPEM), write(name+"-key.pem", keyPEM) + certificate, err := tls.X509KeyPair(certPEM, keyPEM) + if err != nil { + t.Fatal(err) + } + return certificate, certFile, keyFile + } + server, _, _ := issue("server", 2, x509.ExtKeyUsageServerAuth, []string{"nacos.internal"}) + _, _, _ = issue("client", 3, x509.ExtKeyUsageClientAuth, nil) + otherPublic, otherPrivate, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + otherTemplate := &x509.Certificate{SerialNumber: big.NewInt(4), Subject: pkix.Name{CommonName: "Wrong CA"}, NotBefore: now.Add(-time.Hour), NotAfter: now.Add(time.Hour), IsCA: true, BasicConstraintsValid: true, KeyUsage: x509.KeyUsageCertSign} + otherDER, err := x509.CreateCertificate(rand.Reader, otherTemplate, otherTemplate, otherPublic, otherPrivate) + if err != nil { + t.Fatal(err) + } + write("wrong-ca.pem", pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: otherDER})) + pool := x509.NewCertPool() + pool.AddCert(caCertificate) + return secureNacosMaterial{server: server, caPool: pool} +} From ddd7daae0e288c52c905e6258c5a77e3e8a2bf65 Mon Sep 17 00:00:00 2001 From: Nene7ko_ <1604009816@qq.com> Date: Mon, 10 Aug 2026 20:26:18 +0800 Subject: [PATCH 2/2] test(stack): make TLS fixture readable to runtimes --- tests/backend/invoke_record_test.go | 1 + tests/backend/nacos_tls_fixture_test.go | 46 +++++++++++++++++++------ 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/tests/backend/invoke_record_test.go b/tests/backend/invoke_record_test.go index f1aef2f..eb12f01 100644 --- a/tests/backend/invoke_record_test.go +++ b/tests/backend/invoke_record_test.go @@ -62,6 +62,7 @@ type acceptanceEnv struct { tlsRoot string tlsPort string mtlsPort string + secureNacos *secureNacosFixture } func (env *acceptanceEnv) forbid(values ...string) { diff --git a/tests/backend/nacos_tls_fixture_test.go b/tests/backend/nacos_tls_fixture_test.go index 258c65c..d8df4d1 100644 --- a/tests/backend/nacos_tls_fixture_test.go +++ b/tests/backend/nacos_tls_fixture_test.go @@ -20,6 +20,7 @@ import ( "os" "path/filepath" "strings" + "sync/atomic" "testing" "time" ) @@ -29,9 +30,16 @@ type secureNacosMaterial struct { caPool *x509.CertPool } +type secureNacosFixture struct { + tlsAttempts atomic.Int64 + mtlsAttempts atomic.Int64 +} + func startSecureNacosFixture(t *testing.T, env *acceptanceEnv) { t.Helper() material := writeSecureNacosMaterial(t, env.tlsRoot) + fixture := &secureNacosFixture{} + env.secureNacos = fixture target, err := url.Parse(env.nacosURL) if err != nil { t.Fatal(err) @@ -47,7 +55,14 @@ func startSecureNacosFixture(t *testing.T, env *acceptanceEnv) { proxy.ErrorHandler = func(writer http.ResponseWriter, _ *http.Request, _ error) { http.Error(writer, "Nacos fixture upstream unavailable", http.StatusBadGateway) } - serverTLS := &tls.Config{MinVersion: tls.VersionTLS12, Certificates: []tls.Certificate{material.server}} + attempts := &fixture.tlsAttempts + if requireClient { + attempts = &fixture.mtlsAttempts + } + serverTLS := &tls.Config{MinVersion: tls.VersionTLS12, Certificates: []tls.Certificate{material.server}, GetConfigForClient: func(*tls.ClientHelloInfo) (*tls.Config, error) { + attempts.Add(1) + return nil, nil + }} if requireClient { serverTLS.ClientAuth = tls.RequireAndVerifyClientCert serverTLS.ClientCAs = material.caPool @@ -65,29 +80,32 @@ func startSecureNacosFixture(t *testing.T, env *acceptanceEnv) { func assertSecureRegistrationFailureMatrix(t *testing.T, env acceptanceEnv) { t.Helper() - for name, overrides := range map[string][]string{ - "wrong CA": { + for name, test := range map[string]struct { + overrides []string + expectTLSAttempt bool + }{ + "wrong CA": {overrides: []string{ "RUNTIME_B_NACOS_TLS_CA_FILE=/var/run/nekiro-nacos-tls/wrong-ca.pem", "RUNTIME_B_NACOS_TLS_CLIENT_CERT_FILE=/var/run/nekiro-nacos-tls/client.pem", "RUNTIME_B_NACOS_TLS_CLIENT_KEY_FILE=/var/run/nekiro-nacos-tls/client-key.pem", - }, - "wrong server name": { + }, expectTLSAttempt: true}, + "wrong server name": {overrides: []string{ "RUNTIME_B_NACOS_TLS_CA_FILE=/var/run/nekiro-nacos-tls/ca.pem", "RUNTIME_B_NACOS_TLS_SERVER_NAME=other.internal", "RUNTIME_B_NACOS_TLS_CLIENT_CERT_FILE=/var/run/nekiro-nacos-tls/client.pem", "RUNTIME_B_NACOS_TLS_CLIENT_KEY_FILE=/var/run/nekiro-nacos-tls/client-key.pem", - }, - "missing mTLS client": { + }, expectTLSAttempt: true}, + "missing mTLS client": {overrides: []string{ "RUNTIME_B_NACOS_TLS_CA_FILE=/var/run/nekiro-nacos-tls/ca.pem", "RUNTIME_B_NACOS_TLS_SERVER_NAME=nacos.internal", "RUNTIME_B_NACOS_TLS_CLIENT_CERT_FILE=", "RUNTIME_B_NACOS_TLS_CLIENT_KEY_FILE=", - }, + }}, } { t.Run(name, func(t *testing.T) { service := "runtime-b-negative-" + strings.ReplaceAll(strings.ToLower(name), " ", "-") args := []string{"--profile", "runtime-registration", "run", "--rm", "--no-deps"} - for _, override := range overrides { + for _, override := range test.overrides { args = append(args, "-e", override) } args = append(args, @@ -96,6 +114,7 @@ func assertSecureRegistrationFailureMatrix(t *testing.T, env acceptanceEnv) { "runtime-b-directory", ) ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second) + beforeAttempts := env.secureNacos.mtlsAttempts.Load() output, err := composeCommand(ctx, env, args...).CombinedOutput() cancel() if errors.Is(ctx.Err(), context.DeadlineExceeded) { @@ -104,6 +123,9 @@ func assertSecureRegistrationFailureMatrix(t *testing.T, env acceptanceEnv) { if err == nil { t.Fatalf("invalid secure registration unexpectedly succeeded: %s", output) } + if test.expectTLSAttempt && env.secureNacos.mtlsAttempts.Load() <= beforeAttempts { + t.Fatalf("invalid secure registration failed before reaching the mTLS boundary: %s", output) + } outputText := string(output) for _, forbidden := range []string{"/var/run/nekiro-nacos-tls", "PRIVATE KEY", "BEGIN CERTIFICATE", "wrong-ca.pem"} { if strings.Contains(outputText, forbidden) { @@ -134,7 +156,7 @@ func assertNoNacosInstance(t *testing.T, env acceptanceEnv, service string) { func writeSecureNacosMaterial(t *testing.T, directory string) secureNacosMaterial { t.Helper() - if err := os.MkdirAll(directory, 0o700); err != nil { + if err := os.MkdirAll(directory, 0o755); err != nil { t.Fatal(err) } caPublic, caPrivate, err := ed25519.GenerateKey(rand.Reader) @@ -153,7 +175,9 @@ func writeSecureNacosMaterial(t *testing.T, directory string) secureNacosMateria } write := func(name string, content []byte) string { path := filepath.Join(directory, name) - if err := os.WriteFile(path, content, 0o600); err != nil { + // These are ephemeral acceptance credentials mounted read-only into + // non-root fixture containers; repository and product storage never see them. + if err := os.WriteFile(path, content, 0o444); err != nil { t.Fatal(err) } return path