Skip to main content

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

AttributeTypeDescription
idUUIDUnique identifier
ubidStringHuman-readable ID (e.g., et8vw2vcw4vxby1gzgt96p5d2)
keyStringEncrypted API key (32 alphanumeric characters)
owner_tableStringEither "accounts" or "project"
owner_idUUIDID of the owning account or project
used_forStringEither "api" or "inference_endpoint"
project_idUUIDAssociated project
is_validBooleanWhether 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:
ubi ai api-key create
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

Authentication Header

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:
  1. Hash the key: SHA2-256 digest of the provided key
  2. 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
  3. 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
1

Generate new key

A new random 32-character alphanumeric key is created
2

Update record

The encrypted key and timestamp are updated atomically
3

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:
ubi ai api-key list
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

  1. Create: Generate using cryptographically secure random generation
  2. Store: Encrypt immediately before database storage
  3. Use: Transmit over TLS only
  4. Rotate: Generate new keys periodically
  5. 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

  1. Verify key is valid:
    api_key.is_valid  # Should be true
    
  2. Check project status:
    project.active?  # Should be true
    
  3. Verify payment method:
    project.has_valid_payment_method?
    
  4. 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.

Build docs developers (and LLMs) love