Access keys

An access key is a long-lived credential pair — a key ID and a secret — that a non-interactive principal uses to authenticate against Huudis-protected APIs. Service accounts, CI runners, and backend daemons live behind access keys; humans use OIDC. The Go SDK exposes them on client.IAM. For the underlying HTTP shape see API → Access keys; for the principal model see Concepts → Principals.

Field on the Client

client.IAM — the access-key methods below are on this same namespace as the user methods. There's no Get(id) and no Update — access keys are write-once. To rotate, mint a new one and revoke the old.

Methods

ListAccessKeys

Signature. func (r *IamResource) ListAccessKeys(ctx context.Context, p ListAccessKeysParams, auth RequestAuth) ([]IamAccessKey, error)

Lists every key in the workspace, or — with PrincipalArn — every key for one principal. The returned list never contains a SecretAccessKey; the secret is only revealed once at create-time.

keys, err := client.IAM.ListAccessKeys(ctx, huudis.ListAccessKeysParams{
    PrincipalArn: fmt.Sprintf("arn:huudis:iam::%s:service-account/build-bot", accountID),
}, huudis.RequestAuth{})
if err != nil {
    return err
}
for _, k := range keys {
    last := k.LastUsedAt
    if last == "" {
        last = "never used"
    }
    fmt.Println(k.ID, k.Status, last)
}

CreateAccessKey

Signature. func (r *IamResource) CreateAccessKey(ctx context.Context, in CreateAccessKeyInput, auth RequestAuth) (*IamAccessKey, error)

Creates a key for a principal you already control — usually a service account. Description is optional but strongly recommended — it's the only human-readable identifier on the key list.

sa, err := client.IAM.CreateServiceAccount(ctx, huudis.CreateServiceAccountInput{
    Name: "build-bot",
}, huudis.RequestAuth{})
if err != nil {
    return err
}
arn := fmt.Sprintf("arn:huudis:iam::%s:service-account/%s", sa.AccountID, sa.Name)

key, err := client.IAM.CreateAccessKey(ctx, huudis.CreateAccessKeyInput{
    PrincipalArn: arn,
    Description:  "GitHub Actions — main branch deploys",
}, huudis.RequestAuth{})
if err != nil {
    return err
}

// SAVE NOW — never shown again
storeSecret(key.ID, key.SecretAccessKey)

The secret is shown exactly once. SecretAccessKey is set on the create response and is empty on every other call. Pipe it directly into your secret store; never log it.

RevokeAccessKey

Signature. func (r *IamResource) RevokeAccessKey(ctx context.Context, id string, auth RequestAuth) (*IamAccessKey, error)

Marks the key Status = "revoked". Revocation is immediate and propagates to every Huudis-protected service inside one minute via the JWKS rotation channel. The key row stays for audit; DeleteAccessKey removes it entirely.

_, err := client.IAM.RevokeAccessKey(ctx, "akid_01HX...", huudis.RequestAuth{})

Revoke first, delete later — keeping the revoked row makes audit trails readable.

DeleteAccessKey

Signature. func (r *IamResource) DeleteAccessKey(ctx context.Context, id string, auth RequestAuth) error

Permanently removes the key row. Audit entries still reference it by ID, so deletion is safe — you only lose the human-readable description column.

if err := client.IAM.DeleteAccessKey(ctx, "akid_01HX...", huudis.RequestAuth{}); err != nil {
    return err
}

Types

type IamAccessKey struct {
    ID              string `json:"id"`              // "akid_..."
    AccountID       string `json:"accountId"`
    PrincipalArn    string `json:"principalArn"`    // "arn:huudis:iam::acc_X:service-account/Y"
    Description     string `json:"description,omitempty"`
    Status          string `json:"status"`          // "active" | "revoked"
    SecretAccessKey string `json:"secretAccessKey,omitempty"` // only on create
    CreatedAt       string `json:"createdAt"`       // ISO-8601
    LastUsedAt      string `json:"lastUsedAt,omitempty"`
}

LastUsedAt is updated lazily — sub-second-stale is normal. Full field rules at API → Access keys.

Common patterns

Rotate a key with zero downtime

The textbook: mint the new key, ship it to consumers, watch the old key's LastUsedAt go cold, then revoke. Don't delete until you're confident nothing's still using it.

func rotate(ctx context.Context, c *huudis.Client, oldKeyID, principalArn string) error {
    fresh, err := c.IAM.CreateAccessKey(ctx, huudis.CreateAccessKeyInput{
        PrincipalArn: principalArn,
        Description:  "rotation " + time.Now().UTC().Format(time.RFC3339),
    }, huudis.RequestAuth{})
    if err != nil {
        return err
    }
    if err := deploySecret(fresh.ID, fresh.SecretAccessKey); err != nil {
        return err
    }
    for {
        keys, err := c.IAM.ListAccessKeys(ctx, huudis.ListAccessKeysParams{
            PrincipalArn: principalArn,
        }, huudis.RequestAuth{})
        if err != nil {
            return err
        }
        var old *huudis.IamAccessKey
        for i := range keys {
            if keys[i].ID == oldKeyID {
                old = &keys[i]
                break
            }
        }
        if old == nil || ageMinutes(old.LastUsedAt) > 60 {
            break
        }
        time.Sleep(60 * time.Second)
    }
    _, err = c.IAM.RevokeAccessKey(ctx, oldKeyID, huudis.RequestAuth{})
    return err
}

Audit stale keys

A monthly cron:

func staleKeys(ctx context.Context, c *huudis.Client, maxDays int) ([]huudis.IamAccessKey, error) {
    all, err := c.IAM.ListAccessKeys(ctx, huudis.ListAccessKeysParams{}, huudis.RequestAuth{})
    if err != nil {
        return nil, err
    }
    cutoff := time.Now().Add(-time.Duration(maxDays) * 24 * time.Hour)
    var stale []huudis.IamAccessKey
    for _, k := range all {
        last := k.LastUsedAt
        if last == "" {
            last = k.CreatedAt
        }
        t, _ := time.Parse(time.RFC3339, last)
        if t.Before(cutoff) && k.Status == "active" {
            stale = append(stale, k)
        }
    }
    return stale, nil
}

Single-use CI keys

key, err := c.IAM.CreateAccessKey(ctx, huudis.CreateAccessKeyInput{
    PrincipalArn: principalArn,
    Description:  "ci-" + os.Getenv("GITHUB_RUN_ID"),
}, huudis.RequestAuth{})
if err != nil {
    return err
}
defer func() {
    _, _ = c.IAM.RevokeAccessKey(context.Background(), key.ID, huudis.RequestAuth{})
    _ = c.IAM.DeleteAccessKey(context.Background(), key.ID, huudis.RequestAuth{})
}()

cmd := exec.Command("npm", "run", "deploy")
cmd.Env = append(os.Environ(),
    "HUUDIS_ACCESS_KEY_ID="+key.ID,
    "HUUDIS_SECRET_ACCESS_KEY="+key.SecretAccessKey,
)
if err := cmd.Run(); err != nil {
    return err
}

Errors

Code Status Cause
validation_error 400 Malformed PrincipalArn, unknown principal.
not_found 404 Key ID doesn't exist or lives in another workspace.
quota_exceeded 429 Default cap is 5 active keys per principal — revoke unused ones first.
forbidden 403 Caller can't mint keys for that principal (cross-account or insufficient scope).

Next