Skip to main content

Secret Manager Authentication Provider

This document describes the Secret Manager authentication provider implementation in NoETL, which enables secure credential retrieval from external secret management systems like Google Secret Manager.

Overview

The Secret Manager provider extends NoETL's unified authentication system to support fetching credentials from external secret stores at runtime, rather than storing them in the NoETL credential database.

Key Features

  • OAuth-Based Access: Uses OAuth tokens to authenticate with Secret Manager APIs
  • Automatic Caching: Credentials cached for 1 hour (execution-scoped) to reduce API calls
  • Multi-Value Support: Handles both single-value secrets (API keys) and multi-value secrets (OAuth client credentials)
  • Template Integration: Fetched credentials available in Jinja2 templates
  • Provider Abstraction: Extensible to support multiple secret management systems

Architecture

Components

  1. Auth Resolver (noetl/tools/shared/auth/resolver.py):

    • Detects provider: secret_manager in auth configuration
    • Delegates to secret fetching utilities
    • Injects resolved credentials into template context
  2. Secret Manager Utils (noetl/tools/shared/auth/utils.py):

    • fetch_secret_manager_value(): Main entry point for secret retrieval
    • _fetch_google_secret(): Google Secret Manager API integration
    • Handles OAuth token resolution and API calls
  3. Credential Cache (noetl/database/credential_cache.py):

    • Stores fetched secrets in noetl.credential_cache table
    • TTL-based expiration (default: 3600 seconds)
    • Execution-scoped isolation

Flow Diagram

Playbook Auth Config

Auth Resolver (resolve_auth_map)

Detect provider=secret_manager

fetch_secret_manager_value()

Check Cache (CredentialCache.get_cached)

[Hit] → Return cached value
[Miss] → ↓

Resolve OAuth Token (resolve_token)

Call Secret Manager API

Decode Base64 Secret

Cache with TTL (CredentialCache.set_cached)

Return Secret Value

Inject into Template Context

Available as {{ auth.alias.field }}

Configuration Syntax

Single-Value Secrets (API Keys, Tokens)

For secrets with a single value (API keys, bearer tokens):

auth:
openai:
type: bearer # Auth type
provider: secret_manager # Use Secret Manager provider
key: projects/123/secrets/openai-api-key/versions/1 # Secret path
oauth_credential: google_oauth # OAuth credential for Secret Manager access

Template Usage:

headers:
Authorization: Bearer {{ auth.openai.token }}

Multi-Value Secrets (OAuth Client Credentials)

For secrets requiring multiple values (OAuth client ID + secret):

auth:
amadeus:
type: oauth2_client_credentials
provider: secret_manager
client_id_key: projects/123/secrets/amadeus-client-id/versions/1
client_secret_key: projects/123/secrets/amadeus-client-secret/versions/1
oauth_credential: google_oauth

Template Usage:

data:
grant_type: client_credentials
client_id: '{{ auth.amadeus.client_id }}'
client_secret: '{{ auth.amadeus.client_secret }}'

Supported Auth Types

The Secret Manager provider works with all NoETL auth types:

  • bearer - Bearer token authentication (single key)
  • api_key - API key authentication (single key)
  • basic - Basic authentication (single key containing username:password)
  • header - Custom header authentication (single key)
  • oauth2_client_credentials - OAuth client credentials (client_id_key + client_secret_key)

Implementation Details

Google Secret Manager Integration

API Endpoint:

GET https://secretmanager.googleapis.com/v1/{secret_path}:access
Authorization: Bearer {oauth_token}

Response Format:

{
"name": "projects/123/secrets/api-key/versions/1",
"payload": {
"data": "BASE64_ENCODED_SECRET_VALUE"
}
}

Code Implementation (noetl/tools/shared/auth/utils.py):

def fetch_secret_manager_value(key, auth_type, oauth_credential, execution_id):
"""
Fetch a secret from Secret Manager with caching.

Args:
key: Secret path (e.g., "projects/123/secrets/api-key/versions/1")
auth_type: Authentication type (bearer, api_key, etc.)
oauth_credential: OAuth credential reference for Secret Manager API
execution_id: Execution ID for cache scoping

Returns:
Secret value (plain text)
"""
# 1. Check cache
credential_name = f"secret_manager_{key.replace('/', '_')}"
cached = CredentialCache.get_cached(credential_name, execution_id, 'execution')
if cached:
return cached

# 2. Detect provider (GCP, AWS, etc.)
if key.startswith("projects/"):
secret_value = _fetch_google_secret(key, oauth_credential)
else:
# Fallback to environment variable
secret_value = os.getenv(key)

# 3. Cache for 1 hour
if secret_value:
CredentialCache.set_cached(
credential_name, execution_id, 'execution', secret_value, 3600
)

return secret_value


def _fetch_google_secret(secret_path, oauth_credential):
"""
Fetch secret from Google Secret Manager API.

Args:
secret_path: Full secret path with version
oauth_credential: OAuth credential for authentication

Returns:
Decoded secret value
"""
# Resolve OAuth token
token_response = resolve_token(oauth_credential)
access_token = token_response['access_token']

