Skip to content

fix: extend offline secret caching to token-based auth and fix connection check#272

Open
devin-ai-integration[bot] wants to merge 1 commit into
mainfrom
devin/1781886077-fix-offline-fallback
Open

fix: extend offline secret caching to token-based auth and fix connection check#272
devin-ai-integration[bot] wants to merge 1 commit into
mainfrom
devin/1781886077-fix-offline-fallback

Conversation

@devin-ai-integration

Copy link
Copy Markdown
Contributor

Description 📣

Fixes two issues that prevent the CLI's offline secret caching from working in common scenarios:

1. ValidateInfisicalAPIConnection() only checked for transport errors, not HTTP status codes

When a load balancer or reverse proxy returns HTTP 503 (server down), the Go http.Get succeeds without error — so the CLI treated the server as "connected" and never fell back to cached secrets. Now checks for a 2xx response:

// Before: 503 from a proxy → err == nil → isConnected = true → no fallback
_, err := http.Get(...)
return err == nil

// After: 503 → resp.StatusCode == 503 → isConnected = false → fallback triggers
resp, err := http.Get(...)
return resp.StatusCode >= 200 && resp.StatusCode < 300

2. Token-based auth (service tokens, Universal Auth / machine identity) had zero offline caching

GetAllEnvironmentVariables() only wrote/read secret backups in the infisical login user-auth code path. The else branch for InfisicalToken and UniversalAuthAccessToken had no caching logic at all — any fetch failure returned an error immediately with no fallback.

Added the same backup write-on-success / read-on-failure pattern to the token-based auth branch, using the existing WriteBackupSecrets/ReadBackupSecrets/GetBackupEncryptionKey helpers.

Type ✨

  • Bug fix
  • New feature
  • Improvement
  • Breaking change
  • Documentation

Tests 🛠️

Scenario to verify:

  1. Authenticate with a machine identity (Universal Auth) and run infisical run --env=prod --path=/test --projectId=<id> -- env — secrets should be fetched and injected as before
  2. Verify ~/.infisical/secrets-backup/ contains an encrypted backup file
  3. Make the Infisical server unreachable (stop it, or have a proxy return 503)
  4. Re-run the same command — should print a warning and serve cached secrets instead of failing

Link to Devin session: https://app.devin.ai/sessions/2dfab5dd573b484f84cce72d5f38c757

…tion check

- ValidateInfisicalAPIConnection now checks HTTP status code, not just
  transport errors. A 503 from a load balancer/proxy no longer counts as
  'connected', allowing the cache fallback to trigger correctly.

- Added secret backup/restore for service token and Universal Auth
  (machine identity) code paths. Previously, offline caching only worked
  for infisical-login user auth. Now all auth methods write secrets to
  the encrypted backup on success and fall back to cached secrets on
  fetch failure.

Co-Authored-By: jake <jake@infisical.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@greptile-apps

greptile-apps Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR extends offline secret caching to token-based auth paths and tightens the connectivity check to require a 2xx response. The intent is sound, but there are a few correctness gaps introduced alongside the fix.

  • ValidateInfisicalAPIConnection now correctly rejects non-2xx responses, but forgets to close resp.Body, leaking the HTTP connection on every successful health check call.
  • The new token-based caching fallback fires on any fetch error, whereas the existing user-auth path only falls back when !isConnected — a revoked or unauthorized token will silently serve stale cached secrets with a misleading "connection error" warning.
  • Offline caching is silently a no-op for InfisicalToken (service token) callers unless --projectId is explicitly provided, because params.WorkspaceId is never set in that code path before the caching guard.

Confidence Score: 3/5

The connection-check fix is correct in spirit but ships with a resource leak and a behavioral mismatch in the fallback logic that could silently mask auth failures with stale secrets.

The response body leak in ValidateInfisicalAPIConnection will accumulate on every CLI run. The token-based fallback fires on any error including 401/403, so a user with a revoked token will see old cached secrets and a misleading connection error rather than an actionable auth failure. The service-token offline path is effectively dead code because WorkspaceId is never populated there.

Both changed files need attention: common.go for the unclosed response body, and secrets.go for the overly broad fallback condition and the silent no-op for service token caching.

