Skip to main content

Use server-side encryption with customer keys (SSE-C)

How to implement SSE-C with CoreWeave AI Object Storage

This guide demonstrates how to implement server-side encryption with customer keys (SSE-C) with CoreWeave AI Object Storage. SSE-C allows you to provide your own encryption keys for objects stored in Object Storage, giving you complete control over data encryption.

Prerequisites

Before implementing SSE-C, ensure you have:

  • Access to CoreWeave AI Object Storage with appropriate permissions
  • An S3-compatible client or library that supports SSE-C
  • A method to generate and store encryption keys securely
  • Understanding of basic S3 operations (upload, download, copy)
Danger

If you lose your encryption key, you cannot recover your encrypted data. CoreWeave does not store your encryption keys, and cannot decrypt your data without them. See the Key management section for best practices on storing and managing your encryption keys securely.

Understanding key verification

CoreWeave AI Object Storage uses your provided key to encrypt or decrypt your data as required, but does not permanently store that key itself. Instead, CoreWeave stores only a base64-encoded MD5 digest of your encryption key.

This hash is stored solely for verification, meaning, when you later access the object, you must supply the same key and hash you used originally. CoreWeave checks the hash of the supplied key against the stored hash to verify that the correct key is provided before attempting decryption.

Why only the hash?

The hash of the key serves as a checksum: it can verify that the key is correct, but it cannot be used to reconstruct the actual key. By storing only the hash and not the key itself, CoreWeave ensures that only someone who possesses the original encryption key can access the corresponding encrypted data. If you lose your key, neither you nor CoreWeave can recover your data. The hash makes it computationally infeasible to recover the original key due to the one-way nature of cryptographic hashes.

Basic operations

In these examples, $BASE64_KEY represents your base64-encoded encryption key. Replace this with your actual generated key in all examples.

Generate encryption keys

SSE-C requires 256-bit (32-byte) encryption keys. Generate these keys using cryptographically secure methods and encode them in base64 format:

Example
# Generate a random 256-bit key and encode in base64
openssl rand -base64 32

Upload objects with SSE-C

When uploading objects with SSE-C, include the encryption key in your request headers:

Example
# Upload an object with SSE-C
aws s3 cp myfile.txt s3://my-bucket/ --sse-customer-algorithm AES256 \
--sse-customer-key $BASE64_KEY \
--sse-customer-key-md5 $(echo -n "$BASE64_KEY" | base64 --decode | openssl dgst -md5 -binary | base64)

Download objects with SSE-C

When downloading objects that were encrypted with SSE-C, provide the same encryption key:

Example
# Download an object with SSE-C
aws s3 cp s3://my-bucket/myfile.txt ./downloaded-file.txt \
--sse-customer-algorithm AES256 \
--sse-customer-key $BASE64_KEY \
--sse-customer-key-md5 $(echo -n "$BASE64_KEY" | base64 -d | openssl dgst -md5 -binary | base64)

Copy objects with SSE-C

When copying objects that use SSE-C, specify the encryption parameters for both source and destination:

Example
# Copy an object with SSE-C
aws s3 cp s3://my-bucket/source-file.txt s3://my-bucket/dest-file.txt \
--sse-customer-algorithm AES256 \
--sse-customer-key $BASE64_KEY \
--sse-customer-key-md5 $(echo -n "$BASE64_KEY" | base64 --decode | openssl dgst -md5 -binary | base64) \
--copy-source-sse-customer-algorithm AES256 \
--copy-source-sse-customer-key $BASE64_KEY \
--copy-source-sse-customer-key-md5 $(echo -n "$BASE64_KEY" | base64 --decode | openssl dgst -md5 -binary | base64)

Verify encryption

Verify that your objects are encrypted by checking the response headers or object metadata:

Check upload response

Example
# Upload and check response
response = s3_client.upload_file(
'myfile.txt',
'my-bucket',
'myfile.txt',
ExtraArgs={
'SSECustomerAlgorithm': 'AES256',
'SSECustomerKey': encryption_key,
'SSECustomerKeyMD5': key_md5_b64
}
)
# The response will include encryption headers
print(f"Encryption algorithm: {response['ResponseMetadata']['HTTPHeaders']['x-amz-server-side-encryption-customer-algorithm']}")

List objects with encryption info

Example
# List objects and check encryption
response = s3_client.list_objects_v2(
Bucket='my-bucket',
Prefix='myfile.txt'
)
for obj in response['Contents']:
print(f"Object: {obj['Key']}")
# Check if object uses SSE-C
if 'SSECustomerAlgorithm' in obj:
print(f"Encryption: {obj['SSECustomerAlgorithm']}")

Error handling

When using SSE-C, you may encounter specific errors:

ErrorMeaning
InvalidArgumentThe encryption key is not 256 bits (32 bytes)
InvalidRequestThe encryption key MD5 hash doesn't match
AccessDeniedThe encryption key provided doesn't match the one used for upload
NoSuchKeyThe object doesn't exist or the encryption key is incorrect

