Skip to content

Commit 6e1768c

Browse files
committed
feat(backends): --disable-backend-install refuses every backend acquisition
A hardening switch for deployments that must run exactly the backend set they were tested with (production, air-gapped, immutable images, package-managed hosts). With LOCALAI_DISABLE_BACKEND_INSTALL=1 every install and upgrade path refuses — gallery, OCI/URL/path, node-scoped, auto-upgrade, boot-time external backends — HTTP routes answer 403 up front, and a model whose backend is missing is refused before any download: 409 on the API, an error on preload and import. Whether a backend is present is asked of the execution target: the local backend directory on a single node, the workers' listings on a distributed frontend, which installs nothing itself. Backends already present, including system-path ones, keep working. Assisted-by: Claude Code:claude-fable-5 Signed-off-by: Plamen K. Kosseff <p.kosseff@gmail.com>
1 parent 9c0266e commit 6e1768c

57 files changed

Lines changed: 1201 additions & 45 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎core/application/startup.go‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -479,7 +479,7 @@ func New(opts ...config.AppOption) (*Application, error) {
479479
// Start background upgrade checker for backends.
480480
// In distributed mode, uses PostgreSQL advisory lock so only one frontend
481481
// instance runs periodic checks (avoids duplicate upgrades across replicas).
482-
if len(options.BackendGalleries) > 0 {
482+
if len(options.BackendGalleries) > 0 && !options.DisableBackendInstall {
483483
// Pass a lazy getter for the backend manager so the checker always
484484
// uses the active one — DistributedBackendManager is swapped in above
485485
// and asks workers for their installed backends, which is what
@@ -536,14 +536,21 @@ func New(opts ...config.AppOption) (*Application, error) {
536536
modelConfigResync.Start(options.Context, options.Distributed.ModelConfigResyncIntervalOrDefault(), distSvc.Nats)
537537
}
538538

539+
// Preloads run outside the operation loop, so they do not get the
540+
// availability the loop stamps on each op; it comes from the same manager.
541+
preloadOptions := []gallery.InstallOption{gallery.WithArtifactMaterializer(options.ModelArtifactMaterializer)}
542+
if gs := application.GalleryService(); gs != nil {
543+
preloadOptions = append(preloadOptions, gallery.WithBackendAvailability(gs.BackendManager().BackendAvailable))
544+
}
545+
539546
if options.PreloadJSONModels != "" {
540-
if err := galleryop.ApplyGalleryFromString(options.SystemState, application.ModelLoader(), options.EnforcePredownloadScans, options.AutoloadBackendGalleries, options.Galleries, options.BackendGalleries, options.PreloadJSONModels, options.RequireBackendIntegrity, gallery.WithArtifactMaterializer(options.ModelArtifactMaterializer)); err != nil {
547+
if err := galleryop.ApplyGalleryFromString(options.SystemState, application.ModelLoader(), options.EnforcePredownloadScans, options.AutoloadBackendGalleries, options.Galleries, options.BackendGalleries, options.PreloadJSONModels, options.RequireBackendIntegrity, preloadOptions...); err != nil {
541548
return nil, err
542549
}
543550
}
544551

545552
if options.PreloadModelsFromPath != "" {
546-
if err := galleryop.ApplyGalleryFromFile(options.SystemState, application.ModelLoader(), options.EnforcePredownloadScans, options.AutoloadBackendGalleries, options.Galleries, options.BackendGalleries, options.PreloadModelsFromPath, options.RequireBackendIntegrity, gallery.WithArtifactMaterializer(options.ModelArtifactMaterializer)); err != nil {
553+
if err := galleryop.ApplyGalleryFromFile(options.SystemState, application.ModelLoader(), options.EnforcePredownloadScans, options.AutoloadBackendGalleries, options.Galleries, options.BackendGalleries, options.PreloadModelsFromPath, options.RequireBackendIntegrity, preloadOptions...); err != nil {
547554
return nil, err
548555
}
549556
}

‎core/cli/run.go‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ type RunCMD struct {
9191
DisableRuntimeSettings bool `env:"LOCALAI_DISABLE_RUNTIME_SETTINGS,DISABLE_RUNTIME_SETTINGS" default:"false" help:"Disables the runtime settings. When set to true, the server will not load the runtime settings from the runtime_settings.json file" group:"api"`
9292
DisablePredownloadScan bool `env:"LOCALAI_DISABLE_PREDOWNLOAD_SCAN" help:"If true, disables the best-effort security scanner before downloading any files." group:"hardening" default:"false"`
9393
RequireBackendIntegrity bool `env:"LOCALAI_REQUIRE_BACKEND_INTEGRITY,REQUIRE_BACKEND_INTEGRITY" help:"If true, backend installs without a configured signature verification policy (for OCI URIs) or SHA256 (for tarball/HTTP URIs) are rejected. Default is to warn and install. Set this in production once your gallery's verification: block is populated." group:"hardening" default:"false"`
94+
DisableBackendInstall bool `env:"LOCALAI_DISABLE_BACKEND_INSTALL,DISABLE_BACKEND_INSTALL" help:"If true, refuse to install or upgrade backends from galleries, OCI images, URLs or paths, including automatic installs, so the running backend set stays exactly what was deployed and tested; only backends already present or provided via --backends-system-path are used." group:"hardening" default:"false"`
9495
OpaqueErrors bool `env:"LOCALAI_OPAQUE_ERRORS" default:"false" help:"If true, all error responses are replaced with blank 500 errors. This is intended only for hardening against information leaks and is normally not recommended." group:"hardening"`
9596
UseSubtleKeyComparison bool `env:"LOCALAI_SUBTLE_KEY_COMPARISON" default:"false" help:"If true, API Key validation comparisons will be performed using constant-time comparisons rather than simple equality. This trades off performance on each request for resiliancy against timing attacks." group:"hardening"`
9697
DisableApiKeyRequirementForHttpGet bool `env:"LOCALAI_DISABLE_API_KEY_REQUIREMENT_FOR_HTTP_GET" default:"false" help:"If true, a valid API key is not required to issue GET requests to portions of the web ui. This should only be enabled in secure testing environments" group:"hardening"`
@@ -302,6 +303,7 @@ func (r *RunCMD) Run(ctx *cliContext.Context) (result error) {
302303
system.WithBackendDevSuffix(r.BackendDevSuffix),
303304
system.WithPreferDevelopmentBackends(r.PreferDevelopmentBackends),
304305
system.WithRequireBackendIntegrity(r.RequireBackendIntegrity),
306+
system.WithDisableBackendInstall(r.DisableBackendInstall),
305307
)
306308
if err != nil {
307309
return err
@@ -785,6 +787,10 @@ func (r *RunCMD) Run(ctx *cliContext.Context) (result error) {
785787
opts = append(opts, config.WithRequireBackendIntegrity(r.RequireBackendIntegrity))
786788
}
787789

790+
if r.DisableBackendInstall {
791+
opts = append(opts, config.WithDisableBackendInstall(r.DisableBackendInstall))
792+
}
793+
788794
if r.PreferDevelopmentBackends {
789795
opts = append(opts, config.WithPreferDevelopmentBackends(r.PreferDevelopmentBackends))
790796
}

‎core/cli/worker/worker.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ type WorkerFlags struct {
55
BackendGalleries string `env:"LOCALAI_BACKEND_GALLERIES,BACKEND_GALLERIES" help:"JSON list of backend galleries" group:"backends" default:"${backends}"`
66
BackendsSystemPath string `env:"LOCALAI_BACKENDS_SYSTEM_PATH,BACKEND_SYSTEM_PATH" type:"path" default:"/var/lib/local-ai/backends" help:"Path containing system backends used for inferencing" group:"backends"`
77
RequireBackendIntegrity bool `env:"LOCALAI_REQUIRE_BACKEND_INTEGRITY,REQUIRE_BACKEND_INTEGRITY" help:"If true, reject backend installs without a configured signature verification policy (OCI URIs) or SHA256 (tarball/HTTP URIs)." group:"hardening" default:"false"`
8+
DisableBackendInstall bool `env:"LOCALAI_DISABLE_BACKEND_INSTALL,DISABLE_BACKEND_INSTALL" help:"If true, never install the worker's backend from a gallery; use the one already present under the backends or system path, or exit with an error." group:"hardening" default:"false"`
89
ExtraLLamaCPPArgs string `name:"llama-cpp-args" env:"LOCALAI_EXTRA_LLAMA_CPP_ARGS,EXTRA_LLAMA_CPP_ARGS" help:"Extra arguments to pass to llama-cpp-rpc-server"`
910
}
1011

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package worker
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"os"
7+
"path/filepath"
8+
9+
"github.com/mudler/LocalAI/core/gallery"
10+
"github.com/mudler/LocalAI/pkg/system"
11+
. "github.com/onsi/ginkgo/v2"
12+
. "github.com/onsi/gomega"
13+
)
14+
15+
var _ = Describe("findBackendPath with backend installation disabled", func() {
16+
var state *system.SystemState
17+
18+
BeforeEach(func() {
19+
root := GinkgoT().TempDir()
20+
var err error
21+
state, err = system.GetSystemState(
22+
system.WithBackendPath(filepath.Join(root, "backends")),
23+
system.WithBackendSystemPath(filepath.Join(root, "system")),
24+
system.WithDisableBackendInstall(true),
25+
)
26+
Expect(err).NotTo(HaveOccurred())
27+
Expect(os.MkdirAll(state.Backend.BackendsPath, 0o750)).To(Succeed())
28+
Expect(os.MkdirAll(state.Backend.BackendsSystemPath, 0o750)).To(Succeed())
29+
})
30+
31+
It("uses a backend that is already present", func() {
32+
dir := filepath.Join(state.Backend.BackendsSystemPath, "vllm")
33+
Expect(os.MkdirAll(dir, 0o750)).To(Succeed())
34+
meta, _ := json.Marshal(&gallery.BackendMetadata{Name: "vllm"})
35+
Expect(os.WriteFile(filepath.Join(dir, "metadata.json"), meta, 0o644)).To(Succeed())
36+
Expect(os.WriteFile(filepath.Join(dir, "run.sh"), []byte("#!/bin/sh\n"), 0o755)).To(Succeed())
37+
38+
path, err := findBackendPath("vllm", "[]", state, false)
39+
Expect(err).NotTo(HaveOccurred())
40+
Expect(path).To(Equal(dir))
41+
})
42+
43+
It("refuses to install a missing backend", func() {
44+
_, err := findBackendPath("vllm", "[]", state, false)
45+
Expect(errors.Is(err, gallery.ErrBackendInstallDisabled)).To(BeTrue(), err.Error())
46+
})
47+
})

‎core/cli/worker/worker_ds4.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ func (r *DS4Distributed) Run(ctx *cliContext.Context) error {
8181
systemState, err := system.GetSystemState(
8282
system.WithBackendPath(r.BackendsPath),
8383
system.WithBackendSystemPath(r.BackendsSystemPath),
84+
system.WithDisableBackendInstall(r.DisableBackendInstall),
8485
)
8586
if err != nil {
8687
return err

‎core/cli/worker/worker_llamacpp.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ func (r *LLamaCPP) Run(ctx *cliContext.Context) error {
7272
systemState, err := system.GetSystemState(
7373
system.WithBackendPath(r.BackendsPath),
7474
system.WithBackendSystemPath(r.BackendsSystemPath),
75+
system.WithDisableBackendInstall(r.DisableBackendInstall),
7576
)
7677
if err != nil {
7778
return err

‎core/cli/worker/worker_mlx_distributed.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ func (r *MLXDistributed) Run(ctx *cliContext.Context) error {
2323
systemState, err := system.GetSystemState(
2424
system.WithBackendPath(r.BackendsPath),
2525
system.WithBackendSystemPath(r.BackendsSystemPath),
26+
system.WithDisableBackendInstall(r.DisableBackendInstall),
2627
)
2728
if err != nil {
2829
return err

‎core/cli/worker/worker_p2p.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ func (r *P2P) Run(ctx *cliContext.Context) error {
3030
systemState, err := system.GetSystemState(
3131
system.WithBackendPath(r.BackendsPath),
3232
system.WithBackendSystemPath(r.BackendsSystemPath),
33+
system.WithDisableBackendInstall(r.DisableBackendInstall),
3334
)
3435
if err != nil {
3536
return err

‎core/cli/worker/worker_p2p_mlx.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ func (r *P2PMLX) Run(ctx *cliContext.Context) error {
3030
systemState, err := system.GetSystemState(
3131
system.WithBackendPath(r.BackendsPath),
3232
system.WithBackendSystemPath(r.BackendsSystemPath),
33+
system.WithDisableBackendInstall(r.DisableBackendInstall),
3334
)
3435
if err != nil {
3536
return err

‎core/cli/worker/worker_vllm.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ func (r *VLLMDistributed) Run(ctx *cliContext.Context) error {
7272
systemState, err := system.GetSystemState(
7373
system.WithBackendPath(r.BackendsPath),
7474
system.WithBackendSystemPath(r.BackendsSystemPath),
75+
system.WithDisableBackendInstall(r.DisableBackendInstall),
7576
)
7677
if err != nil {
7778
return fmt.Errorf("getting system state: %w", err)

0 commit comments

Comments
 (0)