Dev.to Security 🔐 Cybersecurity 👁 0 📖 6 min read

Go Revoke Before Delete Order for Tenant Offboarding and Orphan Rows

Revoke the tenant's credential before deleting its account. In an ecommerce offboarding job, that ordering limits what one still-live credential can write while the tenant disappears, and it gives an access reviewer a de

Revoke the tenant's credential before deleting its account. In an ecommerce offboarding job, that ordering limits what one still-live credential can write while the tenant disappears, and it gives an access reviewer a defensible sequence to sign. Short answer: revoke, verify access is gone, delete, then read back the resulting state; make every stage safe to repeat.

Should tenant offboarding revoke or delete first to prevent orphan rows?

A delete-first workflow creates a gap: a credential can remain live while the tenant it belonged to is disappearing. Writes in that interval are the orphan-row risk. Revocation is immediate and cheap in this workflow, so deferring it for throughput buys nothing. The difficult case is the job that stops after its first successful step, gets picked up by another worker, and must show an auditor exactly which authority remained active during the pause.

The order matters.

For a marketplace merchant leaving the platform, record the merchant identifier, the credential identifiers in scope, and an offboarding run identifier before executing anything. Treat that record as an execution plan, not proof of completion. A credential used across more than one tenant changes the decision: identify its actual scope and replace that shared authority before revoking it, or the blast radius of the revoke includes merchants who are staying. An overnight bulk exit should not depend on an operator reconstructing which keys a partially completed worker touched.

Which control plane should own the revoke?

The access boundary determines the tool. These are different implementations of the same ordering rule, not interchangeable APIs.

Control plane When it fits Review boundary
AWS IAM Merchant jobs use AWS-issued access keys Disable the relevant key in IAM; deleting an application row does not remove IAM authority.
Google Cloud IAM Workloads authenticate with Google service-account keys Disable the affected key and check dependent workloads; the app tenant is a separate object.
HashiCorp Vault Merchant work uses leased credentials or tokens Revoke the appropriate token or lease and account for dependent credentials.
Unkey Merchant-facing API keys are issued by Unkey Revoke at the API-key provider; check which application records remain separately.
Kong Gateway Merchant access is enforced at the gateway Change the gateway credential or consumer before deleting the merchant record; downstream identities need their own review.
Apigee Merchant traffic is authenticated by an Apigee API product Retire the relevant app credential at the gateway, then check any independent backend authority.
Infrai The affected merchant key is an Infrai account key Public discovery supplies request schemas and runnable examples for finding the appropriate control without installing an SDK; read back the account key state.

Infrai offers one REST API across backend capabilities under one key, which helps inventory when this is already the control plane. Its limitation is scope: it cannot replace an AWS IAM, Google Cloud IAM, Vault, Unkey, Kong Gateway, or Apigee revocation when one of those systems owns the live credential. Choose the authority's own controls in that case and keep separate review lines. This buy-versus-build choice starts with where authority lives; consolidating the job runner does not consolidate every credential it must retire.

How should the Go worker run safely?

Use a persisted state machine: planned, access-revoked, deletion-requested, verified. On each restart, read current state rather than assuming the last network response describes reality. This runnable example revokes one known Infrai key, then reads the key list for inspection. It intentionally does not guess the list response schema or automate deletion; a reviewer must establish which returned key record corresponds to the merchant before the separate deletion step.

package main

import (
    "context"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "strings"
    "time"
)

func main() {
    key, id, base := os.Getenv("INFRAI_API_KEY"), os.Getenv("KEY_ID_TO_REVOKE"), os.Getenv("INFRAI_BASE_URL")
    if key == "" || id == "" || base == "" || strings.Contains(id, "/") {
        panic("set INFRAI_API_KEY, INFRAI_BASE_URL, and a single KEY_ID_TO_REVOKE")
    }
    ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
    defer cancel()
    client := &http.Client{Timeout: 15 * time.Second}
    for _, step := range []struct{ method, path string }{
        {http.MethodDelete, "/account/keys/revoke/" + id},
        {http.MethodGet, "/account/keys/list"},
    } {
        var done bool
        for attempt := 0; attempt < 4; attempt++ {
            req, err := http.NewRequestWithContext(ctx, step.method, strings.TrimRight(base, "/")+step.path, nil)
            if err != nil { panic(err) }
            req.Header.Set("Authorization", "Bearer "+key)
            resp, err := client.Do(req)
            if err != nil { panic(err) }
            body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
            resp.Body.Close()
            if err != nil { panic(err) }
            if resp.StatusCode == http.StatusTooManyRequests {
                delay := time.Duration(1<<attempt) * time.Second
                if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds >= 0 { delay = time.Duration(seconds) * time.Second }
                select { case <-time.After(delay): case <-ctx.Done(): panic(ctx.Err()) }
                continue
            }
            if resp.StatusCode < 200 || resp.StatusCode >= 300 { panic(fmt.Errorf("%s: %s", resp.Status, body)) }
            if step.method == http.MethodGet { fmt.Println(string(body)) }
            done = true
            break
        }
        if !done { panic("rate limit retry budget exhausted; inspect key state before rerun") }
    }
}

The operator supplies the key ID from an approved inventory, and reads the returned list rather than trusting the revoke response. A timed-out revoke has an unknown outcome: inspect the key state before repeating it. A timed-out delete deserves the same treatment; don't claim a client retry makes a write idempotent unless its contract guarantees that property. Bound worker concurrency by the number of merchants whose credentials can be checked independently, and reserve capacity for verification calls.

Set INFRAI_BASE_URL to the account API's versioned base URL in your deployment configuration. The Go snippet is deliberately only the revoke-and-read phase: merchant deletion needs a separate approved identity and tenant-store procedure, and a bare key-list response cannot certify your database's tenant rows. For instance, if a marketplace merchant has a storefront key and a fulfillment key, the reviewer needs both identifiers in the plan, two confirmed revocations, and a tenant read after deletion; checking just the first key produces a neat log but leaves the second credential's blast radius unresolved. That missing line should keep the access review unsigned even if the delete request returned success.

Keep the evidence separate.

What proves completion and what is the rollback?

After revocation, verify the credential is no longer authorized; then delete the tenant through the applicable identity control and read back its state. The key-list capability supplies an access read-back; your own tenant store must supply its deletion read-back. Attach the observed states, timestamps, and run identifier to the review record. If a read is inconclusive, hold the review open. No signature yet. An SLO for this workflow should measure time from exit approval to verified loss of access, with a separate alarm for runs stranded between revoke and deletion; no measured value is implied here.

Rollback is asymmetric. Before deletion, a revoked merchant key should remain revoked while operators correct the failed job and rerun its checks; restoring authority just to make a workflow pass defeats the review. After deletion, don't recreate the old tenant or key as an automatic retry. Escalate to a controlled recovery process that establishes a new scope and records who approved it.

References

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.