Service accounts
A service account is a non-human principal — a bot, a CI runner, a backend daemon. It owns access keys, gets policies attached, shows up in audit logs. The right primitive whenever code (not a person) needs to call Huudis-protected APIs. The Go SDK exposes them on client.IAM. For the HTTP surface see API → Service accounts; for the credential model see Access keys.
Field on the Client
client.IAM — the service-account methods sit alongside the user methods. No Update — service accounts are immutable past creation. Rename by creating fresh + cutover.
Methods
ListServiceAccounts
Signature. func (r *IamResource) ListServiceAccounts(ctx context.Context, auth RequestAuth) ([]IamServiceAccount, error)
Lists every service account in the workspace.
sas, _ := client.IAM.ListServiceAccounts(ctx, huudis.RequestAuth{})
for _, sa := range sas {
fmt.Println(sa.ID, sa.Name, sa.Description)
}
GetServiceAccount
Signature. func (r *IamResource) GetServiceAccount(ctx context.Context, id string, auth RequestAuth) (*IamServiceAccount, error)
Fetches one service account by ID.
sa, _ := client.IAM.GetServiceAccount(ctx, "sa_01HX...", huudis.RequestAuth{})
CreateServiceAccount
Signature. func (r *IamResource) CreateServiceAccount(ctx context.Context, in CreateServiceAccountInput, auth RequestAuth) (*IamServiceAccount, error)
Creates a service account. Name must be unique inside the workspace and is used in the ARN (arn:huudis:iam::{accountId}:service-account/{name}) — pick something stable.
sa, err := client.IAM.CreateServiceAccount(ctx, huudis.CreateServiceAccountInput{
Name: "github-actions-deploy",
Description: "Deploys saas-acme from main branch CI",
}, huudis.RequestAuth{})
if err != nil {
return err
}
A freshly created service account has no credentials and no permissions. The two next steps:
- Mint an access key with
client.IAM.CreateAccessKey(...). - Attach a policy with
client.IAM.AttachPolicy(...).
DeleteServiceAccount
Signature. func (r *IamResource) DeleteServiceAccount(ctx context.Context, id string, auth RequestAuth) error
Removes the service account. Cascades to its access keys (all revoked + deleted) and its policy attachments (all detached). Audit records keep the ARN as a stable string, so deleted SAs still resolve in the audit UI.
_ = client.IAM.DeleteServiceAccount(ctx, "sa_01HX...", huudis.RequestAuth{})
Types
type IamServiceAccount struct {
ID string `json:"id"` // "sa_..."
AccountID string `json:"accountId"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
CreatedAt string `json:"createdAt"` // ISO-8601
}
For the ARN format and per-field rules see API → Service accounts.
Common patterns
Provision a CI principal end-to-end
The full ritual — service account + access key + scoped policy — wrapped:
type CIPrincipal struct {
AccessKeyID string
SecretAccessKey string
}
func provisionCiPrincipal(ctx context.Context, c *huudis.Client, name, policyID string) (*CIPrincipal, error) {
sa, err := c.IAM.CreateServiceAccount(ctx, huudis.CreateServiceAccountInput{
Name: name,
Description: "CI principal for " + name,
}, huudis.RequestAuth{})
if err != nil {
return nil, err
}
arn := fmt.Sprintf("arn:huudis:iam::%s:service-account/%s", sa.AccountID, sa.Name)
key, err := c.IAM.CreateAccessKey(ctx, huudis.CreateAccessKeyInput{
PrincipalArn: arn,
Description: "initial",
}, huudis.RequestAuth{})
if err != nil {
return nil, err
}
if _, err := c.IAM.AttachPolicy(ctx, huudis.AttachPolicyInput{
PolicyID: policyID,
PrincipalArn: arn,
}, huudis.RequestAuth{}); err != nil {
return nil, err
}
return &CIPrincipal{
AccessKeyID: key.ID,
SecretAccessKey: key.SecretAccessKey, // SAVE NOW
}, nil
}
One service account per service
The shape that scales: one SA per downstream consumer, never a shared "deploy" SA used by ten services. The blast radius of a leaked key is then one service, not your whole infra.
for _, s := range []string{"huudis-frontend", "huudis-backend", "huudis-cli", "huudis-cron"} {
_, _ = client.IAM.CreateServiceAccount(ctx, huudis.CreateServiceAccountInput{
Name: s + "-runner",
Description: "CI runner for " + s,
}, huudis.RequestAuth{})
}
Inventory unused service accounts
Cross-reference with access keys' LastUsedAt:
func unusedSas(ctx context.Context, c *huudis.Client, maxDays int) ([]huudis.IamServiceAccount, error) {
sas, err := c.IAM.ListServiceAccounts(ctx, huudis.RequestAuth{})
if err != nil {
return nil, err
}
cutoff := time.Now().Add(-time.Duration(maxDays) * 24 * time.Hour)
var stale []huudis.IamServiceAccount
for _, sa := range sas {
arn := fmt.Sprintf("arn:huudis:iam::%s:service-account/%s", sa.AccountID, sa.Name)
keys, _ := c.IAM.ListAccessKeys(ctx, huudis.ListAccessKeysParams{PrincipalArn: arn}, huudis.RequestAuth{})
var lastUsed time.Time
for _, k := range keys {
if k.LastUsedAt == "" {
continue
}
t, _ := time.Parse(time.RFC3339, k.LastUsedAt)
if t.After(lastUsed) {
lastUsed = t
}
}
if lastUsed.Before(cutoff) {
stale = append(stale, sa)
}
}
return stale, nil
}
Migrate from a user-owned key to a service account
Common when a teammate's personal access key has accumulated machine usage:
func migrateToSa(ctx context.Context, c *huudis.Client, userArn, newSaName string) (string, error) {
attachments, err := c.IAM.ListPolicyAttachments(ctx, huudis.ListPolicyAttachmentsParams{
PrincipalArn: userArn,
}, huudis.RequestAuth{})
if err != nil {
return "", err
}
sa, err := c.IAM.CreateServiceAccount(ctx, huudis.CreateServiceAccountInput{Name: newSaName}, huudis.RequestAuth{})
if err != nil {
return "", err
}
newArn := fmt.Sprintf("arn:huudis:iam::%s:service-account/%s", sa.AccountID, sa.Name)
for _, a := range attachments {
if _, err := c.IAM.AttachPolicy(ctx, huudis.AttachPolicyInput{
PolicyID: a.PolicyID,
PrincipalArn: newArn,
}, huudis.RequestAuth{}); err != nil {
return "", err
}
}
return newArn, nil
}
Errors
Code |
Status |
Cause |
|---|---|---|
validation_error |
400 | Name uses illegal characters (must match [a-z0-9-]{3,64}), description too long. |
conflict |
409 | Name already used in this workspace. |
not_found |
404 | SA ID doesn't exist or lives in another workspace. |
forbidden |
403 | Caller isn't an admin/owner. |
Next
- Access keys — mint credentials for a service account.
- Policies — grant permission to a service account.
- API → Service accounts — HTTP-level reference.