Skip to content

Implement admission, credential expiry and access revocation #977

Closed

Description

@privateip

Parent: #970

Deliverable

A transport-independent admission component that binds each accepted request to one authorized connector and gVPC and terminates forwarding when authority expires.

Dependencies

#972, #976, foundation #994; coordinate pre-admission limits and negative cases with #991.

Galactic Edge integration (#992)

Implement this milestone as a consumer of the Galactic Edge provider contract. #992 owns provider authorization, host attachment lifecycle, shared-state fencing and maintenance, independently enforced forwarding expiry, prefix conflict arbitration/publication, and gVPC traffic policy. MASQUE owns client authentication, connector /96 assignment and retention, session packet checks, transport behavior and its provider adapter. Use the foundation's versioned contract and existing provider helpers; do not introduce a second host-networking controller, lease store or BGP publisher. Any required foundation change must be tracked and qualified under #992 before this milestone claims integrated acceptance.

Work

  • Implement the selected credential profile and trust configuration, resolving an authenticated principal to explicit connector/gVPC grants.
  • Bind immutable authorization context to each CONNECT-IP request; reject missing, invalid, expired, ambiguous or unauthorized credentials and references.
  • Consume revocation and authorization updates through the accepted internal interface and cap forwarding validity by credential, grant and ownership deadlines.
  • Provide an internal test/configuration path to exercise enrollment-independent credentials and grants. Do not add a public API, client login workflow or plugin/portal integration.

Acceptance criteria

  • Unit and integration tests cover valid admission, wrong issuer/audience where applicable, expiry, revoked access and attempts to select another gVPC.
  • Existing forwarding stops within the agreed bound after expiry or revocation, including the documented control-plane outage case.
  • Authorization cannot be extended merely by reconnecting or replaying stale grant state.
  • The component exposes the same decision and immutable context to HTTP/3 and HTTP/2 adapters; credentials are absent from logs.

Additional integration acceptance

Scope follows #970: CONNECT-IP with HTTP/2 fallback; no public API development, plugin/portal integration, product-client development or production rollout.

Activity

  1. changed the title [-]MASQUE: implement admission, credential expiry and access revocation[/-] [+]Implement admission, credential expiry and access revocation[/+] on Oct 22, 2026
  2. added theissue type on Oct 22, 2026
  3. privateip commented on Oct 28, 2026

    @privateip
    CollaboratorAuthor

    Verdict: valid. The capability is missing on main at 69640a5.

    How this was confirmed. This is a missing component rather than a misbehaving one, so the check was a source search, not a failing test.

    • internal/edge/masque/grant.go defines the Authorizer interface (Authorize, Refresh, Retire) that the adapter calls on admission, on every refresh tick and on replacement. Its doc comment states that authentication, /96 allocation and EdgeService issuance are not the adapter's job.
    • The only implementation is the conformance fixture in tests/edgeconformance/masqueprovider/config.go. It compares plaintext credentials from a config file, issues a one-hour deadline with no credential expiry, refers to EdgeServices the deployer created by hand, and forgets retirements when it restarts.
    • Nothing in the repository implements what the MASQUE contract assigns to this issue: digest-only credential storage, a separate grant authority that writes one finite EdgeService per authorization incarnation, the validUntil = min(credential expiry, read start + 30 s − 2 s) renewal, pod-bound provider identity on an authenticated TLS channel, revocation and rotation, and the operator tool for test connectors.

    What is already in place.

    • The foundation enforces EdgeService.spec.validUntil minus MaxSkew through admission, the attachment service controller and the route checks.
    • The adapter already ends a single connector's request when Refresh returns unauthenticated or forbidden, or when its deadline passes, without touching other connectors. Integrate connector ownership with Edge claims and independent expiry #976 added this.
    • internal/edge/admission.IdentityFromUserInfo turns a TokenReview result into a pod-bound identity, so the authority can reuse it.

    Findings beyond the issue text.

  4. self-assigned this
    on Oct 28, 2026
  5. privateip commented on Oct 28, 2026

    @privateip
    CollaboratorAuthor

    Approved plan.

    Root cause. masque.Authorizer has no production implementation. The only one is the conformance fixture's static config.

    Fix, on branch fix/issue-977:

    1. internal/edge/masque/authority, the core.
      • Grant records are one restricted Secret per connector. Each holds the connector UUID, the VPC namespace, name and UID, the cell, the credential's SHA-256 digest and revision, an expiry of at most 24 h, an enabled flag, the allowed destinations, the policy references, and the allocation.
      • Authorize compares digests in constant time. It reads the record and the VPC uncached and requires the VPC UID to match. It then creates one EdgeService per authorization incarnation, with PacketAccess and PrefixReachability, the /96 as its only prefix, the /120 pool as clientPool, and validUntil = min(credential expiry, read start + 28 s).
      • Annotations on the service record the authorization ID, pod UID, revision and nonce. The authority keeps no other state, so a new leader rereads everything, and a retry with the same nonce returns the same incarnation.
      • Refresh rereads the record and renews validUntil with UID and resourceVersion preconditions. It refuses a disabled or rotated revision, a retired incarnation and the wrong pod. When the authority can't read the record, it returns unavailable and never renews from a cache.
      • Retire and a revocation sweep set validUntil in the past. A retired incarnation is never renewed.
      • masque.Grant gains an authorization ID and a credential revision, and sameAuthority compares both.
    2. Ledger. One ConfigMap per VPC and cell holds at most 256 entries. Every insert or retirement uses UID and resourceVersion preconditions. Retired /96s and pools are tombstoned and never reassigned.
    3. Channel and client.
      • The channel is HTTPS with JSON over TLS 1.3. The operator mounts the server certificate and the CA.
      • The provider sends a projected ServiceAccount token for audience masque-authority. The authority checks it with a TokenReview and admission.IdentityFromUserInfo.
      • authority.Client implements masque.Authorizer for the adapter.
    4. Command. cmd/masque-authority has a serve command and grant create|rotate|disable|delete commands. Each grant command validates the VPC UID and the reserved pools, prints the bearer once and stores only its digest.
    5. Fixture and docs. The conformance fixture gets an authority mode, and the provider guide and the contract's status are updated.

    Tests.

    • Admission tests cover:
      • a valid request;
      • a wrong credential;
      • a wrong VPC selector;
      • a recreated VPC with a new UID;
      • an expired credential;
      • a disabled or rotated revision.
    • Refresh tests cover a stale revision or service UID, a request from another pod, a replayed nonce and a retired incarnation.
    • Store tests cover an unavailable store, which must issue no grant.
    • Ledger tests cover conflicts and tombstones.
    • An adapter-level test revokes one connector while another keeps the shared instance renewing. The first stops within its bound and the other keeps forwarding.
    • A test checks that no credential appears in logs.

    Risk. Almost all of the code is new. The only change to an existing API is the two new masque.Grant fields, which touches the fixture and the adapter tests. The foundation is unchanged.

    Out of scope.

  6. privateip commented on Oct 28, 2026

    @privateip
    CollaboratorAuthor

    Three changes to the approved plan. Each came from a test that failed while I implemented it.

    1. Authorize carries the time the call was first sent, and the authority keeps a retired incarnation for 10 s before deleting it. Without this, a late retry could create a second incarnation. The sequence that showed it:

      1. Another pod's takeover retired the old incarnation and deleted it.
      2. The predecessor then retried with its old nonce.
      3. Nothing in the API still carried that nonce, so the authority treated the retry as a new call.

      Now a retry either finds its retired incarnation and is refused, or arrives too late and is refused as stale. The cost is up to 10 s more on a cross-pod takeover, on top of waiting for core to release the old attachments.

    2. Incarnations are named masque-<connector>-r<revision>, not after the authorization ID. A second test raced three pods for one connector. One check after each create could not settle the race: an issuer that checked before its rival existed could lose on name order, and both incarnations stayed live. The authority already issues only after every other incarnation of the connector is gone, so racing issuers now contend for one name. The API server accepts exactly one create. The UID tells successive incarnations apart, and the authorization ID still includes the nonce.

    3. Revocation push notifications are not built. The provider learns of a revocation from its next Refresh, at most 5 s later. The issued validUntil stays the backstop.

    Renewing in place works against core. A new adapter check renews a live service's validUntil past its first deadline, through several new generations. The attachment, route and claim epoch stay unchanged.

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

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

Fields

Priority

None yet

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions