diff --git a/src/lib/containers/start.go b/src/lib/containers/start.go index ff050a4..28cc8bb 100644 --- a/src/lib/containers/start.go +++ b/src/lib/containers/start.go @@ -18,6 +18,7 @@ package containers import ( "context" "fmt" + "os" "path/filepath" "strings" @@ -69,6 +70,15 @@ func StartContainer(ctx context.Context, item types.RegistryContainer) error { return err } + // Dev escape hatch (M2 §2): when Maestro itself runs with + // INSECURE_INTERNAL=1, start services the dev way too — propagate the flag + // into the compose services so infra auto-started by Maestro (e.g. a + // resurrected dcntrlized-auth, see infra.go) keeps a plaintext listener + // instead of coming up as mTLS against the plaintext dev fleet. + if os.Getenv("INSECURE_INTERNAL") == "1" { + propagateInsecureInternal(project) + } + // The compose file may reference the image by a short name (e.g. // "testservice") while install pulled it under its full registry reference. // Make sure those short names exist so Up() can find them. @@ -161,6 +171,23 @@ func injectServiceIdentity(ctx context.Context, project *composetypes.Project, s return nil } +// propagateInsecureInternal injects INSECURE_INTERNAL=1 into every non-oneoff +// compose service when Maestro itself runs in dev mode (the same flag the dev +// debug composes set). Mirroring those composes keeps peer edges plaintext, so +// a service auto-installed and started by Maestro (e.g. a resurrected +// dcntrlized-auth) matches the compose-managed plaintext dev fleet instead of +// binding an mTLS listener the satellites cannot reach. +func propagateInsecureInternal(project *composetypes.Project) { + for name, s := range project.Services { + if isOneoff(s) { + continue + } + s.Environment = mergeEnv(s.Environment, map[string]string{"INSECURE_INTERNAL": "1"}) + project.Services[name] = s + } + logger.Info("Dev mode (INSECURE_INTERNAL=1): propagated plaintext escape hatch to %d service(s)", len(project.Services)) +} + // pinAuthFingerprint returns the service's pinned auth fingerprint, fetching // and persisting it when the install record does not have one yet (M2 §5). // Best-effort: when the auth service is unreachable, it returns "" and the diff --git a/src/lib/containers/start_test.go b/src/lib/containers/start_test.go new file mode 100644 index 0000000..4f51314 --- /dev/null +++ b/src/lib/containers/start_test.go @@ -0,0 +1,58 @@ +// Copyright (C) 2026 Dcntrlized +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package containers + +import ( + "testing" + + composetypes "github.com/compose-spec/compose-go/v2/types" +) + +// TestPropagateInsecureInternal verifies the dev escape hatch is propagated to +// every non-oneoff compose service (and only those), mirroring what the dev +// debug composes set so Maestro-started services keep plaintext peer edges. +func TestPropagateInsecureInternal(t *testing.T) { + project := &composetypes.Project{ + Services: composetypes.Services{ + "app": { + Environment: map[string]*string{"PORT": strptr("8080")}, + }, + "worker": { + Environment: nil, + }, + "migrate": { + CustomLabels: map[string]string{"org.dcntrlized.oneoff": "true"}, + }, + }, + } + + propagateInsecureInternal(project) + + for _, name := range []string{"app", "worker"} { + s := project.Services[name] + v, ok := s.Environment["INSECURE_INTERNAL"] + if !ok || v == nil || *v != "1" { + t.Errorf("service %s: expected INSECURE_INTERNAL=1, got %v", name, v) + } + } + + s := project.Services["migrate"] + if _, ok := s.Environment["INSECURE_INTERNAL"]; ok { + t.Error("oneoff migrate service must not receive the plaintext escape hatch") + } +} + +func strptr(s string) *string { return &s }