Set Up a Service Identity
Give a production workload its own principal and scoped API keys, so credentials rotate without touching the workload's identity or access.
Set Up a Service Identity
A key issued to a person carries that person's authority. When the person changes teams, loses a permission, or leaves, every workload running on their key changes with them. A service identity avoids this: it is a durable principal that belongs to the workload itself. Grants attach to the identity, keys are issued against it, and keys can be rotated indefinitely without changing what the workload is or what it can do.
This guide sets up a retrieval service that reads from one space. The same steps work for an ingestion job, a batch pipeline, or anything else that runs unattended, and every step is shown for the CLI and for the REST API.
Before You Start
- A human credential: the
goodmemCLI authenticated as a human user, or a human API key for REST. Service identities are created by humans; a service key cannot create another service identity. - Export the values the REST examples use:
export GOODMEM_REST_URL="https://localhost:8080" # REST base URL; on GoodMem Cloud, your instance's https:// hostname
export GOODMEM_API_KEY="gm_your_key"The CLI speaks gRPC, which GoodMem Cloud instances do not expose. On Cloud, use the cURL or HTTPie tab — every step here is a plain REST call over HTTPS — or the console. If your own server uses a self-signed certificate, add -k to curl and --verify=no to HTTPie.
- The UUID of the space your workload will read. Find it with
goodmem space list --format jsonorGET /v1/spaces.
export SPACE_ID="70e025f6-76ca-4cbe-b8fc-7dab8e84590a"1. Onboard in One Command
The CLI bundles the whole setup:
goodmem service-identity onboard \
--display-name prod-search \
--description "Semantic retrieval for the support portal" \
--grant READ_SPACE:EXACT:SPACE:$SPACE_ID \
--grant LIST_MEMORY:EXACT:SPACE:$SPACE_ID \
--grant READ_MEMORY:DIRECT_MEMBERS_OF:SPACE:$SPACE_IDEach --grant is one authorization rule in the form OPERATION:SELECTOR[:RESOURCE_KIND:RESOURCE_UUID]:
- The operation comes from the operations catalog, for example
READ_MEMORYorCREATE_MEMORY. - The selector says which resources the rule covers.
EXACTnames the one resource in the last two segments.DIRECT_MEMBERS_OFnames everything directly inside a container, soREAD_MEMORY:DIRECT_MEMBERS_OF:SPACE:$SPACE_IDcovers every memory the space holds now or later.ANYandOWNalso exist and take no resource segments.
The three rules above are what a retrieval service needs: retrieval checks LIST_MEMORY on each requested space and READ_MEMORY on its direct members, and READ_SPACE lets the service read the space's metadata. The key does not need EXECUTE_EMBEDDER; retrieval runs the space's configured embedders as part of the workflow.
The command makes three kinds of API call in order: it creates the identity, creates one direct grant per --grant with the new identity as the grantee, and issues one SCOPED API key whose ceiling repeats the same rules. On success it prints the IDs and the raw key:
Service Identity ID: f031b49e-7a15-4c8c-b4e7-38f77225d2a6
API Key ID: 9be3170a-52cf-4c53-b9ba-2f2b4bd0aa46
Raw API Key: gm_xxxxxxxxxxxxxxxxxxxxxxxx
Grant 1 ID: 1a2b3c4d-...
Grant 2 ID: ...The raw API key is shown once. Store it in your secret manager before closing the terminal; the server keeps only a verifier and cannot display the key again.
The steps are separate calls, so a failure partway through leaves the completed steps in place. The command reports each step's status and its UUID, which is enough to inspect what exists and finish the remaining steps by hand. Use --quiet to print only the raw key, or --format json for the full structured result.
onboard is a client-side convenience; there is no single REST endpoint behind it. Over REST, make the three calls in the next section in that order.
2. Or Do It by Hand
Doing it by hand is useful when the grants and the key ceiling should differ, and it is the only path over REST. The example below sets up only the two retrieval gates.
Create the identity:
goodmem service-identity create --display-name prod-search
export SERVICE_ID="<the UUID from the output>"export SERVICE_ID="$(curl -sS --json '{"displayName": "prod-search"}' \
"$GOODMEM_REST_URL/v1/service-identities" \
--header "x-api-key: $GOODMEM_API_KEY" | jq -r '.serviceIdentityId')"export SERVICE_ID="$(http POST "$GOODMEM_REST_URL/v1/service-identities" \
x-api-key:"$GOODMEM_API_KEY" displayName="prod-search" | jq -r '.serviceIdentityId')"Grant it authority, one rule per call. Over REST, a grant names an audience (one principal, or { "allAuthenticated": true }) and a rule in the same shape a ceiling rule takes:
goodmem access-policy grant create \
--principal $SERVICE_ID \
--operation LIST_MEMORY --selector EXACT \
--resource-kind SPACE --resource-id $SPACE_ID
goodmem access-policy grant create \
--principal $SERVICE_ID \
--operation READ_MEMORY --selector DIRECT_MEMBERS_OF \
--resource-kind SPACE --resource-id $SPACE_IDcurl -sS --json @- "$GOODMEM_REST_URL/v1/access-policy/grants" \
--header "x-api-key: $GOODMEM_API_KEY" <<JSON
{
"audience": { "principalId": "$SERVICE_ID" },
"rule": { "operation": "LIST_MEMORY", "selector": "EXACT",
"assignedResource": { "kind": "SPACE", "resourceId": "$SPACE_ID" } }
}
JSON
curl -sS --json @- "$GOODMEM_REST_URL/v1/access-policy/grants" \
--header "x-api-key: $GOODMEM_API_KEY" <<JSON
{
"audience": { "principalId": "$SERVICE_ID" },
"rule": { "operation": "READ_MEMORY", "selector": "DIRECT_MEMBERS_OF",
"assignedResource": { "kind": "SPACE", "resourceId": "$SPACE_ID" } }
}
JSONhttp POST "$GOODMEM_REST_URL/v1/access-policy/grants" x-api-key:"$GOODMEM_API_KEY" <<JSON
{
"audience": { "principalId": "$SERVICE_ID" },
"rule": { "operation": "LIST_MEMORY", "selector": "EXACT",
"assignedResource": { "kind": "SPACE", "resourceId": "$SPACE_ID" } }
}
JSON
http POST "$GOODMEM_REST_URL/v1/access-policy/grants" x-api-key:"$GOODMEM_API_KEY" <<JSON
{
"audience": { "principalId": "$SERVICE_ID" },
"rule": { "operation": "READ_MEMORY", "selector": "DIRECT_MEMBERS_OF",
"assignedResource": { "kind": "SPACE", "resourceId": "$SPACE_ID" } }
}
JSONEach call returns the grant with its grantId. Issue a scoped key whose subject is the identity:
goodmem apikey create \
--subject $SERVICE_ID \
--authority-mode scoped \
--ceiling LIST_MEMORY:EXACT:SPACE:$SPACE_ID \
--ceiling READ_MEMORY:DIRECT_MEMBERS_OF:SPACE:$SPACE_IDcurl -sS --json @- "$GOODMEM_REST_URL/v1/apikeys" \
--header "x-api-key: $GOODMEM_API_KEY" <<JSON
{
"subjectPrincipalId": "$SERVICE_ID",
"authorityMode": "SCOPED",
"ceiling": [
{ "operation": "LIST_MEMORY", "selector": "EXACT",
"assignedResource": { "kind": "SPACE", "resourceId": "$SPACE_ID" } },
{ "operation": "READ_MEMORY", "selector": "DIRECT_MEMBERS_OF",
"assignedResource": { "kind": "SPACE", "resourceId": "$SPACE_ID" } }
]
}
JSONhttp POST "$GOODMEM_REST_URL/v1/apikeys" x-api-key:"$GOODMEM_API_KEY" <<JSON
{
"subjectPrincipalId": "$SERVICE_ID",
"authorityMode": "SCOPED",
"ceiling": [
{ "operation": "LIST_MEMORY", "selector": "EXACT",
"assignedResource": { "kind": "SPACE", "resourceId": "$SPACE_ID" } },
{ "operation": "READ_MEMORY", "selector": "DIRECT_MEMBERS_OF",
"assignedResource": { "kind": "SPACE", "resourceId": "$SPACE_ID" } }
]
}
JSONThe response carries rawApiKey once, alongside the key's metadata.
Order matters. At issuance, every ceiling rule must already be covered both by the identity's live authority and by yours, so a ceiling for authority the identity does not yet have is rejected with FAILED_PRECONDITION (HTTP 412). Create the grants first. Issuing a key whose subject is a service identity also requires MANAGE_ACCESS on that identity; as its owner, you have that automatically.
Service keys are always scoped — --authority-mode inherit-subject ("authorityMode": "INHERIT_SUBJECT") is only valid for a key you issue to yourself. A scoped key's ceiling is fixed at issuance, so grants the identity receives later do not flow into existing keys. An old credential sitting in a config file somewhere never quietly gains the authority its subject accumulates over time. See API Keys and Ceilings for how the two-sided check works.
3. Rotate Keys
Rotation issues a second key for the same subject, deploys it, and revokes the first. The issue call is the same as above; revocation is a delete:
goodmem apikey create \
--subject $SERVICE_ID \
--authority-mode scoped \
--ceiling LIST_MEMORY:EXACT:SPACE:$SPACE_ID \
--ceiling READ_MEMORY:DIRECT_MEMBERS_OF:SPACE:$SPACE_ID
# ...deploy the new key, then:
goodmem apikey delete $OLD_KEY_ID# ...issue the new key as in step 2 and deploy it, then:
curl -sS -X DELETE "$GOODMEM_REST_URL/v1/apikeys/$OLD_KEY_ID" \
--header "x-api-key: $GOODMEM_API_KEY"# ...issue the new key as in step 2 and deploy it, then:
http DELETE "$GOODMEM_REST_URL/v1/apikeys/$OLD_KEY_ID" x-api-key:"$GOODMEM_API_KEY"Deletion revokes the key permanently; the row is retained for audit and shows up in key listings as revoked. The identity, its grants, and its ownership are untouched throughout.
To audit which credentials a subject holds before revoking, filter the key listing:
goodmem apikey list --subject $SERVICE_ID --lifecycle usablecurl -sS "$GOODMEM_REST_URL/v1/apikeys?subjectPrincipalId=$SERVICE_ID&lifecycleState=USABLE" \
--header "x-api-key: $GOODMEM_API_KEY"http GET "$GOODMEM_REST_URL/v1/apikeys" x-api-key:"$GOODMEM_API_KEY" \
subjectPrincipalId=="$SERVICE_ID" lifecycleState==USABLE--lifecycle accepts not-yet-valid, usable, expired, and revoked (lifecycleState takes NOT_YET_VALID, USABLE, EXPIRED, REVOKED); --view basic (view=BASIC) trims the output to the identifying columns.
For scheduled rotation, give each key a validity window at issuance with --valid-from and --expires-at (RFC3339 timestamps on the CLI; validFrom and expiresAt in epoch milliseconds over REST). A key works from valid-from inclusive to expires-at exclusive, and neither bound can be changed later — replacing the key is the only way to extend it.
4. When the Owner Leaves
Deleting a human user does nothing to the service identities they created or own. The identity stays active and its keys keep authenticating. What the departed human leaves behind is administrative ownership, which someone else should pick up:
goodmem service-identity transfer-ownership $SERVICE_ID \
--new-owner 44e8175d-c57a-4cf2-8f65-a20c05ea2250curl -sS --json '{"newOwnerId": "44e8175d-c57a-4cf2-8f65-a20c05ea2250"}' \
"$GOODMEM_REST_URL/v1/service-identities/$SERVICE_ID:transferOwnership" \
--header "x-api-key: $GOODMEM_API_KEY"http POST "$GOODMEM_REST_URL/v1/service-identities/$SERVICE_ID:transferOwnership" \
x-api-key:"$GOODMEM_API_KEY" newOwnerId="44e8175d-c57a-4cf2-8f65-a20c05ea2250"The current owner, the instance owner, or an instance administrator can run this. The transfer changes the owner and nothing else — grants, roles, keys, and the identity's own authority are all preserved.
5. Housekeeping
goodmem service-identity list --label environment=production
goodmem service-identity update $SERVICE_ID --description "Retired from the support portal"
goodmem service-identity delete $SERVICE_IDcurl -sS "$GOODMEM_REST_URL/v1/service-identities?label.environment=production" \
--header "x-api-key: $GOODMEM_API_KEY"
curl -sS -X PUT --json '{"description": "Retired from the support portal"}' \
"$GOODMEM_REST_URL/v1/service-identities/$SERVICE_ID" \
--header "x-api-key: $GOODMEM_API_KEY"
curl -sS -X DELETE "$GOODMEM_REST_URL/v1/service-identities/$SERVICE_ID" \
--header "x-api-key: $GOODMEM_API_KEY"http GET "$GOODMEM_REST_URL/v1/service-identities" x-api-key:"$GOODMEM_API_KEY" \
label.environment==production
http PUT "$GOODMEM_REST_URL/v1/service-identities/$SERVICE_ID" x-api-key:"$GOODMEM_API_KEY" \
description="Retired from the support portal"
http DELETE "$GOODMEM_REST_URL/v1/service-identities/$SERVICE_ID" x-api-key:"$GOODMEM_API_KEY"Deletion is permanent. The identity becomes a tombstone, every key whose subject it is stops authenticating, and the UUID is never reused. Grants, role assignments, and audit history remain stored as historical facts. There is no undelete; for a workload that might come back, revoke its keys instead and leave the identity in place.
See Also
- Users and Service Identities — how principals, ownership, and deletion fit together
- Issue Scoped API Keys — ceilings in more depth, including keys for humans
- Isolate Agents on a Shared Instance — this pattern repeated once per agent
- Share a Space — granting access to people rather than workloads
- Console: Access Control — the same steps from the browser
- REST reference: service identities, access policy, API keys
Issue Scoped API Keys
Create API keys with immutable permission ceilings for agents, CI jobs, and integrations that should not hold your full authority.
Isolate Agents on a Shared Instance
Run several agents on one GoodMem instance, each confined to its own space, with one service identity and one ceiling-locked key per agent.