Files
LocalAI/docs/content/advanced/private-sources.md
T
237cf456fd feat(gallery): galleries published as OCI artifacts (#12167)
* feat(oci): pull ORAS artifacts into a directory

Galleries published to an OCI registry arrive as an ORAS artifact whose
layers carry their tree path in org.opencontainers.image.title. Nothing in
pkg/oci could read a non-image manifest, so add PullArtifact: it lays the
layers out under a destination directory and returns the manifest digest so
callers can pin and verify signatures on it.

A registry is remote input and this writes files, so the manifest is
validated before any byte is fetched. A title that is absolute, escapes the
destination or is missing is refused, an artifact of an unexpected
artifactType is refused, and both the layer count and the total size are
capped with caller-settable limits.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude:claude-opus-5 [Claude Code]

* feat(gallery): fetch and verify galleries published as OCI artifacts

A gallery whose URL is oci://host/repo:tag is now pulled as an ORAS
artifact and its index.yaml read from the unpacked tree, alongside the
existing HTTP and file sources. Mirrors, the per-candidate timeout, the
failure cooldown and the last known good copy work the same for both
schemes, so a gallery can mirror an OCI primary with an HTTP fallback.

When the gallery declares a verification policy the artifact reference is
resolved to its manifest digest, the publisher signature is checked
against that digest, and only then is the same digest pulled. Nothing
unverified is written to disk or parsed. With strict integrity mode on,
an OCI gallery without a verification policy is refused instead of
warned about, which is what the flag already does for backend installs.

The pull lands in a staging directory that is renamed into the cache only
once the whole tree is on disk and the index reads back as an index, so a
failed or interrupted pull leaves nothing a later fetch would serve. A
fetch that finds a fresh cache entry does not contact the registry.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude:claude-opus-5 [Claude Code]

* feat(gallery): resolve entry urls relative to the gallery root

A gallery published as a self-contained tree, which is what an OCI
gallery is, names its base configs by their place in that tree. Such a
url reached the HTTP client verbatim and no entry of the gallery could
be installed.

A scheme-less entry url now resolves against the root the index came
from: the unpacked artifact for an oci:// gallery, and the directory of
the index URL otherwise. The relative path may not climb out of that
root, reusing the guard the artifact puller already applies to layer
titles, and an entry that does is dropped from the listing rather than
failing the whole gallery. An entry that names its own scheme is
untouched, so .ref indirection and local github: resolution behave as
before.

Reading an entry of an OCI gallery also needs the unpacked artifact
directory as its trusted root, since the cache is deliberately a sibling
of the models directory the downloader otherwise confines a file read
to.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude:claude-opus-5 [Claude Code]

* fix(oci): restrict artifact file permissions

Create artifact files with owner-only permissions to satisfy G302.
Document the existing path validation for the G304 scanner finding.
Check the permissions of downloaded layers in the artifact test.

Assisted-by: Codex:gpt-6 gosec

* fix(oci): confine artifact writes to their root

Use root-relative filesystem operations to reject escapes through existing
directory symlinks. Retain owner-only permissions and add a symlink escape
regression test, without suppressing the path traversal finding.

Assisted-by: Codex:gpt-6

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
2026-09-21 09:23:05 +02:00

128 lines
7.9 KiB
Markdown

+++
disableToc = false
title = "Private Registries and Galleries"
weight = 24
url = '/advanced/private-sources'
+++
LocalAI can pull backends, models and gallery indexes from locations that need authentication: private OCI registries (GHCR, Quay, Harbor, Artifactory, ECR), internal HTTP servers, and private GitHub repositories.
You give LocalAI a credentials file. Each entry matches a URL prefix and says how to authenticate. Gallery files never contain secrets, so you can share a gallery and keep the credentials separate.
## Credentials file
Pass the file with `--credentials-file` or `LOCALAI_CREDENTIALS_FILE`. If neither is set, `local-ai run` reads `credentials.yaml` from the data path (`LOCALAI_DATA_PATH`) when that file exists.
```yaml
# Basic auth for a private registry. The password comes from an env var.
- match: ghcr.io/acme
username: bot
password_env: GHCR_TOKEN
# Bearer token for an internal model server, read from a mounted secret.
- match: https://models.acme.internal/
bearer_file: /var/run/secrets/acme/token
# Private GitHub repository used by a gallery (github: URIs).
- match: github.com/acme
bearer_env: GITHUB_TOKEN
# Artifactory-style API key header.
- match: artifactory.acme.internal
header:
name: X-JFrog-Art-Api
value_env: ART_KEY
# Registry on a private network address. Image pulls from it are matched
# as http://, so the entry needs allow_insecure and no https:// scheme.
- match: 192.168.1.5:5000
username: ci
password_file: /run/secrets/registry-password
allow_insecure: true
```
Each entry has:
| Key | Description |
|-----|-------------|
| `match` | URL prefix: a host, optionally followed by a path, optionally with `http://` or `https://`. Required. It must not contain credentials (`user:token@host`), a query string (`?`) or a fragment (`#`). |
| `username` + `password`, `password_env` or `password_file` | Basic authentication. |
| `bearer`, `bearer_env` or `bearer_file` | Bearer token. For registries it is sent as a registry token. GHCR, Docker Hub and Quay do not accept a bearer entry: use basic auth with any `username` and the token as the `password`. |
| `header.name` + `header.value`, `header.value_env` or `header.value_file` | A custom header. HTTP downloads only; registries ignore it. |
| `allow_insecure` | Allow sending this credential over plain `http://`. Default `false`. Registries on local or private addresses need it, see [Registries on the local network](#registries-on-the-local-network). |
Use exactly one authentication type per entry, and exactly one of the plain, `_env` or `_file` forms per secret.
LocalAI checks the file when it starts and stops with an error if the file is not valid. Unknown keys are errors, so a misspelled key such as `pasword_env` does not load silently. For a value of the wrong type, the error gives the line number but does not show the value.
LocalAI reads `_env` and `_file` values each time it needs them. A rotated Kubernetes secret mount is used without a restart. Trailing newlines in secret files are removed.
## How matching works
- Only `https://` URLs get credentials. `http://` URLs get credentials only from an entry with `allow_insecure: true` (see [Registries on the local network](#registries-on-the-local-network)). Other schemes never get credentials.
- The host must be equal. `ghcr.io` does not match `ghcr.io.evil.net`.
- The path matches whole segments. `ghcr.io/acme` matches `ghcr.io/acme/backend` but not `ghcr.io/acme-tools/backend`.
- A URL whose path has `.` or `..` segments never gets credentials.
- If more than one entry matches, the entry with the longest path wins. If two entries are equal, the first one in the file wins.
- If `match` has a scheme, the request must use that scheme. Without a scheme, the entry matches HTTPS, and also plain HTTP when `allow_insecure: true` is set.
- `github.com/<org>` also matches the `raw.githubusercontent.com/<org>/...` URLs that `github:` URIs download from.
- `docker.io` also matches `index.docker.io` and `registry-1.docker.io`.
- LocalAI checks every redirect separately. If a server redirects a download to a CDN that no entry matches, LocalAI sends no credentials to the CDN.
### Registries on the local network
The registry client treats some registry names as local and uses `http` as their scheme:
- a name that starts with `localhost:` (a port is given, for example `localhost:5000`)
- a name that ends in `.localhost`, with or without a port (for example `registry.localhost:5000`)
- a name that contains `127.0.0.1` or `::1`
- an IPv4 address in `10.0.0.0/8`, `172.16.0.0/12` or `192.168.0.0/16`, with or without a port
For these registries, LocalAI always matches image pulls as `http://<registry>/<repository>`, whatever scheme the connection uses in the end. An entry written as `https://192.168.1.5:5000` never applies to image pulls from that registry.
The entry for such a registry needs `allow_insecure: true`, and its `match` must have no scheme or use `http://`. No scheme is recommended, for example `match: 192.168.1.5:5000` with `allow_insecure: true`, because the same entry then also covers downloads that use HTTPS.
## What uses the credentials
- Gallery indexes and mirrors (`galleries`, `backend_galleries`), including `github:` URLs and galleries published as `oci://` artifacts.
- Model files and model configs downloaded over HTTP(S) or `github:`.
- Backend images and `oci://` / `ollama://` models, including resumed layer downloads and cosign signature checks.
For registries, LocalAI checks the credentials file first. If no entry matches, it uses your docker login (`~/.docker/config.json` or `DOCKER_CONFIG`). Hosts that already use `docker login` do not need a credentials file.
For `ollama://` models, only the blob downloads use credentials. LocalAI fetches the model manifest without credentials, so the manifest must be readable anonymously.
A credential passed directly by LocalAI (for example `HF_TOKEN` for managed Hugging Face artifacts) takes precedence over the file.
## Errors
When a server refuses a download with status 401 or 403, the error shows which case applies:
- `authentication required for <target> (status <code>): no credentials rule matches it`: add an entry that matches this URL. For registries the message continues with `and docker config credentials, if any, were not accepted`, because LocalAI also tried your docker login.
- `credential "<match>" was rejected by <target> (status <code>)`: an entry matched, but the server did not accept it. Check the secret and its permissions.
- `the provided credential was rejected by <target> (status <code>)`: the download carried a credential that LocalAI got from somewhere other than the file (for example `HF_TOKEN`), so the file was not used. Check that credential.
Each message ends with the reason the server gave, when there is one, for example a registry's `DENIED` detail. For HTTP downloads only the status text is shown, because the request URL can contain a signed query string.
If an `_env` variable is not set or a `_file` cannot be read, LocalAI logs a warning at startup and keeps the entry, because a secret mount can appear later. A download that matches the entry fails with an error that names the variable or file. LocalAI does not retry that download.
## Kubernetes
Mount the file and the secrets from a Secret:
```yaml
env:
- name: LOCALAI_CREDENTIALS_FILE
value: /etc/localai/credentials.yaml
volumeMounts:
- name: localai-credentials
mountPath: /etc/localai
readOnly: true
```
## Distributed mode
The controller downloads models and sends them to the workers, so model downloads only need credentials on the controller.
Each worker pulls its own backend images. To install backends from a private registry, give every worker the same credentials file. LocalAI does not send credentials over NATS. If a worker has no matching entry and no docker login for that registry, the install fails, and the node's install error says that no credentials rule matches.