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.
SecretAccessKeyis 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
- Service accounts — the principals access keys are usually bound to.
- Policies — grant a principal permission.
- API → Access keys — HTTP-level reference.