diff --git a/docker-compose.debug.yaml b/docker-compose.debug.yaml index c61c098..d5e23e0 100644 --- a/docker-compose.debug.yaml +++ b/docker-compose.debug.yaml @@ -14,6 +14,7 @@ services: environment: - DEV_MODE=true - DCNTRLIZED_DEV=true + - INSECURE_INTERNAL=1 - GITEA_URL=${GITEA_URL:-http://192.168.122.183:3000} - GITEA_REGISTRY_RO_TOKEN=${GITEA_REGISTRY_RO_TOKEN} - GITEA_RW_PACKAGE_TOKEN=${GITEA_RW_PACKAGE_TOKEN} diff --git a/src/handlers/actions/actions.go b/src/handlers/actions/actions.go index ea54ae9..b96ec46 100644 --- a/src/handlers/actions/actions.go +++ b/src/handlers/actions/actions.go @@ -23,6 +23,7 @@ import ( "net/http" "strings" + "git.dcntrlized.org/Dcntrlized/Maestro/lib/authmgmt" "git.dcntrlized.org/Dcntrlized/Maestro/lib/docker" "git.dcntrlized.org/Dcntrlized/Maestro/lib/logger" ) @@ -64,8 +65,12 @@ func RunAction(w http.ResponseWriter, r *http.Request) { return } - // Construct the URL for the action endpoint, preserving the query string - actionURL := fmt.Sprintf("http://%s:%s/action/%s", network.IPAddress.Address, port, actionName) + // 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. + actionURL := fmt.Sprintf("%s://%s:%s/action/%s", authmgmt.Scheme(), network.IPAddress.Address, port, actionName) if r.URL.RawQuery != "" { actionURL += "?" + r.URL.RawQuery } @@ -93,8 +98,16 @@ func RunAction(w http.ResponseWriter, r *http.Request) { } } + // Maestro's own identity (mTLS) as the peer; plaintext in dev mode. + transport, err := authmgmt.PeerTransport() + if err != nil { + logger.Debug("Failed to build peer transport for action %s on container %s: %v", actionName, containerId, err) + http.Error(w, "Failed to build peer transport", http.StatusInternalServerError) + return + } + // Forward the request to the container's action endpoint - resp, err := http.DefaultClient.Do(req) + resp, err := (&http.Client{Transport: transport}).Do(req) if err != nil { logger.Debug("Failed to forward action request %s to container %s: %v", actionName, containerId, err) http.Error(w, "Failed to forward request to container", http.StatusInternalServerError) diff --git a/src/lib/authmgmt/client.go b/src/lib/authmgmt/client.go index 7e3a5c0..71d27dc 100644 --- a/src/lib/authmgmt/client.go +++ b/src/lib/authmgmt/client.go @@ -90,6 +90,29 @@ type authzRequest struct { // New builds the mTLS management client, provisioning Maestro's own identity // from the instance CA (the CA holder issues its own orchestrator cert). func New() (*Client, error) { + transport, err := maestroTransport() + if err != nil { + return nil, err + } + + baseURL := os.Getenv("AUTH_SERVICE_URL") + if baseURL == "" { + baseURL = AuthServiceURL + } + + return &Client{ + baseURL: baseURL, + http: &http.Client{ + Timeout: 10 * time.Second, + Transport: transport, + }, + }, nil +} + +// maestroTransport builds the mutual-TLS transport Maestro uses as a peer: its +// own identity certificate (CN=maestro) signed by the instance CA, trusting the +// CA as root. This is the client side of every Maestro peer edge. +func maestroTransport() (*http.Transport, error) { dataRoot, err := dataRoot() if err != nil { return nil, fmt.Errorf("resolve data root: %w", err) @@ -112,26 +135,41 @@ func New() (*Client, error) { return nil, fmt.Errorf("no certificates parsed from instance CA") } - baseURL := os.Getenv("AUTH_SERVICE_URL") - if baseURL == "" { - baseURL = AuthServiceURL - } - - return &Client{ - baseURL: baseURL, - http: &http.Client{ - Timeout: 10 * time.Second, - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{ - Certificates: []tls.Certificate{cert}, - RootCAs: pool, - MinVersion: tls.VersionTLS12, - }, - }, + return &http.Transport{ + TLSClientConfig: &tls.Config{ + Certificates: []tls.Certificate{cert}, + RootCAs: pool, + MinVersion: tls.VersionTLS12, }, }, 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). +func Scheme() string { + if InsecureInternal() { + 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. +func PeerTransport() (*http.Transport, error) { + if InsecureInternal() { + return &http.Transport{}, nil + } + return maestroTransport() +} + // TrustState returns the pending authorizations and the approved trust store. func (c *Client) TrustState(ctx context.Context) (*TrustState, error) { var state TrustState diff --git a/src/lib/docker/client.go b/src/lib/docker/client.go index 6835e88..a95695b 100644 --- a/src/lib/docker/client.go +++ b/src/lib/docker/client.go @@ -25,6 +25,7 @@ import ( "strings" "time" + "git.dcntrlized.org/Dcntrlized/Maestro/lib/authmgmt" "git.dcntrlized.org/Dcntrlized/Maestro/lib/logger" "git.dcntrlized.org/Dcntrlized/Maestro/lib/types" @@ -92,12 +93,18 @@ func ListContainers(dc *DockerClient) ([]string, error) { // fetchManifestWithRetry polls the container's manifest endpoint until it succeeds or max retries are reached. // Could probably be simplified to (ctx, container) with maxRetries and retryDelay hardcoded, low priority TODO func FetchManifestWithRetry(ctx context.Context, host, port string, maxRetries int, retryDelay time.Duration) (*types.Manifest, error) { - url := fmt.Sprintf("http://%s:%s/_manifest", host, port) + // The service peer edge is mutual TLS (M2 §2), same as RunAction: Maestro's + // identity cert, https scheme, with the plaintext dev escape hatch. + url := fmt.Sprintf("%s://%s:%s/_manifest", authmgmt.Scheme(), host, port) var resp *http.Response var err error // Create a client that respects context deadlines/cancellation - client := &http.Client{Timeout: 2 * time.Second} + transport, terr := authmgmt.PeerTransport() + if terr != nil { + return nil, fmt.Errorf("build peer transport: %w", terr) + } + client := &http.Client{Timeout: 2 * time.Second, Transport: transport} for range maxRetries { req, reqErr := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)