Skip to content

Security Best Practices

This guide covers security best practices when building serverless applications with Nimbus.

Overview

Security in serverless applications requires attention to multiple layers: infrastructure, application code, data protection, and access control. Nimbus provides built-in security features and follows AWS security best practices.

Core Security Principles

1. Principle of Least Privilege

Grant only the minimum permissions necessary for each function to operate.

typescript
// ✅ Good - specific permissions
const nimbus = new Nimbus({ projectName: 'my-app' });
const api = nimbus.API({ name: 'user-api' });

api.route('POST', '/users', async (event) => {
  // Process user data
  permissions: [
    {
      Effect: 'Allow',
      Action: ['dynamodb:GetItem', 'dynamodb:PutItem'],
      Resource: 'arn:aws:dynamodb:region:account:table/users'
    }
  ]
});

// ❌ Avoid - overly broad permissions  
api.route('POST', '/users', async (event) => {
  // This function would have overly broad permissions
  permissions: [
    {
      Effect: 'Allow',
      Action: ['*'],
      Resource: ['*']
    }
  ]
});

2. Defense in Depth

Implement multiple layers of security controls.

typescript
const api = nimbus.api({
  name: 'secure-api'
});

// Layer 1: Authentication
api.authorizer({
  name: 'jwt-auth',
  type: 'TOKEN',
  handler: './auth/authorizer.js'
});

// Layer 2: Input validation
api.route('POST', '/users', async (event) => {
  // Validate input
  const { error, value } = validateUserInput(event.body);
  if (error) {
    return { statusCode: 400, body: JSON.stringify({ error: error.message }) };
  }
  
  // Process validated data
  return processUser(value);
}, { authorizer: 'jwt-auth' });

3. Secure by Default

Use secure defaults and explicit security configurations.

typescript
// ✅ Good - explicit security settings
const kv = nimbus.KV({
  name: 'user-data',
  encryption: true, // Explicit encryption
  pointInTimeRecovery: true,
  deletionProtection: true
});

const storage = nimbus.Storage({
  name: 'user-uploads',
  encryption: true,
  versioning: true,
  publicAccess: false // Explicit private access
});

Secrets and Configuration Management

Never Hardcode Secrets

typescript
// ❌ NEVER do this
const apiKey = 'sk_live_1234567890abcdef'; // Hardcoded secret

// ✅ DO this instead
const apiKeySecret = nimbus.Secret({
  name: 'stripe-api-key',
  description: 'Stripe API key for payments'
});

// Use in Lambda function
api.route('POST', '/charge', async (event) => {
  const { runtime } = await import('nimbus-framework');
  const apiKey = await runtime.secrets.getString('stripe-api-key');
  // Use apiKey securely
});

Separate Configuration by Environment

typescript
// Environment-specific secrets
const prodDbSecret = nimbus.Secret({
  name: 'prod-database-credentials',
  description: 'Production database credentials'
});

const devDbSecret = nimbus.Secret({
  name: 'dev-database-credentials',
  description: 'Development database credentials'
});

// Environment-specific parameters
const prodConfig = nimbus.Parameter({
  name: '/myapp/prod/config',
  description: 'Production configuration'
});

Use Encryption for Sensitive Parameters

typescript
// ✅ Good - encrypted parameter
const sensitiveConfig = nimbus.Parameter({
  name: '/app/sensitive-config',
  type: 'SecureString', // Encrypted with KMS
  description: 'Sensitive configuration data'
});

// ❌ Avoid - plain text sensitive data
const sensitiveConfig = nimbus.Parameter({
  name: '/app/sensitive-config',
  type: 'String', // Not encrypted
  description: 'Sensitive configuration data'
});

API Security

Authentication and Authorization

typescript
// JWT-based authentication
const jwtAuthorizer = nimbus.api({
  name: 'secure-api'
}).authorizer({
  name: 'jwt-auth',
  type: 'TOKEN',
  handler: async (event) => {
    const { runtime } = await import('nimbus-framework');
    
    try {
      const token = event.authorizationToken.replace('Bearer ', '');
      const jwtSecret = await runtime.secrets.getString('jwt-secret');
      
      const decoded = jwt.verify(token, jwtSecret);
      
      return {
        principalId: decoded.sub,
        policyDocument: {
          Version: '2012-10-17',
          Statement: [{
            Action: 'execute-api:Invoke',
            Effect: 'Allow',
            Resource: event.methodArn
          }]
        },
        context: {
          userId: decoded.sub,
          email: decoded.email
        }
      };
    } catch (error) {
      throw new Error('Unauthorized');
    }
  }
});

