← Back to DevBytes

Securing Model Weights in CI/CD Pipelines

Securing Model Weights in CI/CD Pipelines

Machine learning model weights are valuable intellectual property that often represent months of training, expensive compute resources, and proprietary data. When these artifacts flow through CI/CD pipelines for deployment, they become prime targets for tampering, exfiltration, and supply chain attacks. This tutorial walks through practical strategies for securing model weights throughout the pipeline lifecycle.

What Is Model Weight Security in CI/CD?

Securing model weights in CI/CD pipelines means applying cryptographic, access control, and verification measures to ensure that model artifacts remain confidential, integrity-protected, and traceable from the moment they are produced during training to the point they are deployed into production. This involves signing weights, encrypting them at rest and in transit, restricting pipeline access, and verifying provenance before deployment.

Unlike traditional software artifacts, model weights are opaque binary blobs. You cannot easily inspect them to detect malicious modifications, which makes cryptographic verification essential. A compromised weight file can introduce backdoors, bias, or degraded performance that is extremely difficult to detect through behavioral testing alone.

Why It Matters

How to Secure Model Weights in Your Pipeline

1. Sign Model Weights After Training

The first step is to cryptographically sign model weights immediately after training completes. This signature is later verified before deployment to detect any tampering. Use a tool like cosign from the Sigstore project, which supports keyless signing using OIDC identities.

# Sign model weights using cosign with keyless mode
# This runs in the training job after weights are saved

import subprocess
import os

def sign_model_weights(weights_path, registry_ref):
    """
    Sign model weights and upload signature to OCI registry.
    """
    # Upload weights to OCI registry as artifact
    upload_cmd = [
        "oras", "push", registry_ref,
        f"{weights_path}:application/octet-stream"
    ]
    subprocess.run(upload_cmd, check=True)

    # Sign the artifact using keyless cosign
    sign_cmd = [
        "cosign", "sign", "--yes",
        "--identity-token", os.environ["OIDC_TOKEN"],
        registry_ref
    ]
    subprocess.run(sign_cmd, check=True)
    print(f"Successfully signed {registry_ref}")

# Example usage in training pipeline
sign_model_weights(
    weights_path="./models/classifier_v1.pt",
    registry_ref="ghcr.io/myorg/models/classifier:v1"
)

2. Encrypt Weights at Rest in Storage

When storing weights in artifact repositories or cloud storage, encrypt them using envelope encryption. The following example uses AWS KMS to encrypt a symmetric data key, then uses that key to encrypt the model file.

import boto3
from cryptography.fernet import Fernet
import base64

def encrypt_model_weights(model_path, kms_key_id, s3_bucket, s3_key):
    """
    Encrypt model weights using envelope encryption before uploading to S3.
    """
    kms = boto3.client('kms')
    s3 = boto3.client('s3')

    # Generate a data key from KMS
    response = kms.generate_data_key(KeyId=kms_key_id, KeySpec='AES_256')
    plaintext_key = response['Plaintext']
    encrypted_key = response['CiphertextBlob']

    # Encrypt the model file with the data key
    fernet = Fernet(base64.urlsafe_b64encode(plaintext_key))

    with open(model_path, 'rb') as f:
        model_data = f.read()

    encrypted_data = fernet.encrypt(model_data)

    # Upload encrypted model and wrapped key
    s3.put_object(
        Bucket=s3_bucket,
        Key=f"{s3_key}.enc",
        Body=encrypted_data
    )
    s3.put_object(
        Bucket=s3_bucket,
        Key=f"{s3_key}.key",
        Body=encrypted_key
    )
    print(f"Encrypted and uploaded {model_path} to s3://{s3_bucket}/{s3_key}")

encrypt_model_weights(
    model_path="./models/classifier_v1.pt",
    kms_key_id="arn:aws:kms:us-east-1:123456789012:key/abc123",
    s3_bucket="myorg-secure-models",
    s3_key="classifier/v1"
)

3. Verify Signatures Before Deployment

In the deployment stage of your pipeline, verify the model signature before allowing the weights to be loaded. This step should fail the pipeline if verification fails.

