Skip to content

🤖 CoderProvisioner: a rotation at the target deletes another provisioner's same-named key #414

Closed

Description

@ThomasK33

Problem

Two CoderProvisioner objects that resolve to the same key name in the same Coder organization at the same Coder endpoint delete each other's provisioner key. The key name defaults to metadata.name, and spec.key.name can be set explicitly, so this can happen across namespaces, or with two objects that set the same spec.key.name.

When provisioner P reconciles a target where Coder already holds a key with P's key name, EnsureProvisionerKey returns that key without plaintext. P then deletes the key by name and creates its own. If the key belongs to provisioner Q, Q's Secret now holds a deleted key. Q's reconcile does not notice, because its Secret is still non-empty and its spec did not drift, so Q's provisioner daemons fail to authenticate until something else rotates Q's key.

Where

On main (internal/controller/coderprovisioner_controller.go at ef86c32), the by-name delete followed by a create runs in these paths:

  • drift rotation, which deletes the replaced key by name at its old URL (line 306; PR 🤖 fix: revoke a CoderProvisioner key created at an intermediate control plane endpoint #413 keeps this only for a replaced key in the target slot), and deletes the target key by name when it returns no plaintext (lines 342-345);
  • Secret loss, "provisioner key exists in coderd but secret is missing, rotating key to recover" (lines 427-430);
  • metadata backfill for status from older operators (line 519).

None of them checks that the key ID is one that this provisioner recorded.

Scope note

PR #413 (issue #259) closes the same hazard for the cleanup of a status.pendingKeys entry without key IDs: that cleanup now deletes no key and reports a same-named key with the UnverifiedKeyKept condition. The target paths above are older than #413 and are not changed by it.

Possible direction

Delete by name at the target only when the existing key's ID is one that this provisioner recorded (status.provisionerKeyID or a status.pendingKeys entry). Otherwise do not delete it: set a condition that names the conflicting key, and stop before the create. Status from older operators (no IDs) and a create whose answer was lost both need a path, for example a documented manual delete.


Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: xhigh

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions