# Tigris Security Access Control

> Use when configuring CORS, rotating access keys, setting bucket policies, or securing Tigris storage — covers key lifecycle, roles, CORS rules, presigned URL security, audit checklist

- Skill: `tigrisdata/tigris-security-access-control` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds add tigrisdata/tigris-security-access-control`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tigrisdata/tigris-security-access-control/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: tigrisdata (https://skillmd.com/u/tigrisdata)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/tigrisdata/tigris-security-access-control

---


# Tigris Security & Access Control

Configure access keys, CORS rules, bucket visibility, and presigned URL security for Tigris object storage. Covers key lifecycle management, role-based access, and security auditing.

## Prerequisites

**Before doing anything else**, install the Tigris CLI if it's not already available:

```bash
tigris help || npm install -g @tigrisdata/cli
```

If you need to install it, tell the user: "I'm installing the Tigris CLI (`@tigrisdata/cli`) so we can work with Tigris object storage."

## Quick Reference

| Operation | Command |
|-----------|---------|
| Create key | `tigris access-keys create "my-key"` |
| List keys | `tigris access-keys list` |
| Assign to bucket | `tigris access-keys assign <tid> --bucket <name> --role Editor` |
| Revoke key | `tigris access-keys delete <tid>` |
| Set CORS | `tigris buckets cors set <bucket> --config cors.json` |
| Get CORS | `tigris buckets cors get <bucket>` |
| Remove CORS | `tigris buckets cors delete <bucket>` |

---

## Access Key Lifecycle

### Create

```bash
tigris access-keys create "production-app-key"
# Output:
# Access Key ID: tid_xxx
# Secret Access Key: tsec_yyy  ← shown ONLY once, save immediately
```

### Assign to Bucket

```bash
# Editor: read + write + delete
tigris access-keys assign tid_xxx --bucket my-app-uploads --role Editor

# ReadOnly: read only
tigris access-keys assign tid_xxx --bucket my-app-uploads --role ReadOnly
```

| Role | Read | Write | Delete | Use When |
|------|------|-------|--------|----------|
| `Editor` | Yes | Yes | Yes | App servers that upload/modify files |
| `ReadOnly` | Yes | No | No | Services that only read/serve files |

### Scope Keys Narrowly

Create separate keys for different concerns:

```bash
# Key for the app server (read/write to uploads bucket)
tigris access-keys create "app-server"
tigris access-keys assign tid_app --bucket app-uploads --role Editor

# Key for the CDN/read service (read-only)
tigris access-keys create "cdn-reader"
tigris access-keys assign tid_cdn --bucket app-uploads --role ReadOnly

# Key for backups (write to backup bucket only)
tigris access-keys create "backup-writer"
tigris access-keys assign tid_bak --bucket app-backups --role Editor
```

### Rotate Keys (Zero Downtime)

```bash
# 1. Create new key
tigris access-keys create "production-app-key-v2"
tigris access-keys assign tid_new --bucket my-app-uploads --role Editor

# 2. Update application environment with new key
# (deploy with new TIGRIS_STORAGE_ACCESS_KEY_ID / SECRET)

# 3. Verify app works with new key

# 4. Revoke old key
tigris access-keys delete tid_old
```

### List and Audit

```bash
# List all access keys
tigris access-keys list

# Check which buckets a key can access
tigris access-keys info tid_xxx
```

---

## CORS Configuration

CORS is required for browser-based uploads (direct uploads, presigned PUT URLs).

### Set CORS Rules

```bash
tigris buckets cors set my-app-uploads --config cors.json
```

### Development (Localhost)

```json
{
  "CORSRules": [
    {
      "AllowedOrigins": ["http://localhost:3000", "http://localhost:5173"],
      "AllowedMethods": ["GET", "PUT", "POST", "DELETE", "HEAD"],
      "AllowedHeaders": ["*"],
      "ExposeHeaders": ["ETag", "Content-Length"],
      "MaxAgeSeconds": 3600
    }
  ]
}
```

### Production (Specific Domain)

```json
{
  "CORSRules": [
    {
      "AllowedOrigins": ["https://myapp.com", "https://www.myapp.com"],
      "AllowedMethods": ["GET", "PUT", "HEAD"],
      "AllowedHeaders": ["Content-Type", "Content-MD5", "Content-Disposition"],
      "ExposeHeaders": ["ETag"],
      "MaxAgeSeconds": 86400
    }
  ]
}
```

### Combined Dev + Production

```json
{
  "CORSRules": [
    {
      "AllowedOrigins": [
        "https://myapp.com",
        "https://www.myapp.com",
        "http://localhost:3000"
      ],
      "AllowedMethods": ["GET", "PUT", "POST", "DELETE", "HEAD"],
      "AllowedHeaders": ["*"],
      "ExposeHeaders": ["ETag", "Content-Length"],
      "MaxAgeSeconds": 3600
    }
  ]
}
```

### Permissive (Any Origin)

```json
{
  "CORSRules": [
    {
      "AllowedOrigins": ["*"],
      "AllowedMethods": ["GET"],
      "AllowedHeaders": ["*"],
      "MaxAgeSeconds": 86400
    }
  ]
}
```

**Warning:** Only use `"*"` origins for truly public, read-only content. Never allow `PUT`/`DELETE` from any origin.

### Verify CORS

```bash
# Check current CORS config
tigris buckets cors get my-app-uploads

