Notebook Credentials Guide¶
Version: 1.0
Last Updated: 2025-11-08
Status: Production Ready
Table of Contents¶
- Overview
- Quick Start
- Credential Injection Patterns
- AWS S3 Access
- Database Connections
- API Key Injection
- Multi-Service Examples
- External Secrets Operator (ESO)
- Vault Integration
- Security Best Practices
- Troubleshooting
Overview¶
The Jupyter Notebook Validator Operator supports secure credential injection for notebooks that need to access external services during validation. This guide covers all supported patterns and best practices.
Supported Services¶
- Cloud Storage: AWS S3, Azure Blob Storage, GCP Cloud Storage
- Databases: PostgreSQL, MySQL, MongoDB, Redis
- APIs: OpenAI, Hugging Face, MLflow, custom REST APIs
- Model Registries: MLflow, KServe, Seldon
- Data Platforms: Snowflake, Databricks, BigQuery
Three-Tier Strategy¶
- Tier 1: Environment Variables (Basic) - Kubernetes Secrets as env vars
- Tier 2: External Secrets Operator (Recommended) - Cloud-native secret sync
- Tier 3: Vault Dynamic Secrets (Advanced) - Short-lived credentials
Quick Start¶
Step 1: Create a Secret¶
kubectl create secret generic aws-credentials \
--from-literal=access-key-id=AKIAIOSFODNN7EXAMPLE \
--from-literal=secret-access-key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY \
-n default
Step 2: Reference in NotebookValidationJob¶
apiVersion: mlops.mlops.dev/v1alpha1
kind: NotebookValidationJob
metadata:
name: my-notebook-job
spec:
notebook:
git:
url: "https://github.com/myorg/notebooks.git"
ref: "main"
path: "notebooks/my-notebook.ipynb"
podConfig:
containerImage: "jupyter/scipy-notebook:latest"
env:
- name: AWS_ACCESS_KEY_ID
valueFrom:
secretKeyRef:
name: aws-credentials
key: access-key-id
- name: AWS_SECRET_ACCESS_KEY
valueFrom:
secretKeyRef:
name: aws-credentials
key: secret-access-key
Step 3: Use in Notebook¶
import boto3
import os
# Credentials automatically loaded from environment
s3 = boto3.client('s3')
s3.list_buckets()
Credential Injection Patterns¶
Pattern 1: Individual Environment Variables¶
Use env for individual credentials:
spec:
podConfig:
env:
- name: AWS_ACCESS_KEY_ID
valueFrom:
secretKeyRef:
name: aws-credentials
key: access-key-id
- name: AWS_DEFAULT_REGION
value: "us-east-1" # Plain value for non-sensitive data
When to use: - Few credentials needed - Mix of secret and non-secret values - Fine-grained control over variable names
Pattern 2: Bulk Secret Loading¶
Use envFrom to load all keys from a Secret:
When to use: - Many credentials from same source - All keys should be environment variables - Simpler configuration
Pattern 3: Mixed Approach¶
Combine both patterns:
spec:
podConfig:
envFrom:
- secretRef:
name: database-credentials
env:
- name: AWS_ACCESS_KEY_ID
valueFrom:
secretKeyRef:
name: aws-credentials
key: access-key-id
- name: LOG_LEVEL
value: "INFO"
AWS S3 Access¶
Creating AWS Credentials Secret¶
kubectl create secret generic aws-credentials \
--from-literal=access-key-id=AKIA... \
--from-literal=secret-access-key=wJalr... \
--from-literal=region=us-east-1 \
-n default
NotebookValidationJob Configuration¶
apiVersion: mlops.mlops.dev/v1alpha1
kind: NotebookValidationJob
metadata:
name: s3-data-pipeline
spec:
notebook:
git:
url: "https://github.com/myorg/notebooks.git"
ref: "main"
path: "notebooks/s3-pipeline.ipynb"
podConfig:
containerImage: "jupyter/scipy-notebook:latest"
env:
- name: AWS_ACCESS_KEY_ID
valueFrom:
secretKeyRef:
name: aws-credentials
key: access-key-id
- name: AWS_SECRET_ACCESS_KEY
valueFrom:
secretKeyRef:
name: aws-credentials
key: secret-access-key
- name: AWS_DEFAULT_REGION
valueFrom:
secretKeyRef:
name: aws-credentials
key: region
- name: S3_BUCKET_NAME
value: "my-data-bucket"
Notebook Code (boto3)¶
import boto3
import pandas as pd
import os
# Initialize S3 client (uses AWS_* environment variables)
s3 = boto3.client('s3')
# Get bucket name from environment
bucket = os.environ['S3_BUCKET_NAME']
# Download training data
s3.download_file(bucket, 'data/train.csv', 'train.csv')
df = pd.read_csv('train.csv')
# Train model
model = train_model(df)
# Upload model artifacts
s3.upload_file('model.pkl', bucket, 'models/model.pkl')
Notebook Code (s3fs)¶
import s3fs
import pandas as pd
import os
# Initialize S3 filesystem
fs = s3fs.S3FileSystem(
key=os.environ['AWS_ACCESS_KEY_ID'],
secret=os.environ['AWS_SECRET_ACCESS_KEY']
)
# Read data directly from S3
bucket = os.environ['S3_BUCKET_NAME']
with fs.open(f'{bucket}/data/train.csv', 'r') as f:
df = pd.read_csv(f)
# Write results back to S3
with fs.open(f'{bucket}/results/output.csv', 'w') as f:
df.to_csv(f, index=False)
Database Connections¶
PostgreSQL¶
Creating Database Secret¶
kubectl create secret generic postgres-credentials \
--from-literal=DB_HOST=postgres.example.com \
--from-literal=DB_PORT=5432 \
--from-literal=DB_NAME=mlops_features \
--from-literal=DB_USER=mlops_reader \
--from-literal=DB_PASSWORD=secure-password \
-n default
NotebookValidationJob Configuration¶
apiVersion: mlops.mlops.dev/v1alpha1
kind: NotebookValidationJob
metadata:
name: database-feature-engineering
spec:
notebook:
git:
url: "https://github.com/myorg/notebooks.git"
ref: "main"
path: "notebooks/feature-engineering.ipynb"
podConfig:
containerImage: "jupyter/scipy-notebook:latest"
envFrom:
- secretRef:
name: postgres-credentials
env:
- name: DB_SSL_MODE
value: "require"
Notebook Code (psycopg2)¶
import psycopg2
import pandas as pd
import os
# Connect to PostgreSQL
conn = psycopg2.connect(
host=os.environ['DB_HOST'],
port=os.environ['DB_PORT'],
database=os.environ['DB_NAME'],
user=os.environ['DB_USER'],
password=os.environ['DB_PASSWORD'],
sslmode=os.environ.get('DB_SSL_MODE', 'prefer')
)
# Query features
query = """
SELECT user_id, feature1, feature2, feature3
FROM features
WHERE date >= '2024-01-01'
"""
df = pd.read_sql(query, conn)
# Process features
processed_df = process_features(df)
# Close connection
conn.close()
Notebook Code (SQLAlchemy)¶
from sqlalchemy import create_engine
import pandas as pd
import os
# Create database URL
db_url = f"postgresql://{os.environ['DB_USER']}:{os.environ['DB_PASSWORD']}@{os.environ['DB_HOST']}:{os.environ['DB_PORT']}/{os.environ['DB_NAME']}"
# Create engine
engine = create_engine(db_url)
# Query with pandas
df = pd.read_sql_table('features', engine)
# Or use raw SQL
df = pd.read_sql_query("SELECT * FROM features WHERE date >= '2024-01-01'", engine)
MySQL¶
import mysql.connector
import pandas as pd
import os
# Connect to MySQL
conn = mysql.connector.connect(
host=os.environ['DB_HOST'],
port=int(os.environ['DB_PORT']),
database=os.environ['DB_NAME'],
user=os.environ['DB_USER'],
password=os.environ['DB_PASSWORD']
)
# Query data
df = pd.read_sql("SELECT * FROM features", conn)
conn.close()
MongoDB¶
from pymongo import MongoClient
import os
# Connect to MongoDB
client = MongoClient(
host=os.environ['MONGO_HOST'],
port=int(os.environ['MONGO_PORT']),
username=os.environ['MONGO_USER'],
password=os.environ['MONGO_PASSWORD']
)
# Access database and collection
db = client[os.environ['MONGO_DATABASE']]
collection = db['features']
# Query documents
documents = list(collection.find({'date': {'$gte': '2024-01-01'}}))
API Key Injection¶
OpenAI API¶
Creating API Key Secret¶
kubectl create secret generic api-keys \
--from-literal=openai=sk-proj-... \
--from-literal=huggingface=hf_... \
-n default
NotebookValidationJob Configuration¶
Notebook Code¶
import openai
import os
# Set API key
openai.api_key = os.environ['OPENAI_API_KEY']
# Generate embeddings
response = openai.Embedding.create(
input="Your text here",
model="text-embedding-ada-002"
)
embeddings = response['data'][0]['embedding']
Hugging Face¶
from transformers import pipeline
import os
# Set token
hf_token = os.environ['HUGGINGFACE_TOKEN']
# Load model
classifier = pipeline(
"sentiment-analysis",
use_auth_token=hf_token
)
# Use model
result = classifier("I love this!")
MLflow Tracking¶
Creating MLflow Secret¶
kubectl create secret generic mlflow-credentials \
--from-literal=username=mlflow-user \
--from-literal=password=mlflow-password \
-n default
kubectl create configmap mlflow-config \
--from-literal=MLFLOW_TRACKING_URI=https://mlflow.example.com \
-n default
NotebookValidationJob Configuration¶
spec:
podConfig:
envFrom:
- configMapRef:
name: mlflow-config
env:
- name: MLFLOW_TRACKING_USERNAME
valueFrom:
secretKeyRef:
name: mlflow-credentials
key: username
- name: MLFLOW_TRACKING_PASSWORD
valueFrom:
secretKeyRef:
name: mlflow-credentials
key: password
Notebook Code¶
import mlflow
import os
# Set tracking URI and credentials
mlflow.set_tracking_uri(os.environ['MLFLOW_TRACKING_URI'])
os.environ['MLFLOW_TRACKING_USERNAME'] = os.environ['MLFLOW_TRACKING_USERNAME']
os.environ['MLFLOW_TRACKING_PASSWORD'] = os.environ['MLFLOW_TRACKING_PASSWORD']
# Start experiment
mlflow.set_experiment("my-experiment")
with mlflow.start_run():
# Log parameters
mlflow.log_param("learning_rate", 0.01)
# Train model
model = train_model()
# Log metrics
mlflow.log_metric("accuracy", 0.95)
# Log model
mlflow.sklearn.log_model(model, "model")
Multi-Service Examples¶
See config/samples/mlops_v1alpha1_notebookvalidationjob_multi_service.yaml for a complete example combining:
- AWS S3 for data storage
- PostgreSQL for feature store
- MLflow for experiment tracking
- OpenAI for embeddings
- Hugging Face for models
End-to-End ML Pipeline¶
import boto3
import psycopg2
import mlflow
import openai
import pandas as pd
import os
from sklearn.ensemble import RandomForestClassifier
# 1. Load data from S3
s3 = boto3.client('s3')
s3.download_file(os.environ['S3_BUCKET'], 'data/train.csv', 'train.csv')
df = pd.read_csv('train.csv')
# 2. Load features from database
conn = psycopg2.connect(
host=os.environ['DB_HOST'],
database=os.environ['DB_NAME'],
user=os.environ['DB_USER'],
password=os.environ['DB_PASSWORD']
)
features_df = pd.read_sql("SELECT * FROM features", conn)
conn.close()
# 3. Generate embeddings with OpenAI
openai.api_key = os.environ['OPENAI_API_KEY']
embeddings = []
for text in df['text']:
response = openai.Embedding.create(input=text, model="text-embedding-ada-002")
embeddings.append(response['data'][0]['embedding'])
df['embeddings'] = embeddings
# 4. Train model and track with MLflow
mlflow.set_tracking_uri(os.environ['MLFLOW_TRACKING_URI'])
mlflow.set_experiment("fraud-detection")
with mlflow.start_run():
model = RandomForestClassifier()
model.fit(df[['embeddings']], df['label'])
mlflow.log_param("model_type", "RandomForest")
mlflow.log_metric("accuracy", 0.95)
mlflow.sklearn.log_model(model, "model")
# 5. Save results to S3
s3.upload_file('model.pkl', os.environ['S3_BUCKET'], 'models/fraud-detection.pkl')
External Secrets Operator (ESO)¶
External Secrets Operator syncs secrets from external secret management systems (AWS Secrets Manager, Azure Key Vault, GCP Secret Manager, Vault) into Kubernetes Secrets.
Prerequisites¶
- Install External Secrets Operator:
helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets external-secrets/external-secrets -n external-secrets-system --create-namespace
- Configure cloud provider credentials (example for AWS):
kubectl create secret generic aws-secret-manager-credentials \
--from-literal=access-key-id=AKIA... \
--from-literal=secret-access-key=wJalr... \
-n default
AWS Secrets Manager Integration¶
Step 1: Create SecretStore¶
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: aws-secrets-manager
namespace: default
spec:
provider:
aws:
service: SecretsManager
region: us-east-1
auth:
secretRef:
accessKeyIDSecretRef:
name: aws-secret-manager-credentials
key: access-key-id
secretAccessKeySecretRef:
name: aws-secret-manager-credentials
key: secret-access-key
Step 2: Create ExternalSecret¶
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: database-credentials
namespace: default
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager
kind: SecretStore
target:
name: database-credentials
creationPolicy: Owner
data:
- secretKey: DB_HOST
remoteRef:
key: prod/database/postgres
property: host
- secretKey: DB_USER
remoteRef:
key: prod/database/postgres
property: username
- secretKey: DB_PASSWORD
remoteRef:
key: prod/database/postgres
property: password
Step 3: Use in NotebookValidationJob¶
apiVersion: mlops.mlops.dev/v1alpha1
kind: NotebookValidationJob
metadata:
name: notebook-with-eso
spec:
notebook:
git:
url: "https://github.com/myorg/notebooks.git"
ref: "main"
path: "notebooks/feature-engineering.ipynb"
podConfig:
containerImage: "jupyter/scipy-notebook:latest"
envFrom:
- secretRef:
name: database-credentials # Synced by ESO
Azure Key Vault Integration¶
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: azure-keyvault
namespace: default
spec:
provider:
azurekv:
vaultUrl: "https://my-vault.vault.azure.net"
authType: ServicePrincipal
authSecretRef:
clientId:
name: azure-credentials
key: client-id
clientSecret:
name: azure-credentials
key: client-secret
tenantId: "tenant-id-here"
GCP Secret Manager Integration¶
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: gcp-secret-manager
namespace: default
spec:
provider:
gcpsm:
projectID: "my-project-id"
auth:
secretRef:
secretAccessKeySecretRef:
name: gcp-credentials
key: service-account-key
Vault Integration¶
HashiCorp Vault provides dynamic, short-lived credentials with automatic rotation.
Vault Agent Sidecar Pattern¶
Step 1: Configure Vault Kubernetes Auth¶
# Enable Kubernetes auth
vault auth enable kubernetes
# Configure Kubernetes auth
vault write auth/kubernetes/config \
kubernetes_host="https://kubernetes.default.svc:443" \
kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
token_reviewer_jwt=@/var/run/secrets/kubernetes.io/serviceaccount/token
Step 2: Create Vault Policy¶
Step 3: Create Vault Role¶
vault write auth/kubernetes/role/notebook-validator \
bound_service_account_names=jupyter-notebook-validator-runner \
bound_service_account_namespaces=default \
policies=database-read \
ttl=1h
Step 4: Configure NotebookValidationJob with Vault Annotations¶
apiVersion: mlops.mlops.dev/v1alpha1
kind: NotebookValidationJob
metadata:
name: notebook-with-vault
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "notebook-validator"
vault.hashicorp.com/agent-inject-secret-database: "database/creds/readonly"
vault.hashicorp.com/agent-inject-template-database: |
{{- with secret "database/creds/readonly" -}}
export DB_USER="{{ .Data.username }}"
export DB_PASSWORD="{{ .Data.password }}"
{{- end }}
spec:
notebook:
git:
url: "https://github.com/myorg/notebooks.git"
ref: "main"
path: "notebooks/feature-engineering.ipynb"
podConfig:
containerImage: "jupyter/scipy-notebook:latest"
serviceAccountName: "jupyter-notebook-validator-runner"
Note: Vault Agent sidecar automatically injects credentials and handles rotation.
Security Best Practices¶
1. Use Least Privilege¶
DO: - Create read-only database users for notebooks - Use IAM roles with minimal permissions - Restrict secret access with RBAC
DON'T: - Use admin credentials in notebooks - Grant broad permissions - Share credentials across environments
2. Rotate Credentials Regularly¶
Static Credentials: - Rotate quarterly at minimum - Use automated rotation tools - Track rotation in audit logs
Dynamic Credentials: - Use Vault for short-lived credentials (TTL: 1-24 hours) - Automatic rotation on each notebook run - No manual rotation needed
3. Never Hardcode Credentials¶
DO:
DON'T:
4. Use RBAC for Secret Access¶
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: notebook-secret-reader
namespace: default
rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["aws-credentials", "database-credentials"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: notebook-secret-reader-binding
namespace: default
subjects:
- kind: ServiceAccount
name: jupyter-notebook-validator-runner
namespace: default
roleRef:
kind: Role
name: notebook-secret-reader
apiGroup: rbac.authorization.k8s.io
5. Enable Audit Logging¶
Monitor secret access: - Enable Kubernetes audit logs - Track secret read operations - Alert on suspicious access patterns
6. Encrypt Secrets at Rest¶
Ensure Kubernetes secrets are encrypted:
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: <base64-encoded-secret>
- identity: {}
7. Use Pod Security Standards¶
apiVersion: v1
kind: Pod
metadata:
name: validation-pod
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: notebook
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
Troubleshooting¶
Issue: "Secret not found"¶
Symptoms:
Solutions: 1. Verify secret exists:
-
Check namespace matches:
-
Create secret if missing:
Issue: "Permission denied" accessing secret¶
Symptoms:
Error: secrets "aws-credentials" is forbidden: User "system:serviceaccount:default:jupyter-notebook-validator-runner" cannot get resource "secrets"
Solutions: 1. Check RBAC permissions:
kubectl auth can-i get secrets --as=system:serviceaccount:default:jupyter-notebook-validator-runner -n default
- Create Role and RoleBinding (see Security Best Practices section)
Issue: Environment variables not available in notebook¶
Symptoms:
Solutions: 1. Verify pod has environment variables:
-
Check secret key names match:
-
Verify envFrom syntax:
Issue: ESO ExternalSecret not syncing¶
Symptoms:
Solutions: 1. Check SecretStore status:
-
Verify cloud provider credentials:
-
Check ESO logs:
-
Verify IAM permissions (AWS example):
secretsmanager:GetSecretValuesecretsmanager:DescribeSecret
Issue: Vault Agent sidecar not injecting secrets¶
Symptoms: - No Vault sidecar container in pod - Secrets not available at expected path
Solutions: 1. Verify Vault annotations:
-
Check ServiceAccount has Vault role:
-
Verify Vault Agent Injector is running:
-
Check Vault Agent logs:
Issue: Credentials work locally but not in operator¶
Symptoms: - Notebook runs successfully locally - Fails with authentication errors in operator
Solutions: 1. Check if notebook uses hardcoded credentials:
-
Ensure notebook reads from environment:
-
Test environment variable availability:
AWS Integration¶
AWS Secrets Manager with External Secrets Operator¶
On ROSA and EKS, the recommended pattern uses the External Secrets Operator (ESO) to pull credentials from AWS Secrets Manager and create Kubernetes Secrets that the operator can inject.
# 1. Create a ClusterSecretStore pointing to AWS Secrets Manager
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: aws-secrets-manager
spec:
provider:
aws:
service: SecretsManager
region: us-east-1
auth:
jwt:
serviceAccountRef:
name: external-secrets-sa
namespace: external-secrets
---
# 2. Create an ExternalSecret that syncs specific keys
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: ml-api-credentials
namespace: notebook-validation
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager
kind: ClusterSecretStore
target:
name: ml-api-credentials
creationPolicy: Owner
data:
- secretKey: API_KEY
remoteRef:
key: prod/ml-api-keys
property: api_key
- secretKey: MODEL_ENDPOINT
remoteRef:
key: prod/ml-api-keys
property: model_endpoint
Then reference the synced secret in your NotebookValidationJob:
See the full example at config/samples/mlops_v1alpha1_notebookvalidationjob_aws_secrets_manager.yaml.
IRSA (IAM Roles for Service Accounts)¶
IRSA lets pods assume an IAM role without static credentials. This is the recommended pattern for notebooks that access AWS services (S3, SageMaker, Bedrock) directly.
# 1. Create a ServiceAccount annotated with the IAM role
apiVersion: v1
kind: ServiceAccount
metadata:
name: notebook-validation-sa
namespace: notebook-validation
annotations:
eks.amazonaws.com/role-arn: "arn:aws:iam::123456789012:role/notebook-validation-role"
---
# 2. Reference the SA in the NotebookValidationJob
apiVersion: mlops.mlops.dev/v1alpha1
kind: NotebookValidationJob
metadata:
name: validate-with-irsa
spec:
podConfig:
serviceAccountName: notebook-validation-sa
env:
- name: AWS_DEFAULT_REGION
value: "us-east-1"
The AWS SDK inside the notebook automatically uses the IRSA-provided temporary credentials.
No AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY environment variables are needed.
See the full example at config/samples/mlops_v1alpha1_notebookvalidationjob_irsa.yaml.
Additional Resources¶
- ADR-014: Notebook Credential Injection Strategy
- ADR-009: Secret Management Strategy
- Kubernetes Secrets Documentation
- External Secrets Operator Documentation
- HashiCorp Vault Documentation
- Sample Manifests
Support¶
For issues or questions:
1. Check this troubleshooting guide
2. Review sample manifests in config/samples/
3. Check operator logs: kubectl logs -n jupyter-notebook-validator-system deployment/jupyter-notebook-validator-controller-manager
4. Open an issue on GitHub with:
- NotebookValidationJob YAML
- Pod logs
- Error messages
- Steps to reproduce