Input Validation

typescript
import Joi from 'joi';

const userSchema = Joi.object({
  email: Joi.string().email().required(),
  name: Joi.string().min(2).max(50).required(),
  age: Joi.number().integer().min(18).max(120)
});

api.route('POST', '/users', async (event) => {
  try {
    // Validate input
    const { error, value } = userSchema.validate(JSON.parse(event.body || '{}'));
    
    if (error) {
      return {
        statusCode: 400,
        body: JSON.stringify({
          error: 'Validation failed',
          details: error.details.map(d => d.message)
        })
      };
    }
    
    // Process validated data
    const user = await createUser(value);
    
    return {
      statusCode: 201,
      body: JSON.stringify(user)
    };
    
  } catch (error) {
    console.error('User creation error:', error);
    return {
      statusCode: 500,
      body: JSON.stringify({ error: 'Internal server error' })
    };
  }
});

CORS Configuration

typescript
api.route('GET', '/public-data', async (event) => {
  return {
    statusCode: 200,
    headers: {
      'Access-Control-Allow-Origin': 'https://myapp.com', // Specific origin
      'Access-Control-Allow-Methods': 'GET',
      'Access-Control-Allow-Headers': 'Content-Type, Authorization',
      'Access-Control-Max-Age': '86400'
    },
    body: JSON.stringify({ data: 'public data' })
  };
}, { cors: true });

Data Protection

Encryption at Rest

typescript
// DynamoDB encryption
const encryptedKV = nimbus.KV({
  name: 'sensitive-data',
  encryption: true, // Server-side encryption
  kmsKeyId: 'arn:aws:kms:region:account:key/key-id' // Custom KMS key
});

// S3 encryption
const encryptedStorage = nimbus.Storage({
  name: 'sensitive-files',
  encryption: true,
  kmsKeyId: 'arn:aws:kms:region:account:key/key-id'
});

Encryption in Transit

typescript
// All Nimbus APIs use HTTPS by default
const api = nimbus.api({
  name: 'secure-api',
  // HTTPS is enforced automatically
});

// Custom domain with TLS certificate
const api = nimbus.api({
  name: 'secure-api',
  customDomain: 'api.myapp.com', // Automatic ACM certificate
});

Data Sanitization

typescript
api.route('POST', '/comments', async (event) => {
  const { runtime } = await import('nimbus-framework');
  
  const { comment } = JSON.parse(event.body || '{}');
  
  // Sanitize HTML input
  const sanitizedComment = sanitizeHtml(comment, {
    allowedTags: ['b', 'i', 'em', 'strong'],
    allowedAttributes: {}
  });
  
  // Store sanitized data
  await runtime.kv.put('comments-table', {
    id: generateId(),
    comment: sanitizedComment,
    createdAt: new Date().toISOString()
  });
  
  return {
    statusCode: 201,
    body: JSON.stringify({ message: 'Comment created' })
  };
});

Monitoring and Logging

Security Monitoring

typescript
// Enable X-Ray tracing for security analysis
const api = nimbus.api({
  name: 'monitored-api',
  tracing: true // Enable X-Ray tracing
});

// Log security events
api.route('POST', '/sensitive-operation', async (event) => {
  const userId = event.requestContext.authorizer?.userId;
  const sourceIP = event.requestContext.identity?.sourceIp;
  
  // Log security-relevant events
  console.log(JSON.stringify({
    event: 'sensitive_operation_attempted',
    userId,
    sourceIP,
    timestamp: new Date().toISOString(),
    userAgent: event.headers['User-Agent']
  }));
  
  // Perform operation
  const result = await performSensitiveOperation();
  
  // Log successful completion
  console.log(JSON.stringify({
    event: 'sensitive_operation_completed',
    userId,
    timestamp: new Date().toISOString()
  }));
  
  return {
    statusCode: 200,
    body: JSON.stringify(result)
  };
});

Error Handling Without Information Disclosure

