DeployStack Docs

Cloud Credentials Management

DeployStack provides a secure cloud credentials management system that allows teams to store and manage cloud provider credentials for deployments. This system features encryption, role-based access control, and provider validation.

Architecture Overview

The cloud credentials system consists of several key components:

  • Provider Configuration: Defines supported cloud providers and their required fields
  • Encryption Service: Handles secure storage of credential values
  • Validation System: Validates credential data against provider schemas
  • Role-Based Access: Different response formats based on user permissions
  • API Layer: RESTful endpoints for credential management

Database Schema

Team Cloud Credentials Table

CREATE TABLE team_cloud_credentials (
  id TEXT PRIMARY KEY,
  team_id TEXT NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
  provider_id TEXT NOT NULL,
  name TEXT NOT NULL,
  comment TEXT,
  credentials TEXT NOT NULL, -- Encrypted JSON
  created_by TEXT NOT NULL REFERENCES authUser(id),
  created_at INTEGER NOT NULL,
  updated_at INTEGER NOT NULL,
  UNIQUE(team_id, provider_id, name)
);

Key Features

  • Team Isolation: Credentials are scoped to specific teams
  • Provider Support: Multiple cloud providers per team
  • Encrypted Storage: All credential values are encrypted
  • Audit Trail: Tracks creation and modification metadata
  • Unique Constraints: Prevents duplicate credential names per provider/team

Provider Configuration

Cloud providers are configured in services/backend/config/cloud-providers.ts:

export interface CloudProvider {
  id: string;
  name: string;
  description: string;
  fields: CloudProviderField[];
  enabled: boolean;
}

export interface CloudProviderField {
  key: string;
  label: string;
  type: 'text' | 'password' | 'textarea';
  required: boolean;
  secret: boolean;
  placeholder?: string;
  description?: string;
  validation?: {
    pattern?: string;
    minLength?: number;
    maxLength?: number;
  };
}

Example Provider Configuration

{
  id: 'aws',
  name: 'Amazon Web Services',
  description: 'AWS cloud platform credentials',
  fields: [
    {
      key: 'access_key_id',
      label: 'Access Key ID',
      type: 'text',
      required: true,
      secret: false,
      placeholder: 'AKIAIOSFODNN7EXAMPLE'
    },
    {
      key: 'secret_access_key',
      label: 'Secret Access Key',
      type: 'password',
      required: true,
      secret: true,
      validation: {
        minLength: 40
      }
    }
  ],
  enabled: true
}

Encryption System

Storage Format

Credentials are stored as encrypted JSON with metadata:

interface StoredCredentials {
  [fieldKey: string]: {
    value: string;        // Encrypted value
    secret: boolean;      // Field type from provider config
    updatedAt: string;    // ISO timestamp
  };
}

Encryption Process

  1. Field Validation: Validate against provider schema
  2. Individual Encryption: Each field value encrypted separately
  3. Metadata Storage: Include field type and timestamp
  4. JSON Serialization: Store as encrypted JSON string

Security Features

  • AES-256-GCM: Industry-standard encryption algorithm
  • Separate Keys: Encryption keys managed separately from data
  • Field-Level: Each credential field encrypted individually
  • No Plaintext: Credential values never stored in plaintext

Role-Based Access Control

The cloud credentials system uses team-contextual permissions rather than global permissions. For detailed role information and permission matrices, see Role-Based Access Control.

Access Levels

User TypeAccess LevelField InformationCredential Values
Global AdminMetadata only✅ Field types & status❌ No values shown
Team AdminFull CRUD✅ Field types & status🔒 Placeholders for non-secret
Team UserRead-only basic❌ No field details❌ No values shown
Non-memberNo access❌ Blocked❌ Blocked

Key Security Features

  • Team Isolation: Users can only access credentials from teams they belong to
  • No Secret Exposure: Secret values are never returned in API responses
  • Role-Based Responses: API responses vary based on user's role within the team
  • Global Admin Limitations: Even global admins cannot see credential values

API Implementation

Service Layer

The CloudCredentialsService provides the core business logic:

