mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-08 01:16:45 +08:00
* fix(daemon): report the checkout's own repo sync error ensureRepoReady read the sync failure from a per-workspace slot that any concurrent background sync overwrote, so a later successful sync of another repo erased the error and checkout reported only "repo is configured but not synced". syncWorkspaceReposContext now returns its error to the caller and the shared slot is removed. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * feat(server): share SSL_CERT_DIR CAs with daemons for git Daemons clone with the system git, which never sees the backend's SSL_CERT_DIR, so a repository on a host with an internal CA could not sync even after the operator made the backend trust that CA. The new gitca package mirrors crypto/x509's SSL_CERT_DIR loading, skips the system directories, and keeps only certificates crypto/x509 trusts. The register and workspace repos responses carry them as ca_certs. The daemon writes its system CA bundle plus those certificates to a bundle file, adds http.sslCAInfo to its git environment, and records it in the cache and isolated checkout config so agents' fetch and push use it too. An empty value removes the bundle; Windows daemons skip it. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * docs: document private CA trust for daemons Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * fix(daemon): keep operator CA trust and existing checkouts working Review of the private CA support found two regressions for repositories that already worked: - Removing the last backend CA deleted the bundle while existing linked worktrees and isolated checkouts still pointed at it, so their HTTPS fetch and push failed. The bundle is now rewritten to the machine's own trust instead, and each repository drops the reference the next time the cache configures it. - The bundle started from the system bundle and was injected through the environment, overriding an operator's general http.sslCAInfo and any repository-local value. It now starts from the file a general http.sslCAInfo names (system bundle otherwise), the environment no longer carries it, clone passes it with -c, and repository config is only set where unset and only unset where it is ours. URL-scoped settings, repository-local values, and GIT_SSL_CAINFO keep precedence. Isolated checkouts record the setting before their first checkout, which can lazily fetch from origin, and clones before the refspec migration fetch. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> --------- Co-authored-by: Iliya Anastasov <[email protected]> Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
158 lines
4.9 KiB
Go
158 lines
4.9 KiB
Go
// Package gitca extracts the private CA certificates an operator added to the
|
|
// backend's trust store through SSL_CERT_DIR, so the server can hand them to
|
|
// daemons. Daemons clone with the system git binary, which never reads the
|
|
// backend's SSL_CERT_DIR; without these certificates a repository on a host
|
|
// with an internal CA cannot sync.
|
|
package gitca
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"crypto/x509"
|
|
"encoding/hex"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// MaxBundleSize caps the PEM sent to daemons. A handful of internal CAs is a
|
|
// few KB; anything near this limit is a misconfigured directory (for example
|
|
// the whole system store mounted under another path).
|
|
const MaxBundleSize = 256 << 10
|
|
|
|
// Load returns the PEM of every certificate in the non-system directories of
|
|
// SSL_CERT_DIR that crypto/x509 also trusts, or nil when there are none.
|
|
// Problems with individual files or certificates are logged and skipped so a
|
|
// bad file never blocks the server from starting.
|
|
func Load(logger *slog.Logger) []byte {
|
|
certs, warnings := collect(os.Getenv(certDirEnv), systemCertDirectories)
|
|
for _, w := range warnings {
|
|
logger.Warn("git CA: skipping certificate", "detail", w)
|
|
}
|
|
if len(certs) == 0 {
|
|
return nil
|
|
}
|
|
|
|
roots, err := x509.SystemCertPool()
|
|
if err != nil {
|
|
logger.Warn("git CA: system cert pool unavailable; not sharing SSL_CERT_DIR certificates with daemons", "error", err)
|
|
return nil
|
|
}
|
|
|
|
var bundle bytes.Buffer
|
|
fingerprints := make([]string, 0, len(certs))
|
|
for _, cert := range certs {
|
|
if err := trustedBy(cert, roots); err != nil {
|
|
logger.Warn("git CA: certificate is not trusted by the backend; skipping", "subject", cert.Subject.String(), "error", err)
|
|
continue
|
|
}
|
|
block := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})
|
|
if bundle.Len()+len(block) > MaxBundleSize {
|
|
logger.Warn("git CA: bundle size limit reached; skipping remaining certificates", "limit_bytes", MaxBundleSize)
|
|
break
|
|
}
|
|
bundle.Write(block)
|
|
fingerprints = append(fingerprints, fingerprint(cert))
|
|
}
|
|
if bundle.Len() == 0 {
|
|
return nil
|
|
}
|
|
logger.Info("git CA: sharing SSL_CERT_DIR certificates with daemons", "count", len(fingerprints), "sha256", fingerprints)
|
|
return bundle.Bytes()
|
|
}
|
|
|
|
// trustedBy reports whether crypto/x509 accepts cert as a trust anchor, so
|
|
// daemons receive exactly what the backend itself trusts even if collect ever
|
|
// drifts from the standard library's loader.
|
|
func trustedBy(cert *x509.Certificate, roots *x509.CertPool) error {
|
|
_, err := cert.Verify(x509.VerifyOptions{
|
|
Roots: roots,
|
|
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
|
|
})
|
|
return err
|
|
}
|
|
|
|
// collect mirrors the SSL_CERT_DIR half of loadSystemRoots in
|
|
// crypto/x509/root_unix.go (Go 1.26): split on ":", read each directory while
|
|
// skipping symlinks that point within it, and parse every file as PEM. Unlike
|
|
// the standard library it skips the system directories, keeps the parsed
|
|
// certificates instead of a CertPool, and drops duplicates.
|
|
func collect(certDir string, systemDirs []string) ([]*x509.Certificate, []string) {
|
|
if certDir == "" {
|
|
return nil, nil
|
|
}
|
|
system := make(map[string]struct{}, len(systemDirs))
|
|
for _, d := range systemDirs {
|
|
system[filepath.Clean(d)] = struct{}{}
|
|
}
|
|
|
|
var (
|
|
certs []*x509.Certificate
|
|
warnings []string
|
|
seen = map[[sha256.Size]byte]struct{}{}
|
|
)
|
|
for _, dir := range strings.Split(certDir, ":") {
|
|
if dir == "" {
|
|
continue
|
|
}
|
|
if _, ok := system[filepath.Clean(dir)]; ok {
|
|
continue
|
|
}
|
|
entries, err := readUniqueDirectoryEntries(dir)
|
|
if err != nil {
|
|
if !os.IsNotExist(err) {
|
|
warnings = append(warnings, fmt.Sprintf("read %s: %v", dir, err))
|
|
}
|
|
continue
|
|
}
|
|
for _, entry := range entries {
|
|
path := filepath.Join(dir, entry.Name())
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
// Subdirectories land here too; crypto/x509 ignores them the same way.
|
|
continue
|
|
}
|
|
for _, cert := range parseCertificates(path, data, &warnings) {
|
|
sum := sha256.Sum256(cert.Raw)
|
|
if _, dup := seen[sum]; dup {
|
|
continue
|
|
}
|
|
seen[sum] = struct{}{}
|
|
certs = append(certs, cert)
|
|
}
|
|
}
|
|
}
|
|
return certs, warnings
|
|
}
|
|
|
|
// parseCertificates matches CertPool.AppendCertsFromPEM: non-CERTIFICATE
|
|
// blocks and certificates that fail to parse are skipped, not fatal.
|
|
func parseCertificates(path string, data []byte, warnings *[]string) []*x509.Certificate {
|
|
var certs []*x509.Certificate
|
|
for len(data) > 0 {
|
|
var block *pem.Block
|
|
block, data = pem.Decode(data)
|
|
if block == nil {
|
|
break
|
|
}
|
|
if block.Type != "CERTIFICATE" || len(block.Headers) != 0 {
|
|
continue
|
|
}
|
|
cert, err := x509.ParseCertificate(block.Bytes)
|
|
if err != nil {
|
|
*warnings = append(*warnings, fmt.Sprintf("parse %s: %v", path, err))
|
|
continue
|
|
}
|
|
certs = append(certs, cert)
|
|
}
|
|
return certs
|
|
}
|
|
|
|
func fingerprint(cert *x509.Certificate) string {
|
|
sum := sha256.Sum256(cert.Raw)
|
|
return hex.EncodeToString(sum[:])
|
|
}
|