typescript
api.route('POST', '/login', async (event) => {
  try {
    const { email, password } = JSON.parse(event.body || '{}');
    
    const user = await authenticateUser(email, password);
    
    if (!user) {
      // Generic error message - don't reveal if user exists
      return {
        statusCode: 401,
        body: JSON.stringify({ error: 'Invalid credentials' })
      };
    }
    
    return {
      statusCode: 200,
      body: JSON.stringify({ token: generateToken(user) })
    };
    
  } catch (error) {
    // Log detailed error internally
    console.error('Authentication error:', error);
    
    // Return generic error to client
    return {
      statusCode: 500,
      body: JSON.stringify({ error: 'Authentication failed' })
    };
  }
});

Compliance and Auditing

CloudTrail Integration

typescript
// All AWS API calls are automatically logged to CloudTrail
// Including:
// - Secret access
// - Parameter retrieval
// - Database operations
// - File storage access

Data Retention Policies

typescript
const auditStorage = nimbus.Storage({
  name: 'audit-logs',
  encryption: true,
  versioning: true,
  lifecycleRules: [
    {
      id: 'audit-retention',
      status: 'Enabled',
      transitions: [
        {
          days: 30,
          storageClass: 'STANDARD_IA'
        },
        {
          days: 90,
          storageClass: 'GLACIER'
        }
      ],
      expiration: {
        days: 2555 // 7 years retention
      }
    }
  ]
});

Security Checklist

Development Phase

  • [ ] No hardcoded secrets or credentials
  • [ ] Input validation on all endpoints
  • [ ] Proper error handling without information disclosure
  • [ ] Authentication and authorization implemented
  • [ ] HTTPS enforced for all communications
  • [ ] Sensitive data encrypted at rest and in transit

Deployment Phase

  • [ ] Secrets stored in AWS Secrets Manager
  • [ ] Parameters encrypted with SecureString type
  • [ ] CloudTrail logging enabled
  • [ ] Monitoring and alerting configured
  • [ ] Backup and recovery procedures tested

Production Phase

  • [ ] Regular security audits
  • [ ] Secret rotation procedures
  • [ ] Incident response plan
  • [ ] Access reviews and cleanup
  • [ ] Security monitoring and alerting
  • [ ] Compliance reporting

Common Security Pitfalls

1. Logging Sensitive Data

typescript
// ❌ Don't log sensitive data
console.log('User login:', { email, password }); // BAD!

// ✅ Log safely
console.log('User login attempt:', { email, timestamp: new Date().toISOString() });

2. SQL Injection in NoSQL

typescript
// ❌ Vulnerable to injection
const userId = event.pathParameters.userId; // Unsanitized input
const user = await runtime.kv.get('users-table', { id: userId });

// ✅ Validate input
const userId = event.pathParameters.userId;
if (!/^[a-zA-Z0-9-]+$/.test(userId)) {
  return { statusCode: 400, body: 'Invalid user ID format' };
}
const user = await runtime.kv.get('users-table', { id: userId });

3. Insecure Direct Object References

typescript
// ❌ No authorization check
api.route('GET', '/users/:userId', async (event) => {
  const userId = event.pathParameters.userId;
  const user = await getUser(userId); // Any user can access any user
  return { statusCode: 200, body: JSON.stringify(user) };
});

// ✅ Proper authorization
api.route('GET', '/users/:userId', async (event) => {
  const requestedUserId = event.pathParameters.userId;
  const currentUserId = event.requestContext.authorizer?.userId;
  
  // Users can only access their own data
  if (requestedUserId !== currentUserId) {
    return { statusCode: 403, body: 'Access denied' };
  }
  
  const user = await getUser(requestedUserId);
  return { statusCode: 200, body: JSON.stringify(user) };
}, { authorizer: 'jwt-auth' });

Security Resources

AWS Security Best Practices

Security Tools and Libraries

  • Input Validation: Joi, Yup, express-validator
  • Sanitization: DOMPurify, sanitize-html
  • Authentication: jsonwebtoken, passport
  • Encryption: crypto (Node.js built-in), bcrypt

Compliance Frameworks

  • SOC 2: System and Organization Controls
  • PCI DSS: Payment Card Industry Data Security Standard
  • HIPAA: Health Insurance Portability and Accountability Act
  • GDPR: General Data Protection Regulation

Released under the MIT License.