On this page

TAG is designed so your secret keys never leave your hands. This page explains how authentication, authorization, and credential handling work under the hood.

## Setting up credentials  
TAG needs two sets of credentials to operate:

### Client credentials  
Your application clients use their own Tigris credentials, signing requests with standard AWS SigV4. No changes are needed on the client side beyond pointing the endpoint URL at TAG. TAG never sees or stores client secret keys.

### TAG's own credentials  
Your application must provide TAG with credentials via environment variables:

```bash
export AWS_ACCESS_KEY_ID=<TAG's access key>

export AWS_SECRET_ACCESS_KEY=<TAG's secret key>
```

In **transparent proxy mode** (the default) these are TAG's own Tigris credentials, used only to sign proxy headers and perform background cache fetches — so **read-only** access to the cached buckets is sufficient, and TAG's access key must belong to the same Tigris organization as client access keys. In **signing mode** these credentials populate TAG's local credential store: clients authenticate with them and TAG re-signs every request with them, so they must carry whatever permissions clients need (read-only is not sufficient if clients write). See [Credential handling by mode](/content/docs/acceleration-gateway/security/#credential-handling-by-mode/index.html) below.

## Authentication  
TAG operates in transparent proxy mode by default. In this mode, TAG forwards the client's original `Authorization` header to Tigris unchanged and adds cryptographically signed proxy headers (`X-Tigris-Forwarded-Host`, `X-Tigris-Proxy-Access-Key`, `X-Tigris-Proxy-Timestamp`, `X-Tigris-Proxy-Signature`) computed with TAG's own secret key. Tigris independently validates both the client's SigV4 signature and TAG's proxy signature in a single round-trip.

TAG never sees client secret keys, client credentials do not need to be stored anywhere other than on the client, and Tigris retains full control over authorization decisions.

### Signing mode  
Transparent proxy mode works only with Tigris (the `X-Tigris-Proxy-*` identity headers are meaningful only to Tigris). Set `TAG_TRANSPARENT_PROXY=false` (or `upstream.transparent_proxy: false`) to run **signing mode** instead, which works against any S3-compatible service — Tigris, AWS S3, MinIO, and others.

In signing mode TAG validates the incoming request signature against its local credential store, then **re-signs** the request for the upstream with the **same** **access key and secret** and streams it on. It re-signs (rather than forwarding the original signature) because it may transform the request — this is _not_ identity translation; the upstream sees the same identity as the client. Because TAG must know the secret for every access key it serves, clients must authenticate with the credentials in TAG's store (by default the `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` pair).

### Credential handling by mode  
| Aspect | Transparent proxy (default) | Signing mode |
| --- | --- | --- |
| Upstream request signature | Client's original signature, forwarded unchanged | Re-signed by TAG with the client's own credentials |
| Does TAG need client secret keys? | No — cache hits are validated locally using keys learned from Tigris | Yes — the secret for each client access key must be in TAG's store |
| Role of `AWS_*` credentials | TAG's own identity: signs proxy headers and background fetches (read-only) | The store's contents: clients present them and TAG re-signs every request (reads **and** writes) |
| Works with non-Tigris backends | No (Tigris only) | Yes (any S3-compatible service) |

## Local authentication  
TAG doesn't call Tigris on every request. After your client's first request, TAG learns enough to validate signatures locally — so cache hits skip the network entirely.

### How it works  
On the first request, TAG learns your client's derived signing keys from Tigris. Every request after that is validated locally:

**First request (key learning):**  
First Request (Key Learning)ClientTAGTigrisGET /bucket/key + AuthForward auth + proxy headers200 OK + X-Tigris-Proxy-Signing-KeysUnwrap & store derived keysGrant authz cache entry200 OK (signing key stripped)

**Subsequent requests (local validation):**  
Subsequent Requests (Local Validation)ClientTAGGET /bucket/key + AuthValidate SigV4 locallyAuthz cache → grantedServe from cache200 OK · X-Cache: HIT

## Access control flow  
Here's the full decision tree TAG uses when a request arrives. Most paths end with a forward to Tigris — TAG only serves from cache when it has both a valid signature and a cached authorization grant:

Access Control Decision TreeRequest arrivesAuth header?missingForward to TigrismalformedReject 4xxpresentKey known?noForward, learn keysyesSigV4 valid?mismatchForward, re-learnvalidAuthz cache?missForward, re-authorizehitAuthValidatedServe from cache

## Authorization lifecycle  
Authorization decisions are cached per `(accessKey, bucket)` pair:

| Event | Action |
| --- | --- |
| Tigris returns 2xx with signing keys | `AuthzCache.Grant(accessKey, bucket)` |
| Tigris returns 403 | `AuthzCache.Revoke(accessKey, bucket)` |
| TTL expires (10 min default) | Entry removed, next request re-authorizes |

Authorization is strictly per-bucket. A client may have access to some buckets but not others, and TAG enforces this at the cache level.

## Proxy header security  
### Preventing client injection  
TAG overwrites any client-supplied proxy header values with TAG's own computed values. Clients cannot impersonate TAG or bypass proxy authentication.

### Proxy signature computation  
TAG computes the proxy signature using its own secret key. Only TAG (and Tigris, which knows TAG's key) can produce a valid proxy signature.

## Endpoint validation  
TAG validates the upstream endpoint at startup. In **every mode** it must be a well-formed absolute `http://` or `https://` URL with a host.

In **transparent proxy mode** (the default) the host must additionally be one of the following — the `X-Tigris-Proxy-*` identity headers are meaningful only to Tigris:

| Pattern | Example | Use case |
| --- | --- | --- |
| `localhost` | `http://localhost:8080` | Development and testing |
| `*.tigris.dev` | `https://fly.storage.tigris.dev` | Tigris production domains |
| `*.storage.dev` | `https://t3.storage.dev` | Tigris storage domains |

Any other host is a fatal startup error in transparent mode. **Signing mode** does not apply this allowlist — it can front any S3-compatible endpoint. TAG only ever forwards to the single operator-configured upstream (never a client-chosen one), so this is not an open proxy.

## Error mapping  
| Auth error | S3 error code | HTTP status | Action |
| --- | --- | --- | --- |
| Signature mismatch | SignatureDoesNotMatch | 403 | Forward to Tigris |
| Unknown access key | InvalidAccessKeyId | 403 | Forward to Tigris |
| Expired request | RequestTimeTooSkewed | 403 | Forward to Tigris |
| Malformed auth | MalformedAuth | 400 | Reject at TAG |
| Missing auth | (none) | (none) | Forward to Tigris |
