Overview
Inference API keys provide authentication for accessing AI inference endpoints. Each key is scoped to a project and can be used to make requests to any inference endpoint the project has access to.
API Key Model
API keys are stored in the api_key table with the following properties:
class ApiKey < Sequel::Model
many_to_one :project, read_only: true
plugin ResourceMethods, encrypted_columns: :key
include SubjectTag::Cleanup # personal access tokens
include ObjectTag::Cleanup # inference tokens
end
Key Attributes
| Attribute | Type | Description |
|---|
id | UUID | Unique identifier |
ubid | String | Human-readable ID (e.g., et8vw2vcw4vxby1gzgt96p5d2) |
key | String | Encrypted API key (32 alphanumeric characters) |
owner_table | String | Either "accounts" or "project" |
owner_id | UUID | ID of the owning account or project |
used_for | String | Either "api" or "inference_endpoint" |
project_id | UUID | Associated project |
is_valid | Boolean | Whether the key is active |
Creating API Keys
Inference Endpoint Keys
Create an API key for inference endpoint access:
api_key = ApiKey.create_inference_api_key(project)
This creates a key with:
owner_table: "project"
owner_id: project.id
used_for: "inference_endpoint"
key: Randomly generated 32-character alphanumeric string
Personal Access Tokens
Create a personal access token for API access:
api_key = ApiKey.create_personal_access_token(account, project: project)
This creates a key with:
owner_table: "accounts"
owner_id: account.id
used_for: "api"
CLI Command
Create an inference API key via CLI:
Response:
Created inference API key with id:et8vw2vcw4vxby1gzgt96p5d2 key:AbCdEfGhIjKlMnOpQrStUvWxYz123456
The key is only shown once during creation. Store it securely - it cannot be retrieved later.
Key Generation
Keys are automatically generated using secure random generation:
def self.random_key
SecureRandom.alphanumeric(32)
end
def before_validation
if new?
self.key ||= ApiKey.random_key
unless %w[project accounts].include?(owner_table)
fail "Invalid owner_table: #{owner_table}"
end
end
super
end
Using API Keys
Include the API key in the Authorization header:
curl -X POST https://your-endpoint.ai.ubicloud.com/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer AbCdEfGhIjKlMnOpQrStUvWxYz123456" \
-d '{...}'
Inference Gateway Validation
The inference gateway validates API keys on each replica:
- Hash the key: SHA2-256 digest of the provided key
- Check eligibility: Verify the key belongs to an active project with:
- Valid payment method OR available credits OR free quota
- At least one valid inference endpoint API key
- Apply rate limits: Enforce per-project quotas
eligible_projects = eligible_projects_ds.all
.select(&:active?)
.map do
{
ubid: it.ubid,
api_keys: it.api_keys
.select { |k| k.used_for == "inference_endpoint" && k.is_valid }
.map { |k| Digest::SHA2.hexdigest(k.key) },
quota_rps: max_project_rps,
quota_tps: max_project_tps
}
end
API keys are stored as SHA-256 hashes in the inference gateway for security. The gateway never receives the raw key.
Key Rotation
Rotate an API key to generate a new value:
api_key.rotate
# Generates new 32-character key and updates timestamp
Implementation:
def rotate
new_key = ApiKey.random_key
update(key: new_key, updated_at: Time.now)
end
Generate new key
A new random 32-character alphanumeric key is created
Update record
The encrypted key and timestamp are updated atomically
Propagate changes
The inference gateway receives the new key hash on the next ping (every 120 seconds)
Access Control Integration
API keys can be used as subjects in ABAC policies.
Unrestricted Access
Grant a token full access to a project:
api_key.unrestrict_token_for_project(project.id)
This adds the key to the “Admin” subject tag:
def unrestrict_token_for_project(project_id)
AccessControlEntry.where(project_id:, subject_id: id).destroy
DB[:applied_subject_tag]
.insert_ignore
.insert(subject_id: id, tag_id: SubjectTag.where(project_id:, name: "Admin").select(:id))
end
Restricted Access
Remove unrestricted access:
api_key.restrict_token_for_project(project.id)
Implementation:
def restrict_token_for_project(project_id)
unrestricted_project_access_dataset(project_id).delete
end
def unrestricted_project_access_dataset(project_id)
DB[:applied_subject_tag]
.where(subject_id: id, tag_id: SubjectTag.where(project_id:, name: "Admin").select(:id))
end
Check Token Status
if api_key.unrestricted_token_for_project?(project.id)
# Token has full admin access
else
# Token has restricted access based on ACEs
end
Resource Paths
API keys have different paths based on their use:
def path
if used_for == "api"
"/token/#{ubid}/access-control"
else # inference_endpoint
"/inference-api-key/#{ubid}"
end
end
- Personal access tokens:
/token/et8vw2vcw4vxby1gzgt96p5d2/access-control
- Inference API keys:
/inference-api-key/et8vw2vcw4vxby1gzgt96p5d2
Listing API Keys
List all inference API keys for a project:
Filter by project:
project.api_keys.where(used_for: "inference_endpoint", is_valid: true)
Deleting API Keys
Delete an inference API key:
ubi ai api-key destroy <key-id>
This performs cleanup through the included modules:
module SubjectTag::Cleanup
def before_destroy
AccessControlEntry.where(subject_id: id).destroy
DB[:applied_subject_tag].where(subject_id: id).delete
super
end
end
module ObjectTag::Cleanup
def before_destroy
AccessControlEntry.where(object_id: id).destroy
DB[:applied_object_tag].where(object_id: id).delete
super
end
end
Deleting an API key immediately revokes access. In-flight requests may fail.
Security Best Practices
Key Storage
- Keys are encrypted at rest using the
encrypted_columns: :key plugin
- Only the key hash is transmitted to inference gateways
- Keys are never logged in plaintext
Key Lifecycle
- Create: Generate using cryptographically secure random generation
- Store: Encrypt immediately before database storage
- Use: Transmit over TLS only
- Rotate: Generate new keys periodically
- Delete: Revoke immediately and clean up all references
Validation
# Gateway validates keys by comparing hashes
stored_hash = Digest::SHA2.hexdigest(api_key.key)
provided_hash = Digest::SHA2.hexdigest(provided_key)
if stored_hash == provided_hash && api_key.is_valid
# Allow request
else
# Reject request
end
Project Quotas
API keys are only valid if the project has available quota:
eligible_projects_ds = Project.where(api_key_ds)
free_quota_exhausted_projects_ds = FreeQuota.get_exhausted_projects("inference-tokens")
eligible_projects_ds
.left_outer_join(valid_payment_method_ds, [:billing_info_id])
.exclude(valid_payment_method: nil, credit: 0.0, id: free_quota_exhausted_projects_ds)
A project must have at least one of:
- Valid payment method (not marked as fraud)
- Available credits
- Remaining free quota for inference tokens
Troubleshooting
Key Not Working
-
Verify key is valid:
api_key.is_valid # Should be true
-
Check project status:
project.active? # Should be true
-
Verify payment method:
project.has_valid_payment_method?
-
Check free quota:
FreeQuota.get_exhausted_projects("inference-tokens").where(id: project.id).empty?
Gateway Not Accepting Key
Keys are propagated to replicas every 120 seconds via the ping_gateway mechanism. After creating or rotating a key, wait up to 2 minutes for propagation.