Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion packages/agentvault/ca.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,9 @@ func (c *caManager) mintLeaf(hostname string) (tls.Certificate, error) {
return tls.Certificate{}, err
}

notAfter := time.Now().Add(leafTTL)
// Truncate drops the monotonic reading, which pauses while the machine sleeps and kept serving leaves
// past their NotAfter, and matches the whole seconds NotAfter is encoded in.
notAfter := time.Now().Add(leafTTL).Truncate(time.Second)
// 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
Expand Down
11 changes: 7 additions & 4 deletions packages/agentvault/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ type resolvedService struct {
substitutions []substitution
}

// lastSeen and fetchedAt are wall-clock (Round(0)) so time asleep counts toward the grace and idle windows.
Comment thread
saifsmailbox98 marked this conversation as resolved.
// Trade-off: a wall-clock step moves them too (forward re-resolves early, backward stretches grace while
// Infisical is unreachable), which is rarer and milder than sleep.
type sessionEntry struct {
sessionID string
expiresAt *time.Time
Expand Down Expand Up @@ -189,7 +192,7 @@ func (c *sessionCache) lookup(sessionToken string) ([]*resolvedService, *session
delete(c.entries, key)
delete(c.tokens, key)
} else {
entry.lastSeen = time.Now()
entry.lastSeen = time.Now().Round(0)
svcs, grant := entry.services, entry.sessionLog
c.mu.Unlock()
return svcs, grant, nil
Expand Down Expand Up @@ -225,8 +228,8 @@ func (c *sessionCache) lookup(sessionToken string) ([]*resolvedService, *session
expiresAt: result.ExpiresAt,
services: result.Services,
sessionLog: result.SessionLog,
lastSeen: time.Now(),
fetchedAt: time.Now(),
lastSeen: time.Now().Round(0),
fetchedAt: time.Now().Round(0),
Comment thread
saifsmailbox98 marked this conversation as resolved.
}
c.tokens[key] = sessionToken
return cacheLookup{services: result.Services, sessionLog: result.SessionLog}, nil
Expand Down Expand Up @@ -331,7 +334,7 @@ func (c *sessionCache) refreshOne(key, token string) {
entry.expiresAt = result.ExpiresAt
entry.services = result.Services
entry.sessionLog = result.SessionLog
entry.fetchedAt = time.Now()
entry.fetchedAt = time.Now().Round(0)
}
}

Expand Down
26 changes: 26 additions & 0 deletions packages/agentvault/cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -373,3 +373,29 @@ func TestARejectedProxyTokenIsNotReResolvedEveryRequest(t *testing.T) {
t.Fatalf("20 requests against a revoked proxy made %d resolves, want 1", got)
}
}

// == rather than Equal: Equal ignores the monotonic reading, which pauses while the machine sleeps.
func TestCacheTimesIgnoreTheSleepingClock(t *testing.T) {
Comment thread
saifsmailbox98 marked this conversation as resolved.
resolver := &stubResolver{result: &resolveResult{SessionID: "s1", Services: []*resolvedService{serviceWithSecret("v")}}}
cache := newTestCache(resolver)
key := sessionKey("agv_token")

if _, err := cache.get("agv_token"); err != nil {
t.Fatalf("get: %v", err)
}
if e := cache.entries[key]; e.fetchedAt != e.fetchedAt.Round(0) || e.lastSeen != e.lastSeen.Round(0) {
t.Fatal("a fresh entry carries a monotonic reading, so time asleep would not count toward its grace or idle window")
}

if _, err := cache.get("agv_token"); err != nil {
t.Fatalf("get: %v", err)
}
if e := cache.entries[key]; e.lastSeen != e.lastSeen.Round(0) {
t.Fatal("a cache hit stamps lastSeen with a monotonic reading")
}

cache.refresh()
if e := cache.entries[key]; e.fetchedAt != e.fetchedAt.Round(0) {
t.Fatal("a refresh stamps fetchedAt with a monotonic reading")
}
}
26 changes: 26 additions & 0 deletions packages/agentvault/proxy_hostname_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package agentvault

import (
"bytes"
"crypto/x509"
"testing"
)

Expand Down Expand Up @@ -61,3 +62,28 @@ func TestOneHostMintsOneLeafWhateverTheCase(t *testing.T) {
}
}
}

