Skip to content

feat: CRD validator.operatorKeyring surface for in-pod governance signing #219

@bdchatham

Description

@bdchatham

Problem

The SeiNode.spec.validator CRD today surfaces only consensus identity (signingKeypriv_validator_key.json) and P2P identity (nodeKeynode_key.json). There is no surface for the operator account keyring (the node_admin / seivaloper… key) that signs governance, staking, and upgrade-proposal transactions.

As validators migrate from EC2 hosts — where the team's seienv tool SSHes in and runs seid tx gov vote --from node_admin against the host's ~/.seid/keyring-file/ — to Kubernetes pods managed by this controller, the operator-account keyring has no home in the pod and no declarative surface.

Impact

  • Operators running validators on sei-k8s-controller cannot perform governance operations (vote, submit-proposal, deposit) from in-cluster tooling.
  • Blocks the seictl + sidecar governance-signing workstream (sister issues in sei-protocol/seictl).
  • Keys must otherwise live on an operator's laptop or a separate jumphost, defeating the "keys never leave the pod" goal.

Relevant experts

  • kubernetes-specialist — CRD shape, RBAC, mount wiring in internal/noderesource/
  • blockchain-developer — semantics of operator-account vs consensus key separation
  • platform-engineer — Secret lifecycle, GitOps integration with SOPS / ESO

Proposed approach

Mirror the existing SigningKeySource / NodeKeySource discriminated-union shape:

  1. Add ValidatorSpec.OperatorKeyring *OperatorKeyringSource in api/v1alpha1/validator_types.go. Reserve future siblings (TMKMS, Vault) as comments per the existing pattern.
  2. First variant: SecretOperatorKeyringSource with secretName (same-namespace) and optional keyName (the keyring entry name, default node_admin). secretName CEL-immutable via self == oldSelf.
  3. CRD CEL invariants:
    • operatorKeyring.secret.secretName MUST differ from both signingKey.secret.secretName and nodeKey.secret.secretName (trust-boundary, parallel to the existing distinct-Secret rule at validator_types.go:7).
    • operatorKeyring is independently optional — does NOT need to be paired with signingKey / nodeKey. Permits non-signing observers that still want governance-only key surface.
  4. Mount target: sidecar container only (not the seid main container) at \$SEI_HOME/keyring-file/, DefaultMode: 0o400, with subPath semantics. Add the volume/mount construction in internal/noderesource/noderesource.go symmetric to signingKeyVolumes / signingKeyMounts.
  5. New pre-flight validation task validate-operator-keyring in internal/task/. Gets the Secret, verifies non-empty, parses the keyring index to confirm at least one key entry exists. Terminal vs transient error split as in validate_signing_key.go. Plan-gated before apply-statefulset (internal/planner/planner.go:496-509).
  6. OperatorKeyringReady condition in api/v1alpha1/seinode_types.go symmetric to SigningKeyReady / NodeKeyReady, with Validated|NotReady|Invalid reasons.
  7. Bootstrap-pod guard: extend (or generalize) the assertNoSigningKeyOnBootstrapPod check in internal/task/bootstrap_resources.go to also assert no operator-keyring Secret leaks into the bootstrap pod.
  8. RBAC: existing secrets:get;list;watch covers it. No new permission.
  9. Documentation: extend the BYO-validator runbook in .tide/validator-migration.md to cover operator-keyring import.

Acceptance criteria

  • validator.operatorKeyring field added with kubebuilder markers and CEL validation
  • CEL invariants enforced at admission (covered by unit tests of validating webhook)
  • validate-operator-keyring task implemented and unit-tested
  • OperatorKeyringReady condition transitions correctly through Validated|NotReady|Invalid
  • Sidecar container mounts the Secret at \$SEI_HOME/keyring-file/, mode 0400; main seid container does NOT mount it
  • Bootstrap-pod guard extended to assert no operator-keyring Secret
  • make manifests generate regenerates CRD YAML and DeepCopy cleanly
  • e2e test deploys a SeiNode with operatorKeyring, confirms sidecar can read the keyring
  • .tide/validator-migration.md updated

Out of scope

  • Sidecar consumption of the keyring (tracked in seictl: "Sidecar production keyring backend support")
  • TMKMS / Vault / Remote signer variants (placeholder comment only)
  • Cross-namespace Secret references (separate workstream if/when needed)
  • Operator-keyring rotation flow (Secret is CEL-immutable; rotation is delete-recreate)

References

  • Coral session 2026-05-11
  • Existing pattern: api/v1alpha1/validator_types.goSigningKeySource / NodeKeySource discriminated unions
  • Existing pattern: internal/task/validate_signing_key.go
  • Existing pattern: internal/noderesource/noderesource.go:485-562 — Secret-volume / subPath mount
  • Bootstrap guard: internal/task/bootstrap_resources.go:333-348
  • Sister issues in sei-protocol/seictl: sidecar keyring backend, sign-tx task family, seictl gov CLI, sidecar authn

Sequencing

Phase 1, step 1 of 5 in the seictl/sidecar governance-flow migration off slanders/seienv. Pairs with the sidecar keyring-backend issue (must land before the sign-tx task family can consume it).

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions