Add built-in TLS (self-signed autogen, CA pinning) and optional closed registration
coordinator / test (push) Canceled after 0s
python / test (push) Canceled after 0s
release / binaries (amd64, darwin) (push) Canceled after 0s
release / binaries (amd64, linux) (push) Canceled after 0s
release / binaries (amd64, windows) (push) Canceled after 0s
release / binaries (arm64, darwin) (push) Canceled after 0s
release / binaries (arm64, linux) (push) Canceled after 0s
release / binaries (arm64, windows) (push) Canceled after 0s
release / wheel (push) Canceled after 0s
release / image (push) Canceled after 0s
users / test (push) Canceled after 0s
release / release (push) Canceled after 0s

This commit is contained in:
Emil
2026-08-03 20:17:32 +03:00
parent 049113cec8
commit 63c8ef0b8a
15 changed files with 329 additions and 7 deletions
+1 -1
View File
@@ -75,7 +75,7 @@ func (p *WorkerKeyToken) exchangeLocked() error {
return err
}
request.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: p.timeout}
client := &http.Client{Timeout: p.timeout, Transport: tlsTransport(nil)}
response, err := client.Do(request)
if err != nil {
return fmt.Errorf("worker key exchange request failed")
+2 -2
View File
@@ -42,7 +42,7 @@ func checkHTTP(ctx context.Context, url string, timeout time.Duration) (CheckIte
if err != nil {
return CheckItem{Name: "coordinator", OK: false, Detail: "invalid URL"}, ""
}
resp, err := http.DefaultClient.Do(req)
resp, err := (&http.Client{Timeout: timeout, Transport: tlsTransport(nil)}).Do(req)
if err != nil {
detail := err.Error()
if strings.Contains(detail, "connection refused") {
@@ -148,7 +148,7 @@ func CheckAuth(ctx context.Context, url, token, workerKey, userserviceURL string
item.Detail = "no credential configured — will be checked at registration"
return item
}
client := &http.Client{Timeout: 30 * time.Second}
client := &http.Client{Timeout: 30 * time.Second, Transport: tlsTransport(nil)}
if workerKey != "" && userserviceURL != "" {
payload, _ := json.Marshal(map[string]string{"key": workerKey})
req, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(userserviceURL, "/")+"/worker-tokens/exchange", strings.NewReader(string(payload)))
+3 -1
View File
@@ -60,10 +60,12 @@ func NewClient(baseURL string, tokens TokenProvider, timeout time.Duration) *Cli
timeout: timeout,
apiClient: &http.Client{
Timeout: timeout,
Transport: tlsTransport(nil),
CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse },
},
dlClient: &http.Client{
Timeout: transferTimeout,
Timeout: transferTimeout,
Transport: tlsTransport(nil),
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if len(via) >= 10 {
return fmt.Errorf("too many redirects")
+57
View File
@@ -1,8 +1,10 @@
package agent
import (
"context"
"crypto/sha256"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"net/http"
@@ -237,3 +239,58 @@ func TestNewClientTransferTimeoutExceedsAPITimeout(t *testing.T) {
t.Errorf("transfer timeout = %v, want 4x the api timeout", short.dlClient.Timeout)
}
}
func TestTLSClientHonoursSkipVerify(t *testing.T) {
t.Setenv("SCIMESH_INSECURE_SKIP_VERIFY", "1")
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`{"status":"ok"}`))
}))
defer server.Close()
client := tlsClient(5 * time.Second)
req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, server.URL+"/health", nil)
resp, err := client.Do(req)
if err != nil {
t.Fatalf("TLS server must be reachable with skip-verify: %v", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
t.Errorf("status = %d", resp.StatusCode)
}
}
func TestTLSClientFailsWithoutTrust(t *testing.T) {
t.Setenv("SCIMESH_INSECURE_SKIP_VERIFY", "")
t.Setenv("SCIMESH_CA_CERT", "")
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
defer server.Close()
client := tlsClient(5 * time.Second)
req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, server.URL+"/health", nil)
if resp, err := client.Do(req); err == nil {
_ = resp.Body.Close()
t.Error("untrusted TLS server must fail verification")
}
}
func TestTLSClientTrustsCAPool(t *testing.T) {
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("ok"))
}))
defer server.Close()
ca := server.Certificate()
path := filepath.Join(t.TempDir(), "ca.pem")
if err := os.WriteFile(path, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ca.Raw}), 0o600); err != nil {
t.Fatal(err)
}
t.Setenv("SCIMESH_CA_CERT", path)
t.Setenv("SCIMESH_INSECURE_SKIP_VERIFY", "")
client := tlsClient(5 * time.Second)
req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, server.URL+"/health", nil)
resp, err := client.Do(req)
if err != nil {
t.Fatalf("CA-trusted TLS server must verify: %v", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
t.Errorf("status = %d", resp.StatusCode)
}
}
+69
View File
@@ -0,0 +1,69 @@
package agent
import (
"crypto/tls"
"crypto/x509"
"log/slog"
"net"
"net/http"
"os"
"time"
)
// tlsClient builds an HTTP client whose transport trusts the coordinator's
// TLS certificate:
//
// - SCIMESH_CA_CERT=/path/to/ca.pem adds a root CA (for self-signed or
// private-CA coordinators);
// - SCIMESH_INSECURE_SKIP_VERIFY=1 disables verification entirely — only
// for trusted LANs where a self-signed certificate was auto-generated.
//
// Both settings are deliberately opt-in and noisy: a coordinator without them
// fails to verify, never silently downgrades.
func tlsClient(timeout time.Duration) *http.Client {
return &http.Client{Timeout: timeout, Transport: tlsTransport(nil)}
}
// tlsTransport configures a transport honouring the trust environment.
func tlsTransport(base *http.Transport) *http.Transport {
if base == nil {
base = &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{Timeout: 30 * time.Second, KeepAlive: 30 * time.Second}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
}
}
caPath := os.Getenv("SCIMESH_CA_CERT")
skip := os.Getenv("SCIMESH_INSECURE_SKIP_VERIFY") == "1"
if caPath == "" && !skip {
return base
}
tlsConfig := &tls.Config{MinVersion: tls.VersionTLS12} //nolint:gosec // G402: min TLS 1.2 by default
if caPath != "" {
//nolint:gosec // G304: SCIMESH_CA_CERT is operator-configured
pem, err := os.ReadFile(caPath)
if err != nil {
slog.Warn("could not read SCIMESH_CA_CERT", "path", caPath, "err", err)
return base
}
pool, err := x509.SystemCertPool()
if err != nil {
pool = x509.NewCertPool()
}
if !pool.AppendCertsFromPEM(pem) {
slog.Warn("SCIMESH_CA_CERT contained no usable certificates", "path", caPath)
return base
}
tlsConfig.RootCAs = pool
}
if skip {
// G402 is about production code paths; here the operator explicitly
// opts into an unverified LAN trust root, so the bypass is intended.
tlsConfig.InsecureSkipVerify = true //nolint:gosec // G402: operator opt-in for self-signed LAN certs
slog.Warn("SCIMESH_INSECURE_SKIP_VERIFY=1: TLS certificate verification is disabled")
}
base.TLSClientConfig = tlsConfig
return base
}