* feat(credentials): parse and match download credential rules Assisted-by: Claude:claude-opus-5 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(credentials): keep secrets out of parse errors and tighten URL matching Assisted-by: Claude:claude-opus-5 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(credentials): resolve secrets lazily and authenticate HTTP per hop Assisted-by: Claude:claude-opus-5 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(credentials): redact secrets in nested and store formatting Assisted-by: Claude:claude-opus-5 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(credentials): add registry keychain and oras credential adapters Assisted-by: Claude:claude-opus-5 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(credentials): match repository rules for Docker Hub in the oras adapter Assisted-by: Claude:claude-opus-5 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(downloads): authenticate HTTP downloads and gallery reads from the credentials store Assisted-by: Claude:claude-opus-5 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(oci): authenticate registry pulls, resumes, blobs and cosign from the credentials store Assisted-by: Claude:claude-opus-5 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(cli): load download credentials from --credentials-file Assisted-by: Claude:claude-opus-5 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * docs(credentials): correct the local-network registry rules Assisted-by: Claude:claude-opus-5 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(credentials): keep secrets out of match and YAML parse errors A match that fails to parse is no longer quoted in the Parse error, since it may be a URL with a token in it. Userinfo is detected before the scheme check, so ftp://user:token@host is refused as userinfo, and a match with a query string or fragment is refused because it can never apply and a query string is where signed URLs carry their token. Every YAML decode error is now redacted, not only type errors: quoted scalars such as a secret under a mismatched !!int tag are replaced and unquoted map keys are cut off. Assisted-by: Claude:claude-opus-5 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(downloads): make auth errors name the real cause and never retry unresolved secrets AuthError now appends its cause, so a registry's DENIED or UNAUTHORIZED detail reaches the operator. HTTP auth errors print only the status text in place of the cause, because the downloader builds that cause from the requested URL, which can carry a signed query string. Registry pulls say that docker config credentials were tried too, and a download that carried a caller-provided credential (WithBearerToken, or an explicit authorization on gallery reads) reports that credential as rejected instead of blaming the store. The Range probe for a leftover partial file now returns an unresolved secret as a permanent error, like the download request already did. Assisted-by: Claude:claude-opus-5 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(credentials): keep oras pulls anonymous on a broken docker helper and close bodies When docker config names a credsStore helper that cannot run, the oras credential func now logs at debug and returns no credential, so public pulls keep working as they did before the adapter existed. The transport closes the request body when a rule's secret cannot be resolved, as the RoundTripper contract requires. The redirect spec now uses a custom header rule on the origin, which net/http would not strip on its own, to prove the transport does not carry credentials to the next hop. Assisted-by: Claude:claude-opus-5 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * test(oci): cover FetchImageBlob authentication against a private registry FetchImageBlob now has a spec that pulls a layer blob by digest from a basic-auth registry through the oras credential adapter, and one that shows the same fetch fails when no rule matches. oras only speaks HTTPS here, so the registry serves TLS and the spec points http.DefaultTransport, which retry.DefaultClient falls back to, at the test server's client for its duration instead of adding a transport seam to production code. Assisted-by: Claude:claude-opus-5 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * docs(credentials): document auth error wording, ollama manifests and registry tokens The errors section now lists the registry and provided-credential messages and says the server's reason is appended. ollama:// manifests are fetched without credentials, so only blob downloads use the file. GHCR, Docker Hub and Quay need basic auth with the token as password, and match rules must not carry a query string or fragment. The backend gallery docs and the container troubleshooting section now point to the private sources page. Assisted-by: Claude:claude-opus-5 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(credentials): document trusted file path The credentials path comes from operator configuration. Mark the file read with a scoped G304 explanation to resolve the gosec false positive. Assisted-by: Codex:gpt-6 gosec --------- 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>
7.8 KiB
+++ 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.
# 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. |
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 withallow_insecure: true(see Registries on the local network). Other schemes never get credentials. - The host must be equal.
ghcr.iodoes not matchghcr.io.evil.net. - The path matches whole segments.
ghcr.io/acmematchesghcr.io/acme/backendbut notghcr.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
matchhas a scheme, the request must use that scheme. Without a scheme, the entry matches HTTPS, and also plain HTTP whenallow_insecure: trueis set. github.com/<org>also matches theraw.githubusercontent.com/<org>/...URLs thatgithub:URIs download from.docker.ioalso matchesindex.docker.ioandregistry-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 examplelocalhost:5000) - a name that ends in
.localhost, with or without a port (for exampleregistry.localhost:5000) - a name that contains
127.0.0.1or::1 - an IPv4 address in
10.0.0.0/8,172.16.0.0/12or192.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), includinggithub:URLs. - 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 withand 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 exampleHF_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:
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.