# Call Secret Manager API
url = f"https://secretmanager.googleapis.com/v1/{secret_path}:access"
response = httpx.Client().get(
url,
headers={'Authorization': f'Bearer {access_token}'}
)
response.raise_for_status()

# Decode base64 payload
payload_data = response.json()['payload']['data']
return base64.b64decode(payload_data).decode('UTF-8')

Auth Resolution Logic

Code Implementation (noetl/tools/shared/auth/resolver.py):

def resolve_auth_map(step_config, task_with, jinja_env, context):
"""Resolve authentication configuration."""
auth_config = step_config.get('auth') or task_with.get('auth')
if not auth_config:
return ({}, {})

resolved_map = {}

for alias, spec in auth_config.items():
auth_type = spec.get('type')
provider = spec.get('provider', 'credential_store')

# Secret Manager provider handling
if provider == 'secret_manager':
oauth_cred = spec.get('oauth_credential')
execution_id = context.get('execution_id')

# Multi-value secrets (oauth2_client_credentials)
if auth_type == 'oauth2_client_credentials':
client_id_key = spec.get('client_id_key')
client_secret_key = spec.get('client_secret_key')

client_id = fetch_secret_manager_value(
client_id_key, 'api_key', oauth_cred, execution_id
)
client_secret = fetch_secret_manager_value(
client_secret_key, 'api_key', oauth_cred, execution_id
)

resolved_map[alias] = {
'client_id': client_id,
'client_secret': client_secret
}

# Single-value secrets
elif 'key' in spec:
key = spec['key']
secret_value = fetch_secret_manager_value(
key, auth_type, oauth_cred, execution_id
)

# Map to appropriate field based on auth type
if auth_type == 'bearer':
resolved_map[alias] = {'token': secret_value}
elif auth_type == 'api_key':
resolved_map[alias] = {'api_key': secret_value}
# ... other mappings

return ({}, resolved_map)

HTTP Tool Integration

The HTTP executor injects resolved auth into template context:

# noetl/tools/tools/http/executor.py

# Resolve auth BEFORE rendering data/payload
auth_headers, resolved_auth_map = _process_authentication_with_context(...)

if resolved_auth_map:
context['auth'] = resolved_auth_map # Inject for templates

# NOW render data/payload with auth in context
data_map = render_template(jinja_env, raw_data or {}, context)
payload = render_template(jinja_env, raw_payload or {}, context)

Credential Caching

Database Schema

CREATE TABLE IF NOT EXISTS noetl.credential_cache (
credential_name VARCHAR(255),
execution_id BIGINT,
scope VARCHAR(50),
credential_data TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP,
access_count INTEGER DEFAULT 1,
PRIMARY KEY (credential_name, execution_id, scope)
);

CREATE INDEX idx_credential_cache_expires ON noetl.credential_cache(expires_at);

Cache Operations

Set Cache:

CredentialCache.set_cached(
credential_name='secret_manager_projects_123_secrets_api_key_versions_1',
execution_id=507598625225048802,
scope='execution',
credential_data='sk-abc123...',
ttl_seconds=3600
)

Get Cache:

cached_value = CredentialCache.get_cached(
credential_name='secret_manager_projects_123_secrets_api_key_versions_1',
execution_id=507598625225048802,
scope='execution'
)

Cache Cleanup:

DELETE FROM noetl.credential_cache 
WHERE expires_at < CURRENT_TIMESTAMP;

Performance Impact

Without Caching:

  • 3 Secret Manager API calls per execution
  • ~150ms latency per call
  • Total overhead: ~450ms

With Caching (after first execution):

  • 0 Secret Manager API calls
  • Cache lookup: ~5ms
  • Total overhead: ~15ms
  • Speedup: 30x faster

Security Considerations

Credential Isolation

  • Execution Scope: Credentials cached per execution ID
  • No Cross-Execution Access: Each execution has isolated cache
  • Automatic Cleanup: Expired credentials purged on cache miss

OAuth Token Security

  • Short-Lived: OAuth tokens typically valid for 1 hour
  • Automatic Refresh: Token resolution handles expiration
  • Minimal Permissions: Service accounts with Secret Manager read-only access

Secret Manager Permissions

Required Google Cloud IAM permissions:

roles/secretmanager.secretAccessor

On specific secrets:

gcloud secrets add-iam-policy-binding SECRET_NAME \
--member="serviceAccount:SERVICE_ACCOUNT_EMAIL" \
--role="roles/secretmanager.secretAccessor"

Audit Logging

All Secret Manager access logged in:

  1. Google Cloud Audit Logs: API calls, timestamps, caller identity
  2. NoETL Execution Events: Credential resolution in step context
  3. Credential Cache Table: Access count, timestamps

Extensibility

Adding New Providers