export class CloudCredentialsService {
  // Role-specific methods
  async getTeamCredentials(teamId: string): Promise<CloudCredentialResponse[]>
  async getTeamCredentialsGlobalAdmin(teamId: string): Promise<CloudCredentialResponse[]>
  async getTeamCredentialsBasic(teamId: string): Promise<CloudCredentialBasicResponse[]>
  
  // CRUD operations
  async createCredentials(teamId: string, userId: string, input: CreateCloudCredentialRequest): Promise<CloudCredentialResponse>
  async updateCredentials(credentialId: string, teamId: string, input: UpdateCloudCredentialRequest): Promise<CloudCredentialResponse | null>
  async deleteCredentials(credentialId: string, teamId: string): Promise<boolean>
  
  // Internal methods
  async getDecryptedCredentials(credentialId: string, teamId: string): Promise<Record<string, string> | null>
}

Route Implementation

Routes automatically detect user role and call appropriate service methods:

// Check user permissions and role
const roleService = new RoleService();
const hasAdminPermissions = await roleService.userHasPermission(userId, 'cloud_credentials.edit');
const userRole = await roleService.getUserRole(userId);
const isGlobalAdmin = userRole?.id === 'global_admin';

let credentials;
if (hasAdminPermissions && !isGlobalAdmin) {
  // Team admin - full details with placeholders
  credentials = await cloudCredentialsService.getTeamCredentials(teamId);
} else if (isGlobalAdmin) {
  // Global admin - metadata only, no values
  credentials = await cloudCredentialsService.getTeamCredentialsGlobalAdmin(teamId);
} else {
  // Team member - basic details only
  credentials = await cloudCredentialsService.getTeamCredentialsBasic(teamId);
}

API Endpoints

List Cloud Providers

GET /api/teams/:teamId/cloud-providers
Authorization: Required (cloud_credentials.view permission)

Returns available cloud providers with their field schemas.

List Team Credentials

GET /api/teams/:teamId/cloud-credentials
Authorization: Required (cloud_credentials.view permission)

Returns credentials list with response format based on user role.

Create Credentials

POST /api/teams/:teamId/cloud-credentials
Authorization: Required (cloud_credentials.create permission)
Content-Type: application/json

{
  "providerId": "aws",
  "name": "Production AWS",
  "comment": "Production environment credentials",
  "credentials": {
    "access_key_id": "AKIAIOSFODNN7EXAMPLE",
    "secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
  }
}

Update Credentials

PUT /api/teams/:teamId/cloud-credentials/:credentialId
Authorization: Required (cloud_credentials.edit permission)

Supports partial updates of name, comment, and credential values.

Delete Credentials

DELETE /api/teams/:teamId/cloud-credentials/:credentialId
Authorization: Required (cloud_credentials.delete permission)

Permanently removes credentials from the team.

Validation System

Create vs Update Validation

The system provides two validation functions to handle different scenarios:

Full Validation (Create)

export function validateCredentialData(
  providerId: string, 
  credentials: Record<string, string>
): ValidationResult {
  const provider = getCloudProvider(providerId);
  if (!provider) {
    return { valid: false, errors: ['Invalid provider ID'] };
  }

  const errors: string[] = [];
  
  // Check ALL required fields
  for (const field of provider.fields) {
    if (field.required && !credentials[field.key]) {
      errors.push(`${field.label} is required`);
    }
  }
  
  // Validate all provided fields
  for (const [key, value] of Object.entries(credentials)) {
    const field = provider.fields.find(f => f.key === key);
    if (field?.validation) {
      // Apply validation rules
    }
  }
  
  return { valid: errors.length === 0, errors };
}

Partial Validation (Update)

export function validateCredentialDataForUpdate(
  providerId: string, 
  credentials: Record<string, string>
): ValidationResult {
  const provider = getCloudProvider(providerId);
  if (!provider) {
    return { valid: false, errors: ['Invalid provider ID'] };
  }

  const errors: string[] = [];
  
  // Only validate fields that are actually provided
  for (const field of provider.fields) {
    const value = credentials[field.key];
    
    // Skip validation if field is not provided
    if (value === null || value === undefined) {
      continue;
    }
    
    // Validate provided fields
    if (field.required && value.trim() === '') {
      errors.push(`${field.label} cannot be empty`);
    }
    
    // Apply format validation if present
    if (field.validation) {
      // Apply validation rules
    }
  }
  
  return { valid: errors.length === 0, errors };
}