It is strongly recommended to implement proper error handling in your applications. For example:

Example
try:
s3_client.download_file(
'my-bucket',
'myfile.txt',
'downloaded-file.txt',
ExtraArgs={
'SSECustomerAlgorithm': 'AES256',
'SSECustomerKey': encryption_key,
'SSECustomerKeyMD5': key_md5_b64
}
)
except s3_client.exceptions.ClientError as e:
error_code = e.response['Error']['Code']
if error_code == 'InvalidArgument':
print("Invalid encryption key format")
elif error_code == 'AccessDenied':
print("Incorrect encryption key")
else:
print(f"Error: {error_code}")

Key management

Store keys securely

Never store encryption keys in plain text or in your application code. Use secure key management solutions:

  • Environment variables: Store keys in environment variables (not in code)
  • Secret management systems: Use tools like HashiCorp Vault, AWS Secrets Manager, or Kubernetes Secrets
  • Hardware Security Modules (HSMs): For high-security requirements, use HSMs to generate and store keys

Centralize key management

Create a centralized key management system in your application:

Example
import hashlib
import base64
class SSEKeyManager:
def __init__(self, key_id):
self.key_id = key_id
self.key = self.load_key(key_id)
def get_encryption_headers(self):
"""Generate SSE-C headers for S3 requests"""
key_md5 = hashlib.md5(base64.b64decode(self.key)).digest()
key_md5_b64 = base64.b64encode(key_md5).decode('utf-8')
return {
'SSECustomerAlgorithm': 'AES256',
'SSECustomerKey': self.key,
'SSECustomerKeyMD5': key_md5_b64
}
def load_key(self, key_id):
"""Load key from secure storage"""
# Implement secure key loading logic
pass

Validate encryption keys

Always validate encryption keys before use:

Example
def validate_encryption_key(key):
"""Validate that the encryption key meets requirements"""
if not isinstance(key, str):
raise ValueError("Encryption key must be a string")
try:
# Decode base64 to check if it's valid
key_bytes = base64.b64decode(key)
if len(key_bytes) != 32: # 32 bytes = 256 bits
raise ValueError("Encryption key must be 32 bytes (256 bits)")
except Exception:
raise ValueError("Encryption key must be valid base64-encoded 32-byte key")
return True

Advanced topics

SSE-C with presigned URLs

When using SSE-C with presigned URLs, you must include the encryption parameters when generating the URL and when accessing it. The encryption key is required both during URL generation and when the URL is used.

Generate presigned URLs with SSE-C

Example
import boto3
import hashlib
import base64
# Configure your S3 client
s3_client = boto3.client(
's3',
endpoint_url='https://object-storage.coreweave.com',
aws_access_key_id='your-access-key',
aws_secret_access_key='your-secret-key'
)
# Your encryption key (base64-encoded)
encryption_key = '$BASE64_KEY'
# Calculate MD5 hash of the key
key_md5 = hashlib.md5(base64.b64decode(encryption_key)).digest()
key_md5_b64 = base64.b64encode(key_md5).decode('utf-8')
# Generate presigned URL for download with SSE-C
presigned_url = s3_client.generate_presigned_url(
'get_object',
Params={
'Bucket': 'my-bucket',
'Key': 'myfile.txt',
'SSECustomerAlgorithm': 'AES256',
'SSECustomerKey': encryption_key,
'SSECustomerKeyMD5': key_md5_b64
},
ExpiresIn=3600 # URL expires in 1 hour
)
print(f"Presigned URL: {presigned_url}")

Use presigned URLs with SSE-C

When accessing a presigned URL that was generated with SSE-C, you must include the same encryption parameters:

Example
# Download using presigned URL with SSE-C
curl -H "x-amz-server-side-encryption-customer-algorithm: AES256" \
-H "x-amz-server-side-encryption-customer-key: $BASE64_KEY" \
-H "x-amz-server-side-encryption-customer-key-md5: $(echo -n "$BASE64_KEY" | base64 -d | openssl dgst -md5 -binary | base64)" \
"https://object-storage.coreweave.com/my-bucket/myfile.txt?X-Amz-Algorithm=...&X-Amz-Credential=...&X-Amz-Date=...&X-Amz-Expires=...&X-Amz-SignedHeaders=...&X-Amz-Signature=..."

SSE-C with bucket policies

Bucket policies can be configured to enforce SSE-C usage for specific operations. This ensures that objects are always encrypted with customer-provided keys.

Enforce SSE-C for uploads

Create a bucket policy that requires SSE-C for all upload operations:

