+1 (415) 366-1364

Securing the TensorFlow Model Supply Chain: Safe Deserialization, Scanning, and Sigstore Signing

Every TensorFlow team now downloads model weights the way it downloads packages: a from_preset() call, a Hugging Face pull, a checkpoint a partner emailed over. That is a supply chain, and in 2024-2025 it became an audited one. Enterprise security reviews increasingly ask the same three questions before a model goes to production: where did this artifact come from, has it been modified, and can it execute code when we load it? This tutorial shows how to answer all three on the TensorFlow and Keras stack - serialization formats and their actual risk, safe_mode and custom objects, scanning untrusted artifacts, signing and verifying models with Sigstore, and wiring verification into CI and into your serving containers.

Why model artifacts are different from ordinary data

A .pkl, a legacy .h5 with a Lambda layer, or a SavedModel with custom ops are not inert data files. They contain instructions that the loader will happily run in your training cluster or your serving pod - often as root, often with cloud credentials attached. The well-known attack classes are:

  • Deserialization execution. Python pickle payloads embedded in checkpoints; arbitrary Python inside a serialized Lambda layer.
  • Artifact substitution. A mirror, a stale cache, or a compromised bucket hands you weights that are not the ones you evaluated. Nothing in a raw .keras file tells you who built it.
  • Backdoored weights. Weights that behave normally on your eval set and misbehave on a trigger input. Scanning will not catch this; provenance and evaluation will.
  • Dependency drift. The model loads fine, but pulls a custom-op wheel or a preset-fetching library you never reviewed.

The mitigations differ per class, so treat them separately rather than buying one tool and declaring the problem solved.

Step 1: choose a serialization format on purpose

FormatLoads code?Use it for
.keras (Keras 3 zip archive)Config JSON + weights; no arbitrary code while safe_mode=TrueDefault for Keras 3 training artifacts
.weights.h5Weights onlySafest exchange when both sides share model code
safetensorsTensors only, no executionCross-framework weight exchange
TF SavedModelSerialized graph, plus custom ops if presentServing via TF Serving / Vertex
Legacy .h5 full model, picklesYes, via Lambda / pickleAvoid for anything untrusted

In Keras 3, saving the whole model is the archive format:

model.save("churn_v7.keras")                 # config + weights, no pickle
model.save_weights("churn_v7.weights.h5")    # weights only

Step 2: load untrusted models in safe mode

Keras 3 defends deserialization with safe_mode, which is on by default and refuses to reconstruct Lambda layers (the classic arbitrary-code vector):

import keras

# Default. A Lambda layer in the archive raises instead of executing.
model = keras.saving.load_model("vendor_model.keras")

# Only for artifacts you built yourself and can re-derive:
model = keras.saving.load_model("ours.keras", safe_mode=False)

Two rules we enforce on client projects:

  1. safe_mode=False never appears in code that loads a third-party artifact. If loading fails in safe mode, that is the security control firing, not a bug to work around.
  2. Replace Lambda layers with real layers. A three-line subclass is reviewable, diffable, and safe to deserialize:
@keras.saving.register_keras_serializable(package="deepadvisors")
class ScaleByConstant(keras.layers.Layer):
    def __init__(self, scale=1.0, **kwargs):
        super().__init__(**kwargs)
        self.scale = scale

    def call(self, x):
        return x * self.scale

    def get_config(self):
        return {**super().get_config(), "scale": self.scale}

Custom objects still execute your code path, so the registered class must come from a package you pin and review - the archive only names it, it does not carry it.

Step 3: scan artifacts before they touch a GPU

Scanning is cheap and catches the boring, common cases. Run it at the ingestion boundary - the job that copies a vendor artifact into your internal registry - not at load time in production.

pip install modelscan
modelscan -p ./incoming/vendor_model.keras -r json -o scan.json

A minimal ingestion gate:

import json, subprocess, sys, zipfile, pathlib

def scan(path: str) -> None:
    out = subprocess.run(["modelscan", "-p", path, "-r", "json", "-o", "/tmp/scan.json"],
                         capture_output=True, text=True)
    report = json.loads(pathlib.Path("/tmp/scan.json").read_text())
    issues = report.get("issues", [])
    if issues:
        sys.exit(f"{len(issues)} unsafe operator(s) in {path}: {issues[:3]}")

def inspect_keras_archive(path: str) -> dict:
    """A .keras file is a zip - read config.json without constructing anything."""
    with zipfile.ZipFile(path) as z:
        return json.loads(z.read("config.json"))

scan("incoming/vendor_model.keras")
cfg = inspect_keras_archive("incoming/vendor_model.keras")
layers = {l["class_name"] for l in cfg["config"]["layers"]}
assert "Lambda" not in layers, "Lambda layer present - reject"
print(sorted(layers))

