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
- Intellectual property protection: Model weights may cost tens or hundreds of thousands of dollars in compute to produce. Leaking them through an exposed pipeline is a direct financial loss.
- Supply chain integrity: Attackers who can modify weights mid-pipeline can inject backdoors that activate on specific inputs, undermining model reliability.
- Compliance requirements: Regulations like GDPR, HIPAA, and emerging AI governance frameworks increasingly require demonstrable provenance and integrity controls for deployed models.
- Reproducibility: Signed, versioned weights ensure that what you tested in staging is exactly what runs in production.
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
- Sign early, verify often: Sign weights immediately after training and verify at every stage transition in the pipeline. Never skip verification for "trusted" internal stages.
- Use keyless signing where possible: Tools like cosign with OIDC federation eliminate the need to manage signing keys, reducing the attack surface.
- Isolate training and deployment environments: Training jobs should not have deployment permissions, and deployment jobs should not have access to training data or training infrastructure.
- Rotate encryption keys regularly: Set up automatic KMS key rotation and re-encrypt stored model artifacts on a defined schedule.
- Audit pipeline access: Log every access to model artifacts, including who downloaded, decrypted, or verified weights. Send these logs to a tamper-proof store.
- Pin dependencies in training: A compromised dependency in the training environment can produce backdoored weights. Pin and verify all training dependencies.
- Use immutable tags: Once a model version is published to a registry, make the tag immutable. Never allow overwriting of existing version tags.
- Implement break-glass procedures: Define and test an emergency procedure for revoking compromised model versions and rolling back to a known-good signed version.
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.