Skip to content
Open
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
4dedc49
feat(agent-vault): host matcher, session cache and resolve client
saifsmailbox98 Sep 2, 2026
8e8f313
feat(agent-vault): the proxy itself, and infisical av proxy
saifsmailbox98 Sep 2, 2026
48f0628
fix(agent-vault): stop zeroing credentials in place; resolve before DNS
saifsmailbox98 Sep 2, 2026
2ffb2c7
feat(agent-vault): infisical av run
saifsmailbox98 Sep 2, 2026
f0b1720
fix(agent-vault): let a connection beat the bypass list
saifsmailbox98 Sep 3, 2026
8a5a604
test(agent-vault): pin the bypass precedence
saifsmailbox98 Sep 3, 2026
14293eb
refactor(agent-vault): make the bypass list an exception to deny, not…
saifsmailbox98 Sep 3, 2026
ade511e
fix(agent-vault): keep a stream alive while it is still producing
saifsmailbox98 Sep 4, 2026
c6aa146
refactor(agent-vault): stop pruning names from the agent's environment
saifsmailbox98 Sep 4, 2026
79a6228
fix(agent-vault): stop av run hanging on the macOS keychain prompt
saifsmailbox98 Sep 4, 2026
3b58b2a
feat(agent-vault): let the proxy reach a private network
saifsmailbox98 Sep 4, 2026
244913b
fix(agent-vault): normalise the request host once, and use it everywhere
saifsmailbox98 Sep 4, 2026
0d999fb
refactor(agent-vault): stop reporting the proxy build on heartbeat
saifsmailbox98 Sep 4, 2026
f35293f
docs(agent-vault): drop the status code from the re-enrollment comment
saifsmailbox98 Sep 4, 2026
10d806f
fix(agent-vault): let --port 0 mean any free port, and bound the range
saifsmailbox98 Sep 4, 2026
90ca535
fix(agent-vault): refuse --ttl and --keep-session alongside --token
saifsmailbox98 Sep 4, 2026
7c8dc89
fix(agent-vault): brokered means a credential went out
saifsmailbox98 Sep 4, 2026
a51fe6c
fix(agent-vault): log a brokered request where an operator will see it
saifsmailbox98 Sep 4, 2026
2dcb6a0
refactor(agent-vault): drop the proxy's whoami endpoint
saifsmailbox98 Sep 4, 2026
069c278
fix(agent-vault): say so when a session cannot be revoked at exit
saifsmailbox98 Sep 5, 2026
f0a8e8b
fix(agent-vault): name the real cause when a session is dropped
saifsmailbox98 Sep 5, 2026
4c21a7d
chore(agent-vault): name handleRefreshFailure for what it does
saifsmailbox98 Sep 5, 2026
8f9811f
fix(agent-vault): refuse a stored CA whose key does not match its cer…
saifsmailbox98 Sep 5, 2026
5325419
fix(agent-vault): trust only the certificate the pin was checked against
saifsmailbox98 Sep 5, 2026
514eb7f
fix(agent-vault): bound control-plane calls and age the cache on read
saifsmailbox98 Sep 5, 2026
2a057cd
refactor(agent-vault): fetch the proxy CA with net/http, not a bare r…
saifsmailbox98 Sep 5, 2026
c28910b
test(agent-vault): sync the host-pattern fixture with the backend
saifsmailbox98 Sep 5, 2026
fe67adb
fix(av): rename av run --token to --session-token
saifsmailbox98 Sep 7, 2026
c88007a
fix(av): exit the proxy when Infisical rejects its token
saifsmailbox98 Sep 7, 2026
4634d3c
feat(av): one access bundle per session
saifsmailbox98 Sep 7, 2026
ae1e833
chore(agent-vault): cut the comments that only say what the code says
saifsmailbox98 Sep 7, 2026
63113ec
test(agent-vault): keep the host grammar cases in each repo's own tests
saifsmailbox98 Sep 7, 2026
a72eef2
feat(av): mint a session by naming the bundle, not its id
saifsmailbox98 Sep 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 193 additions & 0 deletions packages/agentvault/ca.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
package agentvault

import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"math/big"
"net"
"strings"
"sync"
"time"
)

const (
rootCaTTL = 5 * 365 * 24 * time.Hour

leafTTL = 24 * time.Hour
leafReuseMargin = 1 * time.Hour
maxLeafCacheEntries = 8192
)