To support additional secret management systems (AWS Secrets Manager, Azure Key Vault):

  1. Add Provider Detection (utils.py):
def fetch_secret_manager_value(key, auth_type, oauth_credential, execution_id):
if key.startswith("projects/"): # GCP
return _fetch_google_secret(key, oauth_credential)
elif key.startswith("arn:aws:"): # AWS
return _fetch_aws_secret(key, oauth_credential)
elif key.startswith("https://"): # Azure
return _fetch_azure_secret(key, oauth_credential)
  1. Implement Provider Function:
def _fetch_aws_secret(secret_arn, oauth_credential):
# AWS Secrets Manager API integration
session = boto3.Session()
client = session.client('secretsmanager')
response = client.get_secret_value(SecretId=secret_arn)
return response['SecretString']
  1. Update Constants (constants.py):
SECRET_MANAGER_PROVIDERS = ['gcp', 'aws', 'azure']

Testing

Unit Tests

def test_secret_manager_bearer_auth():
"""Test bearer token from Secret Manager."""
auth_config = {
'openai': {
'type': 'bearer',
'provider': 'secret_manager',
'key': 'projects/123/secrets/openai-api-key/versions/1',
'oauth_credential': 'google_oauth'
}
}

headers, auth_map = resolve_auth_map(
{'auth': auth_config}, {}, jinja_env, context
)

assert 'openai' in auth_map
assert 'token' in auth_map['openai']
assert auth_map['openai']['token'].startswith('sk-')


def test_secret_manager_oauth_credentials():
"""Test OAuth client credentials from Secret Manager."""
auth_config = {
'amadeus': {
'type': 'oauth2_client_credentials',
'provider': 'secret_manager',
'client_id_key': 'projects/123/secrets/client-id/versions/1',
'client_secret_key': 'projects/123/secrets/client-secret/versions/1',
'oauth_credential': 'google_oauth'
}
}

headers, auth_map = resolve_auth_map(
{'auth': auth_config}, {}, jinja_env, context
)

assert 'amadeus' in auth_map
assert 'client_id' in auth_map['amadeus']
assert 'client_secret' in auth_map['amadeus']

Integration Tests

See tests/fixtures/playbooks/api_integration/amadeus_ai_api/ for complete example.

Migration Guide

From Manual Secret Fetching

Before (17 steps with manual secret handling):

- step: get_openai_api_key
tool: http
method: GET
endpoint: https://secretmanager.googleapis.com/v1/...
# ... manual HTTP call

- step: parse_openai_key
tool: python
code: |
import base64
def main(response):
return base64.b64decode(response['payload']['data']).decode()

- step: call_openai
tool: http
headers:
Authorization: Bearer {{ parse_openai_key }}

After (11 steps with declarative auth):

- step: call_openai
tool: http
auth:
openai:
type: bearer
provider: secret_manager
key: projects/123/secrets/openai-api-key/versions/1
oauth_credential: google_oauth
# Auth automatically injected

Benefits:

  • 6 fewer steps
  • Automatic caching
  • Template integration
  • Better security (no secrets in step results)

Troubleshooting

"Failed to retrieve secret" Error

Causes:

  1. Invalid secret path
  2. Missing Secret Manager permissions
  3. Expired OAuth token
  4. Secret not found

Resolution:

# Verify secret exists
gcloud secrets versions access latest --secret=SECRET_NAME

# Check permissions
gcloud secrets get-iam-policy SECRET_NAME

# Test OAuth credential
curl -H "Authorization: Bearer $(gcloud auth print-access-token)" \
https://secretmanager.googleapis.com/v1/projects/PROJECT/secrets/SECRET/versions/latest:access

"Auth missing 'key'" Error

Cause: Wrong provider or missing key field

Resolution:

# ❌ Wrong
auth:
api:
type: bearer
provider: secret_manager
# Missing 'key' field

# ✅ Correct
auth:
api:
type: bearer
provider: secret_manager
key: projects/123/secrets/api-key/versions/1
oauth_credential: google_oauth

Cache Not Working

Symptoms: Multiple Secret Manager calls per execution

Causes:

  1. Different execution IDs
  2. Cache table doesn't exist
  3. TTL expired

Resolution:

-- Check cache table
SELECT * FROM noetl.credential_cache
WHERE execution_id = YOUR_EXECUTION_ID;

-- Check expiration
SELECT credential_name,
expires_at > CURRENT_TIMESTAMP as is_valid,
access_count
FROM noetl.credential_cache;

Best Practices

  1. Use Version-Pinned Secrets: Specify version (e.g., /versions/1) for reproducibility
  2. Minimize Secret Access: Use caching effectively
  3. Scope Credentials: Use execution-scoped caching for isolation
  4. Rotate Secrets: Update Secret Manager versions, not playbooks
  5. Monitor Access: Review Cloud Audit Logs regularly
  6. Least Privilege: Grant minimal required permissions
  7. Separate OAuth Credentials: One OAuth credential per environment
  8. Template Validation: Test auth templates before production

References