feat(dev): DCNTRLIZED_DEV drives dev mode; dev fleet runs real mTLS
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.
This commit is contained in:
+17
-15
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -14,45 +14,3 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
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 }
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
Vendored
+34
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
// 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")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user