Skip to content

Commit aeb4d65

Browse files
committed
Use short-lived Bearer tokens to test credential cache invalidation
Use 3-second token TTL so cached Bearer tokens expire during the test. After expiry, the docker.Authorizer's cached handler tries to re-fetch a token with old credentials baked into its TokenOptions. The token server rejects the old password, causing doBearerAuth to fail without calling AddResponses — so the handler is never deleted and the Authorizer is stuck. Without InvalidateRegistryHosts: layer "N" unavailable (permanent failure). With InvalidateRegistryHosts: cache cleared, fresh Authorizer created, new handler fetches token with new password, container runs.
1 parent fd0bb97 commit aeb4d65

2 files changed

Lines changed: 27 additions & 13 deletions

File tree

integration/run_test.go

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1028,13 +1028,19 @@ services:
10281028

10291029
// Start the token server inside the testing container.
10301030
// The binary is built on the host and mounted via the auth volume.
1031+
// Use short-lived tokens (3s). After expiry, the cached Bearer handler
1032+
// in docker.Authorizer tries to re-fetch a token from the token server
1033+
// using the baked-in credentials. If the password has changed, fetchToken
1034+
// fails inside doBearerAuth — AddResponses is never called, so the handler
1035+
// is never deleted. The Authorizer is stuck with stale credentials.
10311036
sh.Gox("/auth/tokenserver",
10321037
"--addr", ":5001",
10331038
"--cert", "/auth/domain.crt",
10341039
"--key", "/auth/domain.key",
10351040
"--signing-key", "/auth/signing.key",
10361041
"--password-file", "/auth/password.txt",
1037-
"--issuer", "test-issuer")
1042+
"--issuer", "test-issuer",
1043+
"--token-ttl", "3")
10381044
// Wait for token server to start.
10391045
sh.Retry(30, "curl", "-sk", "https://localhost:5001/token")
10401046

@@ -1056,9 +1062,12 @@ services:
10561062
sh.X(append(imagePullCmd, "--soci-index-digest", indexDigest, image)...)
10571063

10581064
// First run: warms up registryHostMap cache. The docker.Authorizer creates
1059-
// a Bearer handler that caches the token server credentials internally.
1065+
// a Bearer handler that caches the token and token server credentials.
10601066
sh.X(append(runSociCmd, "--name", "warmup", "--rm", image, "echo", "ok")...)
10611067

1068+
// Wait for the short-lived token to expire (3s TTL + buffer).
1069+
sh.X("sleep", "5")
1070+
10621071
// Rotate the token server password.
10631072
newPass := "rotatedpass"
10641073
if err := os.WriteFile(filepath.Join(authDir, "password.txt"), []byte(newPass), 0666); err != nil {
@@ -1068,15 +1077,18 @@ services:
10681077
// Login with the new password (updates docker config keychain).
10691078
sh.X("nerdctl", "login", "-u", regConfig.user, "-p", newPass, regConfig.host)
10701079

1071-
// Second run: check() fires. The cached AuthClient's Bearer handler tries
1072-
// to fetch a token from the token server using the OLD password (baked into
1073-
// the handler at creation time). The token server rejects it. This error
1074-
// occurs inside doBearerAuth → fetchToken, so AddResponses is never called
1075-
// and the handler is never deleted.
1080+
// Second run: check() fires (check_always=true). The cached AuthClient's
1081+
// docker.Authorizer has a Bearer handler whose token has expired.
1082+
// doBearerAuth calls fetchToken with the OLD password baked into the
1083+
// handler's TokenOptions. The token server rejects it. This error occurs
1084+
// inside doBearerAuth — AddResponses is never called, so the handler is
1085+
// never deleted. The Authorizer is stuck with stale credentials.
10761086
//
10771087
// Without InvalidateRegistryHosts: stuck with stale handler → permanent failure.
1078-
// With InvalidateRegistryHosts: cache cleared → fresh Authorizer → new handler
1079-
// with new password from docker config → token server accepts → success.
1088+
// With InvalidateRegistryHosts: cache cleared → fresh Authorizer → empty
1089+
// handlers map → first request goes unauthenticated → registry returns 401
1090+
// Bearer challenge → AddResponses creates new handler with fresh password
1091+
// from docker config → fetchToken succeeds → container runs.
10801092
_, err = sh.OLog(append(runSociCmd, "--name", "after-rotation", "--rm", image, "echo", "ok")...)
10811093
if err != nil {
10821094
t.Fatalf("expected container run to succeed after credential rotation, got: %v", err)

integration/tokenserver/main.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ func main() {
2929
passwordFile := flag.String("password-file", "", "File containing accepted password")
3030
addr := flag.String("addr", ":5001", "Listen address")
3131
issuer := flag.String("issuer", "test-issuer", "Token issuer")
32+
tokenTTL := flag.Int("token-ttl", 3600, "Token TTL in seconds")
3233
flag.Parse()
3334

3435
signingKeyPEM, err := os.ReadFile(*signingKeyFile)
@@ -64,7 +65,8 @@ func main() {
6465
service := r.URL.Query().Get("service")
6566
scope := r.URL.Query().Get("scope")
6667

67-
token, err := makeToken(signingKey, *issuer, service, user, scope)
68+
ttl := time.Duration(*tokenTTL) * time.Second
69+
token, err := makeToken(signingKey, *issuer, service, user, scope, ttl)
6870
if err != nil {
6971
http.Error(w, err.Error(), http.StatusInternalServerError)
7072
return
@@ -73,7 +75,7 @@ func main() {
7375
w.Header().Set("Content-Type", "application/json")
7476
json.NewEncoder(w).Encode(map[string]interface{}{
7577
"token": token,
76-
"expires_in": 3600,
78+
"expires_in": *tokenTTL,
7779
"issued_at": time.Now().UTC().Format(time.RFC3339),
7880
})
7981
})
@@ -84,7 +86,7 @@ func main() {
8486
}
8587
}
8688

87-
func makeToken(key *rsa.PrivateKey, issuer, service, subject, scope string) (string, error) {
89+
func makeToken(key *rsa.PrivateKey, issuer, service, subject, scope string, ttl time.Duration) (string, error) {
8890
now := time.Now().UTC()
8991

9092
var access []map[string]interface{}
@@ -108,7 +110,7 @@ func makeToken(key *rsa.PrivateKey, issuer, service, subject, scope string) (str
108110
"iss": issuer,
109111
"sub": subject,
110112
"aud": service,
111-
"exp": now.Add(time.Hour).Unix(),
113+
"exp": now.Add(ttl).Unix(),
112114
"nbf": now.Add(-time.Second).Unix(),
113115
"iat": now.Unix(),
114116
"jti": fmt.Sprintf("%d", now.UnixNano()),

0 commit comments

Comments
 (0)