Example
# Save the policy to a file
cat > ssec-policy.json << 'EOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "EnforceSSECForUploads",
"Effect": "Deny",
"Principal": "*",
"Action": [
"s3:PutObject"
],
"Resource": "arn:aws:s3:::my-bucket/*",
"Condition": {
"StringNotEquals": {
"s3:x-amz-server-side-encryption-customer-algorithm": "AES256"
}
}
},
{
"Sid": "EnforceSSECKeyForUploads",
"Effect": "Deny",
"Principal": "*",
"Action": [
"s3:PutObject"
],
"Resource": "arn:aws:s3:::my-bucket/*",
"Condition": {
"Null": {
"s3:x-amz-server-side-encryption-customer-key": "true"
}
}
}
]
}
EOF
# Apply the policy
aws s3api put-bucket-policy --bucket my-bucket --policy file://ssec-policy.json

Enforce SSE-C for specific prefixes

You can also enforce SSE-C only for specific object prefixes. For example, the following policy denies uploads to any object under the sensitive/ prefix unless the request uses SSE-C with the AES256 algorithm. This ensures that only objects stored under my-bucket/sensitive/ require customer-provided encryption keys, while other objects in the bucket are not affected by this policy.

Example
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "EnforceSSECForSensitiveData",
"Effect": "Deny",
"Principal": "*",
"Action": [
"s3:PutObject"
],
"Resource": "arn:aws:s3:::my-bucket/sensitive/*",
"Condition": {
"StringNotEquals": {
"s3:x-amz-server-side-encryption-customer-algorithm": "AES256"
}
}
}
]
}

Allow specific encryption keys

For enhanced security, you can restrict uploads to specific encryption keys by checking the key hash:

Example
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowSpecificEncryptionKey",
"Effect": "Allow",
"Principal": "*",
"Action": [
"s3:PutObject"
],
"Resource": "arn:aws:s3:::my-bucket/*",
"Condition": {
"StringEquals": {
"s3:x-amz-server-side-encryption-customer-algorithm": "AES256",
"s3:x-amz-server-side-encryption-customer-key-md5": "your-expected-key-md5-hash"
}
}
}
]
}

Key rotation

Regularly rotate your encryption keys to maintain security:

Example
import datetime
import secrets
import base64
class KeyManager:
def __init__(self):
self.current_key = self.load_current_key()
self.key_expiry = datetime.datetime.now() + datetime.timedelta(days=90)
def should_rotate_key(self):
return datetime.datetime.now() >= self.key_expiry
def rotate_key(self):
# Generate new key
new_key_bytes = secrets.token_bytes(32)
new_key = base64.b64encode(new_key_bytes).decode('utf-8')
# Re-encrypt data with new key (if needed)
self.re_encrypt_data(new_key)
# Update current key
self.current_key = new_key
self.key_expiry = datetime.datetime.now() + datetime.timedelta(days=90)

Performance optimization

Cache encryption headers to avoid recalculating them for every request:

Example
import time
class CachedSSEHeaders:
def __init__(self, key_manager):
self.key_manager = key_manager
self._cached_headers = None
self._cache_timestamp = None
self._cache_duration = 3600 # 1 hour
def get_headers(self):
now = time.time()
if (self._cached_headers is None or
self._cache_timestamp is None or
now - self._cache_timestamp > self._cache_duration):
self._cached_headers = self.key_manager.get_encryption_headers()
self._cache_timestamp = now
return self._cached_headers

Audit logging

Maintain comprehensive audit logs for key operations:

Example
import logging
import time
logger = logging.getLogger(__name__)
def upload_with_monitoring(s3_client, bucket, key, file_path, key_manager):
start_time = time.time()
try:
headers = key_manager.get_encryption_headers()
s3_client.upload_file(file_path, bucket, key, ExtraArgs=headers)
duration = time.time() - start_time
logger.info(f"Successfully uploaded {key} with SSE-C in {duration:.2f}s")
except Exception as e:
duration = time.time() - start_time
logger.error(f"Failed to upload {key} with SSE-C after {duration:.2f}s: {e}")
raise

Testing your implementation

Test key validation

Implement comprehensive tests for key validation:

Example
def test_key_validation():
# Test valid key
valid_key_bytes = secrets.token_bytes(32)
valid_key = base64.b64encode(valid_key_bytes).decode('utf-8')
assert validate_encryption_key(valid_key) == True
# Test invalid key length
try:
validate_encryption_key("short")
assert False, "Should raise ValueError"
except ValueError:
pass
# Test invalid base64
try:
validate_encryption_key("invalid-base64-key-here")
assert False, "Should raise ValueError"
except ValueError:
pass

Test error handling

Test for key-related errors:

Example
def test_key_error_handling():
# Test with incorrect key
wrong_key_bytes = secrets.token_bytes(32)
wrong_key = base64.b64encode(wrong_key_bytes).decode('utf-8')
try:
upload_with_sse_c(s3_client, bucket, key, file_path, wrong_key)
assert False, "Should raise KeyError"
except KeyError:
pass

Additional resources