Update Process

When updating credentials, the system:

  1. Validates only provided fields using validateCredentialDataForUpdate
  2. Retrieves existing credentials from encrypted storage
  3. Merges updates with existing values
  4. Re-encrypts the complete credential set

This allows partial updates without requiring users to re-submit secret values.

Validation Rules

  • Required Fields: Enforced based on provider configuration
  • Field Types: Text, password, textarea validation
  • Format Validation: Pattern matching, length constraints
  • Provider Schema: Validates against defined field structure
  • Partial Updates: Only validates fields being updated

Error Handling

Common Error Scenarios

// Provider not found
throw new Error('Invalid provider ID');

// Validation failure
throw new Error(`Validation failed: ${validation.errors.join(', ')}`);

// Duplicate name
throw new Error('A credential set with this name already exists for this provider');

// Not found
return null; // Handled as 404 in routes

Error Response Format

{
  "success": false,
  "error": "Validation failed",
  "details": ["Access Key ID is required", "Secret Access Key must be at least 40 characters"]
}

Security Considerations

Data Protection

  • Encryption at Rest: All credential values encrypted before storage
  • No Plaintext Logs: Credential values never logged in plaintext
  • Secure Transmission: HTTPS required for all API calls
  • Access Control: Role-based response filtering

Best Practices

  • Principle of Least Privilege: Users see only necessary information
  • Audit Logging: Track all credential operations
  • Regular Rotation: Encourage credential rotation
  • Secure Defaults: Safe fallbacks for all operations

Adding New Providers

1. Define Provider Configuration

Add new provider to cloud-providers.ts:

{
  id: 'new-provider',
  name: 'New Cloud Provider',
  description: 'Description of the provider',
  fields: [
    {
      key: 'api_key',
      label: 'API Key',
      type: 'password',
      required: true,
      secret: true
    }
  ],
  enabled: true
}

2. Update Provider Registry

Add to the providers array and export:

export const CLOUD_PROVIDERS: CloudProvider[] = [
  AWS_PROVIDER,
  RENDER_PROVIDER,
  NEW_PROVIDER, // Add here
];

3. Test Integration

  • Validate field schemas work correctly
  • Test encryption/decryption of new field types
  • Verify API responses include new provider
  • Test credential creation and validation

Troubleshooting

Common Issues

Encryption Errors

  • Verify encryption service is properly configured
  • Check that encryption keys are available
  • Ensure proper error handling for encryption failures

Validation Failures

  • Check provider configuration matches expected format
  • Verify required fields are properly marked
  • Test validation rules with sample data

Permission Errors

  • Confirm user has required permissions
  • Check role assignments are correct
  • Verify middleware is properly applied to routes

Debug Commands

// Test provider configuration
const provider = getCloudProvider('aws');
console.log('Provider config:', provider);

// Test validation
const validation = validateCredentialData('aws', testCredentials);
console.log('Validation result:', validation);

// Check user permissions
const hasPermission = await roleService.userHasPermission(userId, 'cloud_credentials.view');
console.log('Has permission:', hasPermission);

Performance Considerations

Optimization Strategies

  • Lazy Loading: Load provider configurations on demand
  • Caching: Cache provider configurations in memory
  • Batch Operations: Support bulk credential operations
  • Pagination: Implement pagination for large credential lists

Monitoring

  • API Response Times: Monitor credential API performance
  • Encryption Overhead: Track encryption/decryption performance
  • Database Queries: Optimize credential lookup queries
  • Memory Usage: Monitor provider configuration memory usage

Future Enhancements

Planned Features

  • Credential Sharing: Share credentials between teams
  • Expiration Dates: Set expiration dates for credentials
  • Usage Tracking: Track which deployments use which credentials
  • Backup/Restore: Export/import encrypted credential backups
  • Integration Testing: Test credentials against actual providers

Extension Points

  • Custom Providers: Plugin system for custom cloud providers
  • Validation Plugins: Custom validation rules for specific providers
  • Encryption Backends: Support for different encryption systems
  • Audit Plugins: Custom audit logging implementations