# Test with curl
curl -I -H "Origin: https://myapp.com" \
  -H "Access-Control-Request-Method: PUT" \
  -X OPTIONS \
  https://my-app-uploads.t3.storage.dev/test
```

---

## Public vs Private Buckets

| Setting | Who Can Read | URL Access | Use For |
|---------|-------------|------------|---------|
| Private (default) | Only authenticated requests | Presigned URLs | User documents, sensitive files |
| Public | Anyone with URL | Direct URL | Static assets, public images, CDN content |

```bash
# Create public bucket
tigris buckets create my-public-assets --public

# Create private bucket (default)
tigris buckets create my-private-docs
```

---

## Presigned URL Security

### Best Practices

| Parameter | Recommendation |
|-----------|---------------|
| Expiration (download) | 5-60 minutes for most use cases |
| Expiration (upload) | 5-15 minutes |
| Scope | One URL per file, one operation per URL |

```typescript
import { getPresignedUrl } from "@tigrisdata/storage";

// Short-lived download URL
const { data } = await getPresignedUrl("documents/report.pdf", {
  operation: "get",
  expiresIn: 300, // 5 minutes
});

// Short-lived upload URL with content type restriction
const { data } = await getPresignedUrl("uploads/photo.jpg", {
  operation: "put",
  expiresIn: 600,
  contentType: "image/jpeg", // Client must upload this type
});
```

### Never Do

- Set expiration longer than 24 hours
- Generate presigned URLs for entire bucket prefixes
- Share presigned URLs in public channels (they're secrets)
- Use presigned URLs as permanent links (use public bucket URLs instead)

---

## Environment Variable Security

```bash
# .env (never commit this file)
TIGRIS_STORAGE_ACCESS_KEY_ID=tid_xxx
TIGRIS_STORAGE_SECRET_ACCESS_KEY=tsec_yyy
```

```bash
# .gitignore (must include)
.env
.env.local
.env.*.local
```

For CI/CD, use your platform's secrets management:

```bash
# GitHub Actions
gh secret set TIGRIS_STORAGE_ACCESS_KEY_ID
gh secret set TIGRIS_STORAGE_SECRET_ACCESS_KEY

# Vercel
vercel env add TIGRIS_STORAGE_ACCESS_KEY_ID

# Fly.io
fly secrets set TIGRIS_STORAGE_ACCESS_KEY_ID=tid_xxx
```

---

## Security Audit Checklist

- [ ] **No keys in code** — search for `tid_` and `tsec_` in your repo
- [ ] **`.env` in `.gitignore`** — credentials never committed
- [ ] **Keys scoped to buckets** — no unassigned keys with global access
- [ ] **Roles match need** — read-only services use `ReadOnly` role
- [ ] **CORS not overly permissive** — no `"*"` origin with write methods
- [ ] **Presigned URLs have short expiry** — under 1 hour for most cases
- [ ] **Unused keys revoked** — no stale keys from former team members or old deployments
- [ ] **Separate keys per environment** — dev/staging/production use different keys

---

## Key Compromise Response

If an access key is exposed (committed to git, leaked in logs, etc.):

```bash
# 1. Immediately revoke the compromised key
tigris access-keys delete tid_compromised

# 2. Create a new key
tigris access-keys create "replacement-key"
tigris access-keys assign tid_new --bucket my-bucket --role Editor

# 3. Update all deployments with the new key
# (update .env, CI/CD secrets, deployment configs)

# 4. Audit access — check for unauthorized uploads/downloads
tigris ls t3://my-bucket --recursive -l | sort -k4 -r | head -50

# 5. If key was committed to git, rotate and consider the entire
#    git history compromised — use git-filter-repo to remove it
```

---

## Critical Rules

**Always:** Create separate keys per environment and concern | Assign keys to specific buckets with minimum required role | Rotate keys periodically (quarterly recommended) | Set CORS to specific origins in production | Use short-lived presigned URLs

**Never:** Commit keys to git | Use a single key for all environments | Set CORS to `"*"` with write methods | Share presigned URLs publicly | Keep unused keys active

---

## Known Issues

| Problem | Fix |
|---------|-----|
| CORS preflight fails | Check `AllowedOrigins` includes your exact origin (with protocol) |
| "Access denied" after key rotation | Ensure new key is assigned to bucket with correct role |
| Secret key lost | Cannot recover — create new key and reassign |
| Browser upload fails silently | Check CORS config includes `PUT` in `AllowedMethods` |

---

## Related Skills

- **file-storage** — CLI setup and access key creation
- **tigris-s3-migration** — CORS and credential setup during migration

## Official Documentation

- Tigris: https://www.tigrisdata.com/docs/

