---
title: S3 setup
description: Connect Kache to AWS S3 or an S3-compatible service
---
Kache supports AWS S3, Cloudflare R2, Ceph, MinIO, and other compatible endpoints through its S3 backend.
## Configure the remote
```toml title="~/.config/kache/config.toml"
[cache.remote]
type = "s3"
bucket = "my-build-cache"
region = "eu-west-1"
```
The region defaults to `us-east-1`. Kache does not read `AWS_REGION` or `AWS_DEFAULT_REGION`, so set the bucket's region in `region` or `KACHE_S3_REGION`. With a wrong region, `kache doctor` fails the remote check and, when S3 reports it, names the bucket's actual region.
<Tabs items={["AWS S3", "Cloudflare R2", "Ceph or MinIO"]}>
<Tab value="AWS S3">
```toml
[cache.remote]
type = "s3"
bucket = "my-build-cache"
region = "eu-west-1"
profile = "build-cache"
```
</Tab>
<Tab value="Cloudflare R2">
```toml
[cache.remote]
type = "s3"
bucket = "my-build-cache"
endpoint = "https://<account-id>.r2.cloudflarestorage.com"
region = "auto"
```
</Tab>
<Tab value="Ceph or MinIO">
```toml
[cache.remote]
type = "s3"
bucket = "build-cache"
endpoint = "https://s3.internal.example.com"
region = "us-east-1"
```
Kache uses path-style bucket addressing for custom endpoints.
</Tab>
</Tabs>
## Credentials
Kache checks these sources in order:
1. `KACHE_S3_ACCESS_KEY` and `KACHE_S3_SECRET_KEY`
2. `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`, with `AWS_SESSION_TOKEN` when it is set
3. the selected profile, or the `default` profile when none is selected: its keys, legacy inline SSO settings, then `credential_process`
4. only when no profile is selected: environment-based web identity, then ECS/task credentials, then EC2 instance credentials
Both explicit `KACHE_S3_*` credential variables are required. If only one is present, Kache warns and continues down the standard credential chain. There is no session-token variable for them, so pass temporary credentials through `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` and `AWS_SESSION_TOKEN`.
Setting `AWS_ACCESS_KEY_ID` without `AWS_SECRET_ACCESS_KEY`, or the reverse, is an error: S3 requests fail with a message that names the missing variable, and Kache tries no other source. An empty variable counts as unset.
These environment variables override the fields of an S3 remote; a `gcs`, `oci` or `filesystem` remote ignores them:
| Environment | TOML field |
| --- | --- |
| `KACHE_S3_BUCKET` | `bucket` |
| `KACHE_S3_ENDPOINT` | `endpoint` |
| `KACHE_S3_REGION` | `region` |
| `KACHE_S3_PREFIX` | `prefix` |
| `KACHE_S3_PROFILE` | `profile` |
| `KACHE_S3_USER_AGENT` | `user_agent` |
`KACHE_S3_BUCKET` alone configures an S3 remote, and a `KACHE_S3_BUCKET` that is set but empty turns off the bucket the config file names.
For a persistent daemon, put remote settings in the config file or service definition. A daemon Kache starts in the background drops the six variables above and `KACHE_LOCAL_ONLY` from the environment it inherits, and keeps the rest, including credentials, `AWS_PROFILE` and `AWS_ENDPOINT_URL_S3`. `kache stats` warns when the daemon's configuration differs from this shell's, and its `Remote` row shows the remote the daemon uses.
### Selected profile
The remote's `profile` setting (or `KACHE_S3_PROFILE`) selects a profile, and so does an `AWS_PROFILE` that is not empty. The setting wins when both are present. Kache then loads credentials from that profile only, after the explicit keys in steps 1 and 2.
If the selected profile cannot supply credentials, S3 requests fail. Kache does not fall back to web identity, ECS or EC2 instance credentials: on a build machine with an instance role, a mistyped profile name would otherwise sign every request as that role. `kache doctor` fails its `Remote access` check with the profile, the setting that selected it, and the cause when Kache can tell:
- the profile is not defined in the config or credentials file
- one of those files cannot be read
- the profile uses a form Kache does not support, listed under [Compatibility boundaries](#compatibility-boundaries)
- its SSO token has expired, or its `credential_process` failed
Kache also logs the reason as a warning when it first appears and whenever it changes, so it shows in the daemon's log.
On an EC2 machine where `AWS_PROFILE` names a profile without credentials, such as one that only sets `region` for the AWS CLI, unset `AWS_PROFILE` in Kache's environment so it uses the instance role.
Kache logs which source supplied the credentials at info level, in `kache.log` and the daemon's log, when it first signs with that source and whenever the source changes. A passing `Remote access` check names the source too, for example `credentials from EC2 instance metadata accepted`.
### Behind a proxy
Kache asks for ECS and EC2 instance credentials through the HTTP client it uses for S3, and that client honors `HTTP_PROXY`, `HTTPS_PROXY` and `ALL_PROXY`. A metadata request sent through a proxy does not reach this machine's metadata service. When a proxy is set, exclude the metadata addresses:
```bash
export NO_PROXY=169.254.169.254,169.254.170.2
```
## Compatibility boundaries
The S3 implementation signs with SigV4. It does not currently load SigV4a/Multi-Region Access Point settings, assume-role profiles using `source_profile` or `credential_source`, modern `[sso-session]` profiles, or profile-based web identity. Resolve those credentials before starting Kache or use `credential_process`. When the selected profile is one of these, `kache doctor` says which.
`AWS_ENDPOINT_URL_S3` is accepted when no Kache endpoint is set. An endpoint stored only inside an AWS shared profile is not loaded; copy it to `cache.remote.endpoint` or `KACHE_S3_ENDPOINT`.
Use `https://` for an endpoint on another machine. Over plain `http://`, request signatures and cached objects travel unencrypted, and anyone on the path can replace an object: the file hashes Kache checks are stored inside the same pack. Kache warns when it starts with such an endpoint, and `kache doctor` fails it. Plain `http://` to a loopback address, such as a local test server, is fine.
Prefixes are normalized by trimming surrounding separators and collapsing repeated `/`. Backslashes and `.` or `..` segments are rejected. A normalized prefix may point at a different object path from an older non-canonical configuration.
## Object layout and policy
```text
{prefix}/v3/manifests/{crate}/{key}.json
{prefix}/v3/packs/{crate}/{key}.tar.zst
{prefix}/v3/predictions/...
{prefix}/_manifests/...
```
The default prefix is `artifacts`. Readers need only `s3:GetObject`; writers also need `s3:PutObject`. Prefix-scoped policies must include both `v3/` and `_manifests/`.
`s3:ListBucket` is optional. `kache sync` needs it, and so does `cache.remote_key_listing = true`. Without it, S3 answers a request for a missing key with `403` instead of `404`. Kache treats a refused read as a cache miss and says so once in its log. The cost is that a real refusal, such as a policy that denies the prefix or an expired token on an existence check, also looks like a miss: the build compiles instead of failing. A download that S3 refuses with a credential error (an expired token, a bad signature, clock skew) is still reported as an error.
Remote packs use zstd; local blobs remain uncompressed. Configure the level with `cache.compression_level` or `KACHE_COMPRESSION_LEVEL`.
Each v3 entry download is limited to 8 GiB of compressed data and 8 GiB of extracted file data. Kache rejects an oversized download before restoring it; a compiler invocation recompiles when the remote entry cannot be restored. These are per-entry limits, not a total memory budget for concurrent downloads.
Buffered downloads also share a 512 MiB memory budget per Kache process, including space for temporary copying. Larger requests run alone under the existing per-entry limits. Decoders and parsed metadata use memory outside this budget.
Kache never deletes remote objects. Bound the bucket with a lifecycle rule; [Bound a remote](/docs/remote-cache/bounding) lists the prefixes and ages.
## macOS LAN access
macOS may request Local Network permission when an installed LaunchAgent reaches a LAN endpoint. If only the service gets `No route to host`, reinstall it with the current binary and check System Settings > Privacy & Security > Local Network.
Verify the result with:
```bash
kache doctor
kache daemon status
kache sync --dry-run
```