Reading config.json straight out of the zip is the trick worth remembering: you can audit an unknown Keras artifact's full layer list, shapes, and custom-object names without ever calling load_model.

For SavedModel directories, inspect the signatures and look for unexpected custom ops before serving:

saved_model_cli show --dir ./exported/1 --all

Step 4: sign models so you know what you deployed

Scanning answers "is this file dangerous?". Signing answers "is this the artifact our pipeline produced?" - the question that matters for the substitution and backdoor cases. Google's model_signing library (Sigstore-backed, part of the OpenSSF AI work) signs the whole artifact, directory included, and produces a verifiable signature bundle:

pip install model-signing

# In CI, keyless via workload identity (OIDC):
model_signing sign ./exported/churn_v7.keras 
  --signature churn_v7.sig

# At ingestion or at deploy time:
model_signing verify ./exported/churn_v7.keras 
  --signature churn_v7.sig 
  --identity "https://github.com/acme/ml-pipelines/.github/workflows/train.yml@refs/heads/main" 
  --identity_provider "https://token.actions.githubusercontent.com"

The identity flags are the whole point: verification is not just "a valid signature exists", it is "this artifact was signed by our training workflow on main". A signature with no asserted identity buys you almost nothing.

If you cannot adopt Sigstore yet, a hash manifest plus your existing artifact registry's immutability and a recorded digest in the model card is a reasonable interim control. Record the digest next to the eval metrics - see our explainability and model cards tutorial - so that "the model we measured" and "the model we shipped" are provably the same bytes.

Step 5: enforce it in the pipeline, not in a wiki

Make the gate a pipeline step. In a TFX-style flow (see our continuous training tutorial), the natural place is between Trainer/Evaluator and Pusher: sign after blessing, verify before push. In plain CI:

# .github/workflows/release-model.yml (fragment)
permissions:
  id-token: write      # required for keyless Sigstore signing
  contents: read
jobs:
  release:
    steps:
      - run: modelscan -p artifacts/churn_v7.keras -r json -o scan.json
      - run: python ci/fail_on_scan_issues.py scan.json
      - run: python ci/smoke_eval.py artifacts/churn_v7.keras   # golden tests
      - run: model_signing sign artifacts/churn_v7.keras --signature churn_v7.sig
      - run: gcloud storage cp artifacts/churn_v7.keras churn_v7.sig gs://acme-models/churn/v7/

Then verify in the serving image's entrypoint, before the server binds a port:

# entrypoint.py
import subprocess, sys

def verify_or_die(artifact: str, sig: str, identity: str, issuer: str) -> None:
    r = subprocess.run(["model_signing", "verify", artifact, "--signature", sig,
                        "--identity", identity, "--identity_provider", issuer])
    if r.returncode != 0:
        sys.exit("model signature verification failed - refusing to serve")

Fail closed. A serving pod that starts with an unverified model is the exact outcome the control exists to prevent. The deployment mechanics around this are in our TensorFlow Serving tutorial.

Step 6: the rest of the checklist

  • Pin and hash your presets. from_preset("gemma2_2b_en") reaches the network. Mirror presets into an internal bucket, pin the revision, and verify digests, the same way you vendor pip wheels. Our Gemma fine-tuning tutorial assumes internet access; production pipelines should not have it.
  • Record licence and origin. Open-weight models carry usage terms. Capture the preset name, source URL, licence, and digest in the model card at training time - reconstructing it during an audit is painful.
  • Separate credentials. The job that loads a third-party artifact should not hold your data-warehouse or deployment credentials.
  • Keep the backdoor question honest. Signing proves origin, not benignity. For high-stakes models built on donated or third-party weights, budget for a held-out eval your vendor never saw, plus trigger-style robustness probes.
  • Log the digest at inference. Every prediction log line should carry the model digest, so an incident can be scoped to exact artifacts rather than "sometime last quarter".

A workable maturity ladder

You do not need all of this in week one. In practice we phase it:

  1. Ban safe_mode=False and Lambda layers in reviewable code; standardise on .keras / safetensors.
  2. Add a scan-and-inspect gate at the ingestion boundary for every external artifact.
  3. Sign in CI, verify at deploy, fail closed.
  4. Record digests, licences, and provenance in model cards and inference logs.
  5. Add adversarial/held-out evaluation for third-party weights in high-stakes paths.

Steps 1 and 2 are an afternoon. Steps 3 and 4 are a sprint, and they are the ones that satisfy security reviews and emerging AI governance requirements.


Need help hardening your ML supply chain? DeepAdvisors' senior TensorFlow consultants implement signing, verification, and provenance for training and serving pipelines without slowing your teams down. Get in touch to talk through your setup.