// caManager owns the proxy's own root CA and mints leaves for intercepted hosts.
type caManager struct {
mu sync.Mutex
rootKey *ecdsa.PrivateKey
rootCert *x509.Certificate

leafMu sync.Mutex
leafCache map[string]*leafEntry
}

type leafEntry struct {
cert tls.Certificate
expiration time.Time
}

// P-256 because rustls-based agents reject some other curves outright.
func generateRootCa() (*ecdsa.PrivateKey, *x509.Certificate, error) {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, nil, fmt.Errorf("failed to generate the certificate authority key: %w", err)
}

serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
if err != nil {
return nil, nil, err
}

template := &x509.Certificate{
SerialNumber: serial,
Subject: pkix.Name{CommonName: "Infisical Agent Vault Proxy"},
NotBefore: time.Now().Add(-1 * time.Minute),
NotAfter: time.Now().Add(rootCaTTL),
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageDigitalSignature,
BasicConstraintsValid: true,
IsCA: true,
MaxPathLenZero: true,
}

der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
if err != nil {
return nil, nil, err
}

cert, err := x509.ParseCertificate(der)
if err != nil {
return nil, nil, err
}
return key, cert, nil
}

func caPEM(cert *x509.Certificate) []byte {
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})
}

func newCaManager(key *ecdsa.PrivateKey, cert *x509.Certificate) *caManager {
return &caManager{rootKey: key, rootCert: cert, leafCache: make(map[string]*leafEntry)}
}

func (c *caManager) RootPEM() []byte {
c.mu.Lock()
defer c.mu.Unlock()
if c.rootCert == nil {
return nil
}
return caPEM(c.rootCert)
}

func (c *caManager) Fingerprint() string {
c.mu.Lock()
defer c.mu.Unlock()
if c.rootCert == nil {
return ""
}
return FingerprintOf(c.rootCert.Raw)
}

func FingerprintOf(der []byte) string {
sum := sha256.Sum256(der)
parts := make([]string, 0, len(sum))
for _, b := range sum {
parts = append(parts, fmt.Sprintf("%02X", b))
}
return "SHA256:" + strings.Join(parts, ":")
}

func (c *caManager) mintLeaf(hostname string) (tls.Certificate, error) {
c.leafMu.Lock()
if entry, ok := c.leafCache[hostname]; ok && time.Until(entry.expiration) > leafReuseMargin {
c.leafMu.Unlock()
return entry.cert, nil
}
c.leafMu.Unlock()

c.mu.Lock()
rootKey, rootCert := c.rootKey, c.rootCert
c.mu.Unlock()
if rootKey == nil || rootCert == nil {
return tls.Certificate{}, fmt.Errorf("the proxy has no certificate authority loaded")
}

key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return tls.Certificate{}, err
}

serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
if err != nil {
return tls.Certificate{}, err
}

notAfter := time.Now().Add(leafTTL)
// A leaf must never outlive its issuer, or clients reject the chain as the root nears expiry.
if notAfter.After(rootCert.NotAfter) {
notAfter = rootCert.NotAfter
}

template := &x509.Certificate{
SerialNumber: serial,
Subject: pkix.Name{CommonName: hostname},
NotBefore: time.Now().Add(-1 * time.Minute),
NotAfter: notAfter,
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
}
if ip := net.ParseIP(hostname); ip != nil {
template.IPAddresses = []net.IP{ip}
} else {
template.DNSNames = []string{hostname}
}

der, err := x509.CreateCertificate(rand.Reader, template, rootCert, &key.PublicKey, rootKey)
if err != nil {
return tls.Certificate{}, err
}

cert := tls.Certificate{Certificate: [][]byte{der, rootCert.Raw}, PrivateKey: key}

c.leafMu.Lock()
c.evictLeavesIfFullLocked(hostname)
c.leafCache[hostname] = &leafEntry{cert: cert, expiration: notAfter}
c.leafMu.Unlock()

return cert, nil
}

func (c *caManager) evictLeavesIfFullLocked(incoming string) {
if len(c.leafCache) < maxLeafCacheEntries {
return
}
if _, replacing := c.leafCache[incoming]; replacing {
return
}

now := time.Now()
for host, entry := range c.leafCache {
if now.After(entry.expiration) {
delete(c.leafCache, host)
}
}
for host := range c.leafCache {
if len(c.leafCache) < maxLeafCacheEntries {
break
}
delete(c.leafCache, host)
}
}
Loading
Loading