Skip to content

fix: gracefully handle missing secrets in policy monitor - #7571

Merged
ycombinator merged 2 commits into
elastic:mainfrom
ycombinator:fix/graceful-missing-secrets
Aug 6, 2026
Merged

fix: gracefully handle missing secrets in policy monitor#7571
ycombinator merged 2 commits into
elastic:mainfrom
ycombinator:fix/graceful-missing-secrets

Conversation

@ycombinator

@ycombinator ycombinator commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

What is the problem this PR solves?

Fleet Server crash-loops when any secret referenced in an agent policy's secret_references cannot be found in the Fleet secrets store. A single missing secret — from a deleted or orphaned integration credential — causes the entire policy monitor to abort with:

Error: failed to get secret values: unexpected status 404 from fleet secret read: ...
Fleet Server failed

Fleet Server then restarts, re-reads the same stale policy document from the policy monitor (the checkpoint hasn't advanced), encounters the same 404, and crashes again — indefinitely. Every agent on that fleet-server instance goes offline for the duration.

This is a disproportionate blast radius: one deleted integration credential (unrelated to fleet-server's own connectivity) can take the entire Fleet Server offline.

The root causes that produce missing secrets are tracked in Kibana (elastic/kibana#282911 and elastic/kibana#282912) and in fleet-server (#7533, #7534). This PR is a defense-in-depth fix: even after those root causes are resolved, fleet-server should not treat a single missing package-policy credential as a reason to crash-loop and take all agents offline.

How does this PR solve the problem?

Two changes:

  1. bulk/secret.goExtendedAPI.Read: Returns a typed ErrSecretNotFound sentinel when the Fleet secrets API responds with 404. This lets callers distinguish "this secret document doesn't exist" from network/auth failures (403, 500, etc.), which should still be fatal.

  2. bulk/engine.goReadSecrets: When ReadSecret returns ErrSecretNotFound, logs a warning with the secret ID and skips it (continues the loop) rather than aborting. The returned map contains only the secrets that were successfully fetched.

With this change, a missing secret causes the policy to load without that secret value — the raw $co.elastic.secret{id} placeholder remains unsubstituted in the affected integration's config. That integration's credentials won't resolve (it will fail to authenticate), but fleet-server itself stays up and continues serving all other agents.

Callers that need hard failure for missing secrets (e.g. output API key resolution, where an empty key would silently break agent→ES connectivity) can check whether the expected ID is present in the returned map — as the second commit in #7416 already does.

How to test this PR locally

  1. Start a local stack with fleet-server
  2. Create an integration that uses secrets (e.g. any OAuth-based integration)
  3. Directly delete the secret document from Elasticsearch: DELETE /_fleet/secret/<id>
  4. Restart fleet-server and observe that it:
    • Logs WARN secret not found; policy will load without it secret_id=<id>
    • Continues to start successfully
    • Serves other agents normally
  5. Verify (via agent diagnostics) that the affected integration's credential field contains the raw $co.elastic.secret{id} placeholder rather than the secret value

Design Checklist

  • I have ensured my design is stateless and will work when multiple fleet-server instances are behind a load balancer. (Each instance evaluates ReadSecrets independently per policy load; skipping is per-request with no shared state.)
  • I have or intend to scale test my changes, ensuring it will work reliably with 100K+ agents connected. (The change reduces work on failure — skipping a secret is cheaper than returning an error — so scale impact is neutral or positive.)
  • I have included fail safe mechanisms to limit the load on fleet-server: rate limiting, circuit breakers, caching, load shedding, etc. (No new load introduced; 404s are now fast-path skips.)

Checklist

  • I have added tests that prove my fix is effective or that my feature works
  • I have added an entry in ./changelog/fragments using the changelog tool

Related issues

A 404 from GET /_fleet/secret/{id} was treated as a fatal error,
causing the policy monitor to abort and fleet-server to crash-loop —
taking all agents offline when a single integration credential was
missing from the Fleet secrets store.

Add ErrSecretNotFound sentinel to ExtendedAPI.Read so callers can
distinguish "this secret doesn't exist" from network/auth errors.
Change ReadSecrets to log a warning and skip missing secrets rather
than aborting, so the policy loads without the missing value and
only the affected integration degrades.

Closes elastic#7536

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 6, 2026 21:59
@ycombinator
ycombinator requested a review from a team as a code owner August 6, 2026 21:59
@ycombinator ycombinator added the bug Something isn't working label Aug 6, 2026
@ycombinator ycombinator added the bug Something isn't working label Aug 6, 2026
@mergify

mergify Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

This pull request does not have a backport label. Could you fix it @ycombinator? 🙏
To fixup this pull request, you need to add the backport labels for the needed
branches, such as:

  • backport-./d./d is the label to automatically backport to the 8./d branch. /d is the digit
  • backport-active-all is the label that automatically backports to all active branches.
  • backport-active-8 is the label that automatically backports to all active minor branches for the 8 major.
  • backport-active-9 is the label that automatically backports to all active minor branches for the 9 major.

@ycombinator ycombinator added backport-active-all Automated backport with mergify to all the active branches Team:Elastic-Agent-Control-Plane Label for the Agent Control Plane team labels Aug 6, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR adds a defense-in-depth safeguard to prevent Fleet Server from crash-looping when an agent policy references a secret that no longer exists in the Fleet secrets store. It introduces a typed “not found” error from the secrets read path and updates policy secret resolution to treat missing secrets as non-fatal, keeping Fleet Server available while allowing only the affected integration to degrade.

Changes:

  • Introduce an exported ErrSecretNotFound sentinel returned on GET /_fleet/secret/{id} 404s.
  • Update Bulker.ReadSecrets to warn and continue when a secret is missing instead of aborting policy loading.
  • Add unit tests validating 404 behavior and successful secret decoding, plus a changelog fragment.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
internal/pkg/bulk/secret.go Adds ErrSecretNotFound and returns it on 404 responses from the Fleet secrets API.
internal/pkg/bulk/secret_test.go Adds tests for 404 sentinel behavior, generic server errors, and successful reads.
internal/pkg/bulk/engine.go Skips missing secrets (warn + continue) in ReadSecrets while preserving fatal behavior for other errors.
changelog/fragments/1786053515-graceful-missing-secrets.yaml Documents the behavior change as a bug fix.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@MichelLosier MichelLosier left a comment

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.

LGTM!

Comment thread changelog/fragments/1786053515-graceful-missing-secrets.yaml Outdated
Co-authored-by: Michel Losier <mikelosier@gmail.com>
Copilot AI review requested due to automatic review settings August 6, 2026 22:47

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

Suppressed comments (2)

internal/pkg/bulk/engine.go:341

  • ReadSecrets now tolerates ErrSecretNotFound by skipping the missing secret. There are unit tests for ExtendedAPI.Read returning the sentinel, but there isn't a test that asserts ReadSecrets actually continues and returns a partial result map (and does not return an error) when one ID is missing. Adding a focused unit test would help prevent regressions in this crash-loop fix.
// read secrets one by one as there is no bulk API yet to read them in one request
func (b *Bulker) ReadSecrets(ctx context.Context, secretIds []string) (map[string]string, error) {
	result := make(map[string]string)
	esClient := b.Client()
	for _, id := range secretIds {
		val, err := ReadSecret(ctx, esClient, id)
		if err != nil {
			if errors.Is(err, ErrSecretNotFound) {
				zerolog.Ctx(ctx).Warn().Str("secret_id", id).Msg("secret not found; policy will load without it")
				continue
			}
			return nil, err
		}
		result[id] = val
	}
	return result, nil

internal/pkg/bulk/engine.go:336

  • The warning message here is policy-specific ("policy will load without it"), but Bulker.ReadSecrets is also used for output API key resolution (internal/pkg/policy/policy_output.go:427). In that path, a missing secret is fatal, so this log line can be misleading during incidents. Consider making the message generic (or moving logging to the policy-monitor call site) so it remains accurate for all callers.
			if errors.Is(err, ErrSecretNotFound) {
				zerolog.Ctx(ctx).Warn().Str("secret_id", id).Msg("secret not found; policy will load without it")
				continue

@ycombinator
ycombinator enabled auto-merge (squash) August 6, 2026 23:02
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

TL;DR

The failing :gcloud: Cloud e2e FIPS Test is most likely a configuration/infrastructure failure in cloud provisioning, not a regression in this PR. The provided log shows fleet_url is empty and the test script exits with status 1 in that condition.

Remediation

  • Ensure mage test:cloudE2EUp produces a non-empty Terraform output for fleet_url before mage test:cloudE2ERun.
  • Re-run the job with full pre-cleanup logs retained, since the provided excerpt only includes teardown and final exit status.
Investigation details

Root Cause

.buildkite/scripts/cloud_e2e_test.sh:35-45 exits with code 1 when FLEET_SERVER_URL is empty.

The failing job log excerpt includes:

  • /tmp/gh-aw/buildkite-logs/fleet-server-gcloud-cloud-e2e-fips-test.txt:107- fleet_url = "" -> null
  • /tmp/gh-aw/buildkite-logs/fleet-server-gcloud-cloud-e2e-fips-test.txt:136Error: The command exited with status 1

PR #7571 changes are confined to internal/pkg/bulk/secret.go, internal/pkg/bulk/engine.go, and tests/changelog, which do not directly control cloud Terraform output generation for fleet_url.

Evidence

Verification

  • Not run locally (cloud Buildkite provisioning path is external to this sandbox).

Follow-up

  • I did not find an accessible open flaky-test issue in this repo that explicitly matches this fleet_url-empty cloud E2E failure mode.

What is this? | From workflow: PR Buildkite Detective

Give us feedback! React with 🚀 if perfect, 👍 if helpful, 👎 if not.

@ycombinator
ycombinator merged commit 5ece0ba into elastic:main Aug 6, 2026
12 checks passed
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

@Mergifyio backport 9.5 9.4 8.19

@mergify

mergify Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

backport 9.5 9.4 8.19

✅ Backports have been created

Details

Cherry-pick of 5ece0ba has failed:

On branch mergify/bp/9.5/pr-7571
Your branch is up to date with 'origin/9.5'.

You are currently cherry-picking commit 5ece0ba.
  (fix conflicts and run "git cherry-pick --continue")
  (use "git cherry-pick --skip" to skip this patch)
  (use "git cherry-pick --abort" to cancel the cherry-pick operation)

Changes to be committed:
	new file:   changelog/fragments/1786053515-graceful-missing-secrets.yaml
	modified:   internal/pkg/bulk/engine.go
	new file:   internal/pkg/bulk/secret_test.go

Unmerged paths:
  (use "git add <file>..." to mark resolution)
	both modified:   internal/pkg/bulk/secret.go

To fix up this pull request, you can check it out locally. See documentation: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/checking-out-pull-requests-locally

Cherry-pick of 5ece0ba has failed:

On branch mergify/bp/9.4/pr-7571
Your branch is up to date with 'origin/9.4'.

You are currently cherry-picking commit 5ece0ba.
  (fix conflicts and run "git cherry-pick --continue")
  (use "git cherry-pick --skip" to skip this patch)
  (use "git cherry-pick --abort" to cancel the cherry-pick operation)

Changes to be committed:
	new file:   changelog/fragments/1786053515-graceful-missing-secrets.yaml
	modified:   internal/pkg/bulk/engine.go
	new file:   internal/pkg/bulk/secret_test.go

Unmerged paths:
  (use "git add <file>..." to mark resolution)
	both modified:   internal/pkg/bulk/secret.go

To fix up this pull request, you can check it out locally. See documentation: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/checking-out-pull-requests-locally

Cherry-pick of 5ece0ba has failed:

On branch mergify/bp/8.19/pr-7571
Your branch is up to date with 'origin/8.19'.

You are currently cherry-picking commit 5ece0ba.
  (fix conflicts and run "git cherry-pick --continue")
  (use "git cherry-pick --skip" to skip this patch)
  (use "git cherry-pick --abort" to cancel the cherry-pick operation)

Changes to be committed:
	new file:   changelog/fragments/1786053515-graceful-missing-secrets.yaml
	modified:   internal/pkg/bulk/engine.go
	new file:   internal/pkg/bulk/secret_test.go

Unmerged paths:
  (use "git add <file>..." to mark resolution)
	both modified:   internal/pkg/bulk/secret.go

To fix up this pull request, you can check it out locally. See documentation: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/checking-out-pull-requests-locally

ycombinator added a commit that referenced this pull request Aug 7, 2026
Cherry-pick left unresolved conflict markers in secret.go for the
imports ("errors", "fmt", "io") and the status-code check blocks.
Take both incoming additions as intended by the original fix.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ycombinator added a commit that referenced this pull request Aug 7, 2026
Cherry-pick left unresolved conflict markers in secret.go for the
imports ("errors", "fmt", "io") and the status-code check blocks.
Take both incoming additions as intended by the original fix.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ycombinator added a commit that referenced this pull request Aug 7, 2026
Cherry-pick left unresolved conflict markers in secret.go for the
imports ("errors", "fmt", "io") and the status-code check blocks.
Take both incoming additions as intended by the original fix.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ycombinator added a commit that referenced this pull request Aug 7, 2026
The >= 400 check was introduced on main in #7416 but never backported
to these branches. The cherry-pick dragged it in as a side-effect.
Remove it so non-404 errors fall through as they did before, keeping
the backport minimal and matching the intent of #7571.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ycombinator added a commit that referenced this pull request Aug 7, 2026
The >= 400 check was introduced on main in #7416 but never backported
to these branches. The cherry-pick dragged it in as a side-effect.
Remove it so non-404 errors fall through as they did before, keeping
the backport minimal and matching the intent of #7571.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ycombinator added a commit that referenced this pull request Aug 7, 2026
The >= 400 check was introduced on main in #7416 but never backported
to these branches. The cherry-pick dragged it in as a side-effect.
Remove it so non-404 errors fall through as they did before, keeping
the backport minimal and matching the intent of #7571.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport-active-all Automated backport with mergify to all the active branches bug Something isn't working Team:Elastic-Agent-Control-Plane Label for the Agent Control Plane team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add graceful handling for missing secrets in policy monitor

4 participants