# GitHub Actions deployment step for model verification
name: Deploy Model

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
      - name: Install cosign
        uses: sigstore/cosign-installer@v3

      - name: Verify model signature
        env:
          MODEL_REF: ghcr.io/myorg/models/classifier:v1
          ISSUER: https://token.actions.githubusercontent.com
          IDENTITY: myorg/.github/workflows/train.yml@refs/heads/main
        run: |
          cosign verify "$MODEL_REF" \
            --certificate-identity "$IDENTITY" \
            --certificate-oidc-issuer "$ISSUER"
          echo "Model signature verified successfully"

      - name: Download and decrypt weights
        run: |
          python scripts/download_and_decrypt.py \
            --model-ref "$MODEL_REF" \
            --output ./models/classifier.pt

      - name: Deploy to inference server
        run: |
          kubectl apply -f k8s/deployment.yml

4. Implement Access Controls on Pipeline Secrets

Restrict which pipeline jobs and users can access decryption keys and registry credentials. Use short-lived credentials and workload identity federation instead of long-lived tokens.

# Python script to download and decrypt model weights
# Uses workload identity for cloud auth - no static credentials

import boto3
from cryptography.fernet import Fernet
import base64
import subprocess
import sys

def download_and_decrypt(model_ref, output_path, kms_key_id, s3_bucket, s3_key):
    # Assume IAM role via workload identity (no static creds)
    s3 = boto3.client('s3')
    kms = boto3.client('kms')

    # Download encrypted artifacts
    enc_data = s3.get_object(Bucket=s3_bucket, Key=f"{s3_key}.enc")['Body'].read()
    enc_key = s3.get_object(Bucket=s3_bucket, Key=f"{s3_key}.key")['Body'].read()

    # Decrypt the data key using KMS
    response = kms.decrypt(CiphertextBlob=enc_key)
    plaintext_key = response['Plaintext']

    # Decrypt the model
    fernet = Fernet(base64.urlsafe_b64encode(plaintext_key))
    model_data = fernet.decrypt(enc_data)

    with open(output_path, 'wb') as f:
        f.write(model_data)

    # Verify integrity by checking file hash
    result = subprocess.run(
        ["sha256sum", output_path], capture_output=True, text=True
    )
    print(f"Model decrypted. SHA256: {result.stdout.split()[0]}")

if __name__ == "__main__":
    download_and_decrypt(
        model_ref=sys.argv[1],
        output_path="./models/classifier.pt",
        kms_key_id="arn:aws:kms:us-east-1:123456789012:key/abc123",
        s3_bucket="myorg-secure-models",
        s3_key="classifier/v1"
    )

5. Generate and Verify SBOMs for Model Artifacts

A Software Bill of Materials for models should include the training dataset hash, framework versions, hyperparameters, and weight checksum. This metadata travels with the weights and is verified at deployment.

import hashlib
import json
from datetime import datetime

def generate_model_sbom(weights_path, model_metadata):
    """
    Generate an SBOM for a model artifact including weight checksums.
    """
    sha256 = hashlib.sha256()
    with open(weights_path, 'rb') as f:
        for chunk in iter(lambda: f.read(8192), b''):
            sha256.update(chunk)

    sbom = {
        "schema_version": "1.0",
        "generated_at": datetime.utcnow().isoformat(),
        "model": {
            "name": model_metadata["name"],
            "version": model_metadata["version"],
            "framework": model_metadata["framework"],
            "framework_version": model_metadata["framework_version"],
        },
        "training": {
            "dataset_hash": model_metadata["dataset_hash"],
            "hyperparameters": model_metadata["hyperparameters"],
            "training_job_id": model_metadata["training_job_id"],
        },
        "integrity": {
            "weights_sha256": sha256.hexdigest(),
            "weights_size_bytes": os.path.getsize(weights_path),
        }
    }
    return sbom

# Write SBOM alongside weights
sbom = generate_model_sbom(
    weights_path="./models/classifier_v1.pt",
    model_metadata={
        "name": "classifier",
        "version": "v1",
        "framework": "pytorch",
        "framework_version": "2.1.0",
        "dataset_hash": "sha256:abc123...",
        "hyperparameters": {"lr": 0.001, "epochs": 50},
        "training_job_id": "job-98765"
    }
)

with open("./models/classifier_v1.sbom.json", 'w') as f:
    json.dump(sbom, f, indent=2)

Best Practices

Conclusion

Securing model weights in CI/CD pipelines requires a defense-in-depth approach that combines cryptographic signing, encryption, strict access controls, and continuous verification. By signing weights at training time, encrypting them in storage, verifying signatures before every deployment, and maintaining detailed provenance metadata, you create a chain of trust that makes tampering detectable and exfiltration significantly harder. As ML systems become more central to business operations, treating model weights with the same security rigor as production source code is not optional — it is a fundamental requirement for responsible AI deployment.

— Ad —

Google AdSense will appear here after approval

← Back to all articles