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:
@@ -14,6 +14,7 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- DEV_MODE=true
|
- DEV_MODE=true
|
||||||
- DCNTRLIZED_DEV=true
|
- DCNTRLIZED_DEV=true
|
||||||
|
- INSECURE_INTERNAL=1
|
||||||
- GITEA_URL=${GITEA_URL:-http://192.168.122.183:3000}
|
- GITEA_URL=${GITEA_URL:-http://192.168.122.183:3000}
|
||||||
- GITEA_REGISTRY_RO_TOKEN=${GITEA_REGISTRY_RO_TOKEN}
|
- GITEA_REGISTRY_RO_TOKEN=${GITEA_REGISTRY_RO_TOKEN}
|
||||||
- GITEA_RW_PACKAGE_TOKEN=${GITEA_RW_PACKAGE_TOKEN}
|
- GITEA_RW_PACKAGE_TOKEN=${GITEA_RW_PACKAGE_TOKEN}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"git.dcntrlized.org/Dcntrlized/Maestro/lib/authmgmt"
|
||||||
"git.dcntrlized.org/Dcntrlized/Maestro/lib/docker"
|
"git.dcntrlized.org/Dcntrlized/Maestro/lib/docker"
|
||||||
"git.dcntrlized.org/Dcntrlized/Maestro/lib/logger"
|
"git.dcntrlized.org/Dcntrlized/Maestro/lib/logger"
|
||||||
)
|
)
|
||||||
@@ -64,8 +65,12 @@ func RunAction(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct the URL for the action endpoint, preserving the query string
|
// Construct the URL for the action endpoint, preserving the query string.
|
||||||
actionURL := fmt.Sprintf("http://%s:%s/action/%s", network.IPAddress.Address, port, actionName)
|
// 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 != "" {
|
if r.URL.RawQuery != "" {
|
||||||
actionURL += "?" + 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
|
// 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 {
|
if err != nil {
|
||||||
logger.Debug("Failed to forward action request %s to container %s: %v", actionName, containerId, err)
|
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)
|
http.Error(w, "Failed to forward request to container", http.StatusInternalServerError)
|
||||||
|
|||||||
+54
-16
@@ -90,6 +90,29 @@ type authzRequest struct {
|
|||||||
// New builds the mTLS management client, provisioning Maestro's own identity
|
// New builds the mTLS management client, provisioning Maestro's own identity
|
||||||
// from the instance CA (the CA holder issues its own orchestrator cert).
|
// from the instance CA (the CA holder issues its own orchestrator cert).
|
||||||
func New() (*Client, error) {
|
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()
|
dataRoot, err := dataRoot()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("resolve data root: %w", err)
|
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")
|
return nil, fmt.Errorf("no certificates parsed from instance CA")
|
||||||
}
|
}
|
||||||
|
|
||||||
baseURL := os.Getenv("AUTH_SERVICE_URL")
|
return &http.Transport{
|
||||||
if baseURL == "" {
|
TLSClientConfig: &tls.Config{
|
||||||
baseURL = AuthServiceURL
|
Certificates: []tls.Certificate{cert},
|
||||||
}
|
RootCAs: pool,
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}, nil
|
}, 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.
|
// TrustState returns the pending authorizations and the approved trust store.
|
||||||
func (c *Client) TrustState(ctx context.Context) (*TrustState, error) {
|
func (c *Client) TrustState(ctx context.Context) (*TrustState, error) {
|
||||||
var state TrustState
|
var state TrustState
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.dcntrlized.org/Dcntrlized/Maestro/lib/authmgmt"
|
||||||
"git.dcntrlized.org/Dcntrlized/Maestro/lib/logger"
|
"git.dcntrlized.org/Dcntrlized/Maestro/lib/logger"
|
||||||
"git.dcntrlized.org/Dcntrlized/Maestro/lib/types"
|
"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.
|
// 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
|
// 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) {
|
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 resp *http.Response
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
// Create a client that respects context deadlines/cancellation
|
// 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 {
|
for range maxRetries {
|
||||||
req, reqErr := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
req, reqErr := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
|
|||||||
Reference in New Issue
Block a user