// == rather than Equal: Equal ignores the monotonic reading, which pauses while the machine sleeps.
func TestALeafExpiryIgnoresTheSleepingClock(t *testing.T) {
key, cert, err := generateRootCa()
if err != nil {
t.Fatalf("generateRootCa: %v", err)
}
ca := newCaManager(key, cert)

leaf, err := ca.mintLeaf("api.example.com")
if err != nil {
t.Fatalf("mintLeaf: %v", err)
}
exp := ca.leafCache["api.example.com"].expiration
if exp != exp.Round(0) {
t.Fatal("the cached leaf expiry carries a monotonic reading, so time asleep would not count toward it")
}
parsed, err := x509.ParseCertificate(leaf.Certificate[0])
if err != nil {
t.Fatalf("ParseCertificate: %v", err)
}
if !exp.Equal(parsed.NotAfter) {
t.Fatalf("the cache expires the leaf at %v but the certificate says %v", exp, parsed.NotAfter)
}
}
7 changes: 5 additions & 2 deletions packages/agentvault/session_log.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,9 @@ func (r *sessionLogRecorder) record(g *sessionLogGrant, rec sessionLogRecord) {
// Without the monotonic reading, Before and After compare wall time, which is what Ts shows.
rec.at = now.Round(0)
rec.Ts = rec.at.UTC().Format(time.RFC3339Nano)
spool.lastRecordAt = now
// rec.at, not now: idle close must count time asleep, while lastFlushAt stays monotonic so a
// backward clock step cannot stall the flush interval.
spool.lastRecordAt = rec.at

if !r.admitLocked(g) {
// Switching off says so when it happens; the proxy-wide cap is only reported here.
Expand Down Expand Up @@ -545,7 +547,8 @@ func (r *sessionLogRecorder) sendShipment(ctx context.Context, next shipment, pa
}
res.posted = true
res.postedURL = created.UploadURL
res.postedExpires = r.now().Add(time.Duration(created.ExpiresInSeconds) * time.Second)
// Round(0): the link expires by the wall clock, which keeps running while the machine sleeps.
res.postedExpires = r.now().Add(time.Duration(created.ExpiresInSeconds) * time.Second).Round(0)
Comment thread
saifsmailbox98 marked this conversation as resolved.
uploadURL = created.UploadURL
}

Expand Down
18 changes: 18 additions & 0 deletions packages/agentvault/session_log_ship_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package agentvault

import (
"context"
"errors"
"net/http"
"net/http/httptest"
"strconv"
Expand Down Expand Up @@ -242,3 +243,20 @@ func TestAChunkAlreadyStoredCountsAsUploaded(t *testing.T) {
t.Fatalf("a chunk that is already stored was treated as a failed upload: %v", err)
}
}

// The real clock, not newTestLog's: its fake times never carry a monotonic reading, so the check would always pass.
// == rather than Equal: Equal ignores the monotonic reading, which pauses while the machine sleeps.
func TestAnUploadLinkExpiryIgnoresTheSleepingClock(t *testing.T) {
// A failing upload keeps the chunk queued, so the link it was given can be read back.
log := newSessionLogRecorder("proxy-1", &fakeShipper{putDefault: errors.New("bucket down")})
log.record(testGrant("s1"), aRecord("api.github.com"))
log.flush(context.Background(), flushFinal)

pending := log.spools["s1"].pending
if len(pending) != 1 {
t.Fatalf("%d chunks queued, want the one whose upload failed", len(pending))
}
if exp := pending[0].urlExpires; exp.IsZero() || exp != exp.Round(0) {
t.Fatal("the upload link expiry carries a monotonic reading, so time asleep would not count toward it")
}
}
11 changes: 11 additions & 0 deletions packages/agentvault/session_log_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1616,3 +1616,14 @@ func TestOneStuckRequestAtShutdownDoesNotHoldBackTheOtherSessions(t *testing.T)
})
}
}

// The real clock, not newTestLog's: its fake times never carry a monotonic reading, so the check would always pass.
// == rather than Equal: Equal ignores the monotonic reading, which pauses while the machine sleeps.
func TestIdleCloseIgnoresTheSleepingClock(t *testing.T) {
log := newSessionLogRecorder("proxy-1", &fakeShipper{})
log.record(testGrant("s1"), aRecord("api.github.com"))

if at := log.spools["s1"].lastRecordAt; at != at.Round(0) {
t.Fatal("lastRecordAt carries a monotonic reading, so time asleep would not count toward idle close")
}
}
Loading