Security Review

  • SSRF via INFISICAL_URL (packages/util/common.go:44): config.INFISICAL_URL is passed directly to http.Get without scheme or host validation. A user-controlled or misconfigured value could be used to probe internal network endpoints. This vector predates the PR but is now load-bearing for the offline-fallback decision, raising its practical impact.

Important Files Changed

Filename Overview
packages/util/common.go Adds HTTP status code check to ValidateInfisicalAPIConnection, but leaks the response body — resp.Body is never closed after a successful GET.
packages/util/secrets.go Adds write-on-success / read-on-failure caching to the token-based auth branch, but the fallback fires on all errors (not just connection failures), and the service-token path never populates WorkspaceId so caching is silently skipped for it.

Comments Outside Diff (1)

  1. packages/util/secrets.go, line 362-364 (link)

    P1 Caching silently skipped for service tokens

    GetPlainTextSecretsViaServiceToken is called for InfisicalToken without ever setting params.WorkspaceId. The caching block below is guarded by params.WorkspaceId != "", so for service token auth both write-on-success and read-on-failure are silently no-ops. The offline fallback feature advertised by this PR has no effect for InfisicalToken users unless WorkspaceId is separately provided via --projectId.

Reviews (1): Last reviewed commit: "fix: extend offline secret caching to to..." | Re-trigger Greptile

Comment thread packages/util/common.go
Comment on lines +44 to +48
resp, err := http.Get(fmt.Sprintf("%v/status", config.INFISICAL_URL))
if err != nil {
return false
}
return resp.StatusCode >= 200 && resp.StatusCode < 300

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 The response body is never closed after http.Get succeeds, leaking the underlying TCP connection. Per Go's net/http docs, callers must always close resp.Body, even if they don't read from it — otherwise the HTTP client cannot reuse the connection and the goroutine/socket is leaked. Since ValidateInfisicalAPIConnection is called on every CLI invocation that touches secrets, repeated calls will accumulate leaks.

Suggested change
resp, err := http.Get(fmt.Sprintf("%v/status", config.INFISICAL_URL))
if err != nil {
return false
}
return resp.StatusCode >= 200 && resp.StatusCode < 300
resp, err := http.Get(fmt.Sprintf("%v/status", config.INFISICAL_URL))
if err != nil {
return false
}
defer resp.Body.Close()
return resp.StatusCode >= 200 && resp.StatusCode < 300

Comment thread packages/util/secrets.go
Comment on lines +383 to +393
} else if errorToReturn != nil && params.WorkspaceId != "" {
backupEncryptionKey, _ := GetBackupEncryptionKey()
if backupEncryptionKey != nil {
backedUpSecrets, err := ReadBackupSecrets(params.WorkspaceId, params.Environment, params.SecretsPath, backupEncryptionKey)
if len(backedUpSecrets) > 0 {
PrintWarning("Unable to fetch the latest secret(s) due to connection error, serving secrets from last successful fetch. For more info, run with --debug")
secretsToReturn = backedUpSecrets
errorToReturn = err
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Fallback triggered on all errors, not just connection failures

The user-auth path (line 349) only reads from cache when !isConnected, so auth errors (401/403) propagate correctly. The new token-based path falls back on any errorToReturn != nil, meaning a revoked token or permission error will silently serve stale cached secrets instead of surfacing the error to the caller. A user whose token is rotated would see old secrets with only a vague "connection error" warning, with no indication that their token is actually invalid.

Comment thread packages/util/common.go
func ValidateInfisicalAPIConnection() (ok bool) {
_, err := http.Get(fmt.Sprintf("%v/status", config.INFISICAL_URL))
return err == nil
resp, err := http.Get(fmt.Sprintf("%v/status", config.INFISICAL_URL))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 security SSRF via user-controlled INFISICAL_URL

config.INFISICAL_URL is user-supplied and passed directly to http.Get. An attacker who can influence this value (e.g., via a misconfigured environment variable) could use it to probe internal network endpoints — making the CLI an SSRF oracle. Consider validating that the URL is a known-safe external host, or at minimum ensuring the scheme is https and the host is not a private/loopback address. This vector existed before this PR but the change makes it more prominent as the health-check path is now load-bearing for the offline fallback decision.

Context Used: Flag SSRF risks (source)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants