From addaabc0a8b11aaf84e7ea305cba98693dc50560 Mon Sep 17 00:00:00 2001
From: Admin <1+admin@noreply.git.dcntrlized.org>
Date: Sun, 16 Aug 2026 15:51:49 +0100
Subject: [PATCH] feat(dev): DCNTRLIZED_DEV drives dev mode; dev fleet runs
real mTLS
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The single canonical dev flag is DCNTRLIZED_DEV=true, injected by every
docker-compose.debug/dev.yaml. Dev mode now selects endpoints and behaviors
without downgrading control-plane security:
- lib/env: IsDev()/GiteaURL(); registry BaseURL keys on DCNTRLIZED_DEV and
DCNTRLIZED_GITEA_URL (renamed from GITEA_URL).
- authmgmt: Scheme()/PeerTransport() (satellite edges) key on DCNTRLIZED_DEV
— dev satellites keep a plaintext RN-facing listener, production mTLS. The
auth-management edge (New()) stays mTLS in both, preferring
DCNTRLIZED_AUTH_URL in dev.
- Removed INSECURE_INTERNAL + propagateInsecureInternal from Maestro: the
orchestrator no longer runs plaintext, and no longer injects the escape
hatch into services it starts.
- lib/devboot: at boot (dev only) Maestro provisions the identities its
compose-managed peers bind-mount at /dcntrlized/pki (before infra boots)
and approves the known fleet in the auth trust store. Unknown peers still
record a pending authorization and are refused (mechanisms active).
- Composes: prod docker-compose.yaml is clean (no DCNTRLIZED_DEV/GITEA_URL);
the debug compose carries the dev flag + DCNTRLIZED_GITEA_URL only.
---
docker-compose.debug.yaml | 4 +-
docker-compose.yaml | 2 -
src/frontend/PRODUCT.md | 2 +-
src/frontend/vite.config.ts | 2 +-
src/handlers/actions/actions.go | 5 +-
src/lib/authmgmt/client.go | 32 +++++-----
src/lib/containers/start.go | 27 --------
src/lib/containers/start_test.go | 42 -------------
src/lib/devboot/devboot.go | 102 +++++++++++++++++++++++++++++++
src/lib/env/env.go | 34 +++++++++++
src/lib/registry/registry.go | 7 ++-
src/main.go | 15 +++++
12 files changed, 178 insertions(+), 96 deletions(-)
create mode 100644 src/lib/devboot/devboot.go
create mode 100644 src/lib/env/env.go
diff --git a/docker-compose.debug.yaml b/docker-compose.debug.yaml
index fd8d73a..ed8bd56 100644
--- a/docker-compose.debug.yaml
+++ b/docker-compose.debug.yaml
@@ -14,11 +14,9 @@ services:
context: .
dockerfile: Dockerfile.debug
environment:
- - DEV_MODE=true
- DCNTRLIZED_DEV=true
- - INSECURE_INTERNAL=1
- XDG_DATA_HOME=/data
- - GITEA_URL=${GITEA_URL:-http://192.168.122.183:3000}
+ - DCNTRLIZED_GITEA_URL=${DCNTRLIZED_GITEA_URL:-http://192.168.122.183:3000}
- GITEA_REGISTRY_RO_TOKEN=${GITEA_REGISTRY_RO_TOKEN}
- GITEA_RW_PACKAGE_TOKEN=${GITEA_RW_PACKAGE_TOKEN}
- GITEA_ORG=${GITEA_ORG:-Dcntrlized}
diff --git a/docker-compose.yaml b/docker-compose.yaml
index d8504d4..75cf657 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -19,8 +19,6 @@ services:
- GITEA_RW_PACKAGE_TOKEN=${GITEA_RW_PACKAGE_TOKEN}
- GITEA_ORG=${GITEA_ORG:-Dcntrlized}
- GITEA_USER=${GITEA_USER:-admin}
- - GITEA_URL=${GITEA_URL:-http://192.168.122.183:3000}
- - DCNTRLIZED_DEV=${DCNTRLIZED_DEV:-true}
- GIT_TOKEN=${GIT_TOKEN}
- REGISTRY_USER=${REGISTRY_USER:-admin}
diff --git a/src/frontend/PRODUCT.md b/src/frontend/PRODUCT.md
index 10111e6..61225a8 100644
--- a/src/frontend/PRODUCT.md
+++ b/src/frontend/PRODUCT.md
@@ -26,7 +26,7 @@ The mechanism is a Git-backed, decentralized service registry: services are publ
- Real-time statuses come from Docker (running, exited, stopped, created, restarting) plus UI states (installed, uninstalled).
- Service metadata (title, vendor, author, description, source, licenses, revision) comes from OCI annotations published with each image.
- Upstream registry defaults to git.dcntrlized.org (Gitea), org `Dcntrlized`;
- in dev (`DCNTRLIZED_DEV=true`) it targets `GITEA_URL` (the dev VM
+ in dev (`DCNTRLIZED_DEV=true`) it targets `DCNTRLIZED_GITEA_URL` (the dev VM
`192.168.122.183:3000`).
- Maestro itself runs as a container (image `maestro`); its own container is hidden from the list.
- Update detection compares the installed version against the registry latest via semver; versions are normalized to `X.Y.Z`.
diff --git a/src/frontend/vite.config.ts b/src/frontend/vite.config.ts
index f5bf3c1..ffc0a76 100644
--- a/src/frontend/vite.config.ts
+++ b/src/frontend/vite.config.ts
@@ -21,7 +21,7 @@ export default defineConfig(({ command }): UserConfig => {
? {
'/api': 'http://127.0.0.1:8080',
'/gitea-api': {
- target: process.env.GITEA_URL || 'https://git.dcntrlized.org', // Gitea base URL (override with GITEA_URL in dev)
+ target: process.env.DCNTRLIZED_GITEA_URL || 'https://git.dcntrlized.org', // Gitea base URL (override with DCNTRLIZED_GITEA_URL in dev)
changeOrigin: true,
rewrite: (path) => path.replace(/^\/gitea-api/, ''),
},
diff --git a/src/handlers/actions/actions.go b/src/handlers/actions/actions.go
index b96ec46..d7095d1 100644
--- a/src/handlers/actions/actions.go
+++ b/src/handlers/actions/actions.go
@@ -68,8 +68,9 @@ func RunAction(w http.ResponseWriter, r *http.Request) {
// Construct the URL for the action endpoint, preserving the query string.
// The service peer edge is mutual TLS (M2 §2): Maestro presents its own
// identity cert and the service's listener requires a CA-issued client
- // cert, so only Maestro can drive actions (T4). Under INSECURE_INTERNAL=1
- // (dev escape hatch) it stays plaintext.
+ // cert, so only Maestro can drive actions (T4). In dev mode
+ // (DCNTRLIZED_DEV=true) the satellites keep a plaintext listener for the
+ // RN clients, so the action edge stays plaintext there too.
actionURL := fmt.Sprintf("%s://%s:%s/action/%s", authmgmt.Scheme(), network.IPAddress.Address, port, actionName)
if r.URL.RawQuery != "" {
actionURL += "?" + r.URL.RawQuery
diff --git a/src/lib/authmgmt/client.go b/src/lib/authmgmt/client.go
index 71d27dc..fd49e81 100644
--- a/src/lib/authmgmt/client.go
+++ b/src/lib/authmgmt/client.go
@@ -32,6 +32,7 @@ import (
"path/filepath"
"time"
+ "git.dcntrlized.org/Dcntrlized/Maestro/lib/env"
"git.dcntrlized.org/Dcntrlized/Maestro/lib/logger"
"git.dcntrlized.org/Dcntrlized/Maestro/lib/pki"
)
@@ -95,7 +96,13 @@ func New() (*Client, error) {
return nil, err
}
- baseURL := os.Getenv("AUTH_SERVICE_URL")
+ baseURL := ""
+ if env.IsDev() {
+ baseURL = os.Getenv("DCNTRLIZED_AUTH_URL")
+ }
+ if baseURL == "" {
+ baseURL = os.Getenv("AUTH_SERVICE_URL")
+ }
if baseURL == "" {
baseURL = AuthServiceURL
}
@@ -144,27 +151,22 @@ func maestroTransport() (*http.Transport, error) {
}, nil
}
-// InsecureInternal reports whether the dev escape hatch is enabled
-// (INSECURE_INTERNAL=1): peer edges run plaintext, mirroring the services'
-// listeners in the dev compose files.
-func InsecureInternal() bool {
- return os.Getenv("INSECURE_INTERNAL") == "1"
-}
-
-// Scheme returns "http" under the dev escape hatch and "https" otherwise, for
-// building service peer URLs (RunAction, manifest fetch).
+// Scheme returns "http" in dev mode and "https" otherwise, for building
+// satellite peer URLs (RunAction, manifest fetch). Dev satellites keep a
+// plaintext listener so the RN clients (which cannot do mTLS) can reach them;
+// production satellites bind mTLS listeners.
func Scheme() string {
- if InsecureInternal() {
+ if env.IsDev() {
return "http"
}
return "https"
}
-// PeerTransport returns the transport Maestro uses for the service peer edges
-// (M2 §2). Normally mutual TLS with Maestro's identity; a plaintext transport
-// under INSECURE_INTERNAL=1 so the dev fleet keeps working mid-migration.
+// PeerTransport returns the transport Maestro uses for the satellite peer
+// edges (M2 §2): plaintext in dev mode (matching the RN-facing listeners),
+// mutual TLS with Maestro's identity in production.
func PeerTransport() (*http.Transport, error) {
- if InsecureInternal() {
+ if env.IsDev() {
return &http.Transport{}, nil
}
return maestroTransport()
diff --git a/src/lib/containers/start.go b/src/lib/containers/start.go
index 28cc8bb..ff050a4 100644
--- a/src/lib/containers/start.go
+++ b/src/lib/containers/start.go
@@ -18,7 +18,6 @@ package containers
import (
"context"
"fmt"
- "os"
"path/filepath"
"strings"
@@ -70,15 +69,6 @@ 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.
@@ -171,23 +161,6 @@ 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
index 4f51314..87830f1 100644
--- a/src/lib/containers/start_test.go
+++ b/src/lib/containers/start_test.go
@@ -14,45 +14,3 @@
// 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 }
diff --git a/src/lib/devboot/devboot.go b/src/lib/devboot/devboot.go
new file mode 100644
index 0000000..80d5869
--- /dev/null
+++ b/src/lib/devboot/devboot.go
@@ -0,0 +1,102 @@
+// 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 devboot provisions the dev fleet the DCNTRLIZED_DEV way: Maestro
+// issues the identities its docker-compose.dev/debug.yaml peers bind-mount at
+// /dcntrlized/pki, and approves those satellites in the auth service trust
+// store. Dev mode runs the real security mechanisms (mTLS peer edges,
+// authorized-keys enforcement) — this only removes the human-in-the-loop for
+// the *known* dev fleet; any unknown peer still records a pending
+// authorization and is refused.
+package devboot
+
+import (
+ "context"
+
+ "git.dcntrlized.org/Dcntrlized/Maestro/lib/authmgmt"
+ "git.dcntrlized.org/Dcntrlized/Maestro/lib/containers"
+ "git.dcntrlized.org/Dcntrlized/Maestro/lib/logger"
+ "git.dcntrlized.org/Dcntrlized/Maestro/lib/pki"
+)
+
+// Infra are the shared services whose identities the dev composes mount. They
+// are peers/attesters themselves, not approval candidates (auth is the
+// trust-store owner, Maestro uses the internal RequireMaestro endpoints).
+var Infra = []string{"dcntrlized-auth"}
+
+// Fleet lists the dev-mode satellite identities Maestro issues and approves.
+// Their CNs must match the service names used in the dev composes' pki mounts.
+var Fleet = []string{"keepnote", "calendar", "testservice"}
+
+// EnsureIdentities provisions the instance CA identities the dev composes
+// bind-mount at /dcntrlized/pki, so compose-managed services come up with a
+// valid mTLS identity before they ever dial the auth service. Run before the
+// shared infrastructure is ensured, because dcntrlized-auth's mTLS listener
+// refuses to start without its identity.
+func EnsureIdentities() {
+ dataRoot, err := containers.DataRoot()
+ if err != nil {
+ logger.Warn("devboot: resolve data root: %v", err)
+ return
+ }
+ ca, err := pki.EnsureCA(dataRoot)
+ if err != nil {
+ logger.Warn("devboot: ensure instance CA: %v", err)
+ return
+ }
+ for _, name := range append(append([]string{}, Infra...), Fleet...) {
+ if _, err := pki.EnsureServiceIdentity(ca, name); err != nil {
+ logger.Warn("devboot: ensure identity for %s: %v", name, err)
+ }
+ }
+ logger.Info("devboot: ensured dev fleet identities (%d infra + %d satellites)", len(Infra), len(Fleet))
+}
+
+// ApproveFleet approves the dev satellites in the auth service trust store —
+// the owner decision a fresh first-contact would pend, made once for the known
+// fleet. Best-effort: if the auth service is not up yet, the first-contact
+// no-op path records the pendings and the next boot approves them.
+func ApproveFleet(ctx context.Context) {
+ if len(Fleet) == 0 {
+ return
+ }
+ client, err := authmgmt.New()
+ if err != nil {
+ logger.Warn("devboot: build auth client: %v", err)
+ return
+ }
+ dataRoot, err := containers.DataRoot()
+ if err != nil {
+ logger.Warn("devboot: resolve data root: %v", err)
+ return
+ }
+ ca, err := pki.EnsureCA(dataRoot)
+ if err != nil {
+ logger.Warn("devboot: ensure instance CA: %v", err)
+ return
+ }
+ for _, name := range Fleet {
+ identity, err := pki.EnsureServiceIdentity(ca, name)
+ if err != nil {
+ logger.Warn("devboot: load identity for %s: %v", name, err)
+ continue
+ }
+ if err := client.Approve(ctx, name, identity.Meta.Fingerprint); err != nil {
+ logger.Warn("devboot: approve %s: %v", name, err)
+ continue
+ }
+ logger.Info("devboot: approved dev peer %s (%s)", name, identity.Meta.Fingerprint)
+ }
+}
diff --git a/src/lib/env/env.go b/src/lib/env/env.go
new file mode 100644
index 0000000..3b69242
--- /dev/null
+++ b/src/lib/env/env.go
@@ -0,0 +1,34 @@
+// 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 env centralizes environment-derived configuration. DCNTRLIZED_DEV is
+// the single canonical dev-mode flag, injected by every docker-compose.dev.yaml
+// and docker-compose.debug.yaml. It selects dev-only endpoints and behaviors
+// (registry URL, plaintext satellite peer edges) — it never downgrades the
+// transport security of the control-plane edges, which run mTLS in dev too.
+package env
+
+import "os"
+
+// IsDev reports whether Maestro runs in dev mode (DCNTRLIZED_DEV=true).
+func IsDev() bool {
+ return os.Getenv("DCNTRLIZED_DEV") == "true"
+}
+
+// GiteaURL returns the dev Gitea base URL (DCNTRLIZED_GITEA_URL). Empty in
+// production, where the hardcoded https://git.dcntrlized.org is used.
+func GiteaURL() string {
+ return os.Getenv("DCNTRLIZED_GITEA_URL")
+}
diff --git a/src/lib/registry/registry.go b/src/lib/registry/registry.go
index 0b8576c..ccde62e 100644
--- a/src/lib/registry/registry.go
+++ b/src/lib/registry/registry.go
@@ -31,6 +31,7 @@ import (
"strings"
"time"
+ "git.dcntrlized.org/Dcntrlized/Maestro/lib/env"
"git.dcntrlized.org/Dcntrlized/Maestro/lib/logger"
"git.dcntrlized.org/Dcntrlized/Maestro/lib/types"
@@ -42,10 +43,10 @@ import (
// BaseURL returns the Gitea instance base URL (scheme://host[:port]).
// Production defaults to git.dcntrlized.org; dev mode (DCNTRLIZED_DEV=true)
-// overrides it with GITEA_URL when set.
+// overrides it with DCNTRLIZED_GITEA_URL when set.
func BaseURL() string {
- if os.Getenv("DCNTRLIZED_DEV") == "true" {
- if u := strings.TrimRight(os.Getenv("GITEA_URL"), "/"); u != "" {
+ if env.IsDev() {
+ if u := strings.TrimRight(env.GiteaURL(), "/"); u != "" {
return u
}
}
diff --git a/src/main.go b/src/main.go
index bb8f27c..d8d6a88 100644
--- a/src/main.go
+++ b/src/main.go
@@ -28,7 +28,9 @@ import (
"git.dcntrlized.org/Dcntrlized/Maestro/handlers/peers"
handlers "git.dcntrlized.org/Dcntrlized/Maestro/handlers/registry"
libcontainers "git.dcntrlized.org/Dcntrlized/Maestro/lib/containers"
+ "git.dcntrlized.org/Dcntrlized/Maestro/lib/devboot"
"git.dcntrlized.org/Dcntrlized/Maestro/lib/docker"
+ "git.dcntrlized.org/Dcntrlized/Maestro/lib/env"
"git.dcntrlized.org/Dcntrlized/Maestro/lib/pki"
)
@@ -64,6 +66,13 @@ func main() {
if _, err := pki.EnsureCA(dataRoot); err != nil {
log.Fatalf("Failed to provision instance CA: %v", err)
}
+ // Dev mode: provision the identities the docker-compose.dev/debug.yaml
+ // peers bind-mount at /dcntrlized/pki, BEFORE the shared infrastructure
+ // boots — dcntrlized-auth's mTLS listener refuses to start without its
+ // identity. Must run before EnsureDatabaseRunning/EnsureAuthRunning.
+ if env.IsDev() {
+ devboot.EnsureIdentities()
+ }
if err := cache.LoadInstalledContainers(); err != nil {
log.Fatalf("Failed to load installed containers: %v", err)
}
@@ -77,6 +86,12 @@ func main() {
if err := libcontainers.EnsureAuthRunning(ctx); err != nil {
log.Printf("Warning: failed to ensure shared auth service: %v", err)
}
+ // Dev mode: approve the known fleet in the auth trust store once the
+ // auth service is up, so the compose-managed satellites' mTLS proxies
+ // are authorized (the decision a first contact would pend).
+ if env.IsDev() {
+ devboot.ApproveFleet(ctx)
+ }
cache.ReconcileContainers(ctx, dc)
go cache.WatchContainers(ctx, dc)
}