M2 §2: run actions and manifest fetch over mTLS with Maestro's identity

Extract maestroTransport() (Maestro's identity cert + instance CA) and
expose PeerTransport()/Scheme()/InsecureInternal() so every service peer
edge uses the same client. RunAction now targets
https://{ip}:{port}/action/{name} presenting Maestro's cert (services
require a CA-issued client cert, so only Maestro can drive actions — T4)
and FetchManifestWithRetry fetches /_manifest the same way.
INSECURE_INTERNAL=1 keeps the dev fleet plaintext (debug compose updated
to set it).
This commit is contained in:
2026-08-16 01:12:01 +01:00
parent 77bd21aea2
commit 92f4787146
4 changed files with 80 additions and 21 deletions
+16 -3
View File
@@ -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)
+54 -16
View File
@@ -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
+9 -2
View File
@@ -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)