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
+10 -2
View File
@@ -223,8 +223,16 @@ func runWithConfig(cfg infra.Config) error {
// deps.ready backs /health: readiness means the database answers, not just
// that the process is alive.
api := httptransport.NewServer(useCases, log, cfg.RequestTimeout, cfg.HeartbeatInterval, cfg.MaxUploadBytes, cfg.JWTSecret, cfg.UserserviceURL, m, deps.ready, cfg.PublicCoordinatorURL, cfg.PublicUserserviceURL, cfg.DocsDir)
err = infra.RunServer(ctx, log, cfg.Addr, api.Handler(cfg.Token, cfg.UIToken))
api := httptransport.NewServerWithOptions(
useCases, log, cfg.RequestTimeout, cfg.HeartbeatInterval, cfg.MaxUploadBytes,
cfg.JWTSecret, cfg.UserserviceURL, m, deps.ready,
httptransport.ServerOptions{DisableRegistration: cfg.DisableRegistration},
cfg.PublicCoordinatorURL, cfg.PublicUserserviceURL, cfg.DocsDir)
var tlsOpts []infra.TLSConfig
if cfg.TLSCertFile != "" && cfg.TLSKeyFile != "" {
tlsOpts = []infra.TLSConfig{{CertFile: cfg.TLSCertFile, KeyFile: cfg.TLSKeyFile}}
}
err = infra.RunServer(ctx, log, cfg.Addr, api.Handler(cfg.Token, cfg.UIToken), tlsOpts...)
// Shutdown order matters, and defers alone cannot express it (they run
// LIFO, so the deferred stop() would fire *after* the wait below).
+102
View File
@@ -6,10 +6,16 @@ import (
"github.com/emil28092005/SciMesh/coordinator/internal/agent"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"crypto/x509/pkix"
"encoding/hex"
"encoding/pem"
"flag"
"fmt"
"log/slog"
"math/big"
"net"
"os"
"os/exec"
@@ -43,6 +49,10 @@ func runServe(args []string) error {
email = flags.String("admin-email", "admin@scimesh.local", "admin account email")
password = flags.String("admin-password", "", "admin password (generated on first run when empty)")
publicURL = flags.String("public-url", "", "browser/worker-facing coordinator URL (default: http://<addr>)")
tlsCert = flags.String("tls-cert", "", "TLS certificate file (enables HTTPS together with --tls-key)")
tlsKey = flags.String("tls-key", "", "TLS private key file")
tlsGen = flags.Bool("tls-autogen", false, "generate a self-signed certificate in the data dir and serve HTTPS")
noReg = flags.Bool("disable-registration", false, "forbid new UI accounts")
)
if err := flags.Parse(args); err != nil {
return err
@@ -107,10 +117,33 @@ func runServe(args []string) error {
defer stopAgents(agents)
// 6. The coordinator server itself.
// TLS: explicit cert/key win; --tls-autogen creates a self-signed pair in
// the data dir on first use (fingerprint printed for pinning).
tlsCertFile, tlsKeyFile := *tlsCert, *tlsKey
if tlsCertFile == "" && tlsKeyFile == "" && *tlsGen {
tlsCertFile = filepath.Join(*dataDir, "tls.crt")
tlsKeyFile = filepath.Join(*dataDir, "tls.key")
if _, err := os.Stat(tlsCertFile); err != nil {
fingerprint, err := generateSelfSigned(tlsCertFile, tlsKeyFile, *dataDir, *addr)
if err != nil {
return fmt.Errorf("generate TLS certificate: %w", err)
}
log.Info("generated a self-signed TLS certificate", "cert", tlsCertFile, "fingerprint", fingerprint)
fmt.Printf("TLS: self-signed certificate generated (SHA-256 fingerprint %s).\n", fingerprint)
fmt.Printf("Trust it on workers with SCIMESH_CA_CERT=%s (or SCIMESH_INSECURE_SKIP_VERIFY=1).\n", tlsCertFile)
}
}
if (tlsCertFile == "") != (tlsKeyFile == "") {
return fmt.Errorf("--tls-cert and --tls-key must be provided together")
}
cfg := infra.Config{
Addr: *addr,
DatabaseEngine: "sqlite",
DBPath: filepath.Join(*dataDir, "scimesh.db"),
TLSCertFile: tlsCertFile,
TLSKeyFile: tlsKeyFile,
DisableRegistration: *noReg || os.Getenv("SCIMESH_DISABLE_REGISTRATION") == "1",
Token: workerToken,
JWTSecret: jwtSecret,
UserserviceURL: "http://" + usersAddr,
@@ -369,3 +402,72 @@ func serveURLs(addr, publicURL string) (agentURL, resolvedPublic string) {
return agentURL, "http://" + addr
}
}
// generateSelfSigned writes a self-signed certificate for the listen host and
// the machine's LAN addresses, so HTTPS works without a CA on a trusted
// network. The returned value is the certificate's SHA-256 fingerprint.
func generateSelfSigned(certPath, keyPath, dataDir, addr string) (string, error) {
host, _, err := net.SplitHostPort(addr)
if err != nil {
host = addr
}
host = strings.Trim(host, "[]")
ips := []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::1")}
if parsed := net.ParseIP(host); parsed != nil && !parsed.IsUnspecified() {
ips = append(ips, parsed)
} else if host == "" || parsed != nil {
// Wildcard listen addresses: add every local interface address.
if addrs, err := net.InterfaceAddrs(); err == nil {
for _, a := range addrs {
if ipnet, ok := a.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
ips = append(ips, ipnet.IP)
}
}
}
}
names := []string{"localhost", host}
if host != "" && host != "localhost" {
names = append(names, host)
}
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return "", err
}
serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
if err != nil {
return "", err
}
template := x509.Certificate{
SerialNumber: serial,
Subject: pkix.Name{CommonName: "SciMesh coordinator"},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().AddDate(1, 0, 0),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
DNSNames: names,
IPAddresses: ips,
}
der, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
if err != nil {
return "", err
}
if err := os.MkdirAll(dataDir, 0o750); err != nil {
return "", err
}
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
// The certificate is shared with workers via SCIMESH_CA_CERT, so it must
// stay readable; the key stays private.
if err := os.WriteFile(certPath, certPEM, 0o644); err != nil { //nolint:gosec // G306: cert is public by design
return "", err
}
if err := os.WriteFile(keyPath, keyPEM, 0o600); err != nil {
return "", err
}
sum := sha256.Sum256(der)
var parts []string
for _, b := range sum[:] {
parts = append(parts, fmt.Sprintf("%02x", b))
}
return strings.Join(parts, ":"), nil
}