Skip to main content

Security & Compliance


Module: Tuner Component: Security Architecture Version: 1.0.0-RELEASE Last Updated: October 26, 2025 Document Type: Security Architecture (Compliance Reference)


Table of Contents

  1. Security Overview
  2. AWS Account Security
  3. Authentication & Authorization
  4. Data Security
  5. Network Security
  6. Compliance & Certifications
  7. Security Best Practices
  8. Incident Response

Security Overview

Security Principles

AWS Tuner is designed with zero-trust architecture and follows defense-in-depth principles:

Security Defense in Depth Layers

Key Security Features

FeatureImplementationBenefit
Read-Only AccessIAM policies with only Describe* and Get* actionsCannot modify customer AWS resources
External IDUnique per-customer identifier in AssumeRolePrevents confused deputy attack
Encryption at RestAES-256 for all databasesData protected if physical media compromised
Encryption in TransitTLS 1.3 for all connectionsData protected during transmission
Secrets ManagementAWS Secrets ManagerNo hardcoded credentials
Audit LoggingAll API calls loggedComplete audit trail
RBACRole-based access controlLeast privilege access

AWS Account Security

Cross-Account Access Model

AWS Tuner uses IAM AssumeRole for secure, temporary access to customer AWS accounts:

Cross-Account Access Model

IAM Policy (Read-Only)

Recommended IAM Policy for CloudKeeperTunerRole:

{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "TunerEC2ReadOnly",
"Effect": "Allow",
"Action": [
"ec2:DescribeInstances",
"ec2:DescribeVolumes",
"ec2:DescribeSnapshots",
"ec2:DescribeImages",
"ec2:DescribeNatGateways",
"ec2:DescribeVpcEndpoints",
"ec2:DescribeAddresses",
"ec2:DescribeRegions",
"ec2:DescribeAvailabilityZones"
],
"Resource": "*"
},
{
"Sid": "TunerRDSReadOnly",
"Effect": "Allow",
"Action": [
"rds:DescribeDBInstances",
"rds:DescribeDBClusters",
"rds:DescribeDBSnapshots",
"rds:DescribeDBEngineVersions"
],
"Resource": "*"
},
{
"Sid": "TunerCloudWatchReadOnly",
"Effect": "Allow",
"Action": [
"cloudwatch:GetMetricStatistics",
"cloudwatch:ListMetrics"
],
"Resource": "*"
},
{
"Sid": "TunerCostExplorerReadOnly",
"Effect": "Allow",
"Action": [
"ce:GetCostAndUsage",
"ce:GetCostForecast"
],
"Resource": "*"
},
{
"Sid": "TunerPricingReadOnly",
"Effect": "Allow",
"Action": [
"pricing:GetProducts"
],
"Resource": "*"
},
{
"Sid": "TunerS3ReadOnly",
"Effect": "Allow",
"Action": [
"s3:ListAllMyBuckets",
"s3:ListBucket",
"s3:GetBucketLocation",
"s3:GetBucketVersioning",
"s3:GetBucketLifecycleConfiguration"
],
"Resource": "*"
},
{
"Sid": "TunerDynamoDBReadOnly",
"Effect": "Allow",
"Action": [
"dynamodb:DescribeTable",
"dynamodb:ListTables",
"dynamodb:DescribeTimeToLive"
],
"Resource": "*"
},
{
"Sid": "TunerRedshiftReadOnly",
"Effect": "Allow",
"Action": [
"redshift:DescribeClusters",
"redshift:DescribeClusterSnapshots"
],
"Resource": "*"
},
{
"Sid": "TunerElastiCacheReadOnly",
"Effect": "Allow",
"Action": [
"elasticache:DescribeCacheClusters",
"elasticache:DescribeReplicationGroups"
],
"Resource": "*"
},
{
"Sid": "TunerAutoScalingReadOnly",
"Effect": "Allow",
"Action": [
"autoscaling:DescribeAutoScalingGroups",
"autoscaling:DescribeLaunchConfigurations",
"autoscaling:DescribeScalingActivities"
],
"Resource": "*"
},
{
"Sid": "TunerECSReadOnly",
"Effect": "Allow",
"Action": [
"ecs:DescribeClusters",
"ecs:DescribeServices",
"ecs:DescribeTasks"
],
"Resource": "*"
},
{
"Sid": "TunerLambdaReadOnly",
"Effect": "Allow",
"Action": [
"lambda:ListFunctions",
"lambda:GetFunction"
],
"Resource": "*"
}
]
}

External ID Best Practices

What is External ID?

External ID is a security measure to prevent the confused deputy problem:

Bad Scenario (without External ID):
1. Attacker creates account in CloudKeeper using victim's AWS account ID
2. Attacker tricks victim into creating IAM role trusting CloudKeeper
3. Attacker uses CloudKeeper to access victim's AWS account
4. ❌ Victim's data exposed to attacker

Good Scenario (with External ID):
1. Attacker creates account in CloudKeeper using victim's AWS account ID
2. CloudKeeper generates unique External ID: "uuid-12345-abcde"
3. Victim creates IAM role requiring External ID "uuid-12345-abcde"
4. Attacker cannot use CloudKeeper to access victim's account (doesn't know External ID)
5. ✅ Victim's data protected

Generating Secure External IDs:

public String generateExternalId(String accountId, String organizationId) {
// Cryptographically secure random UUID
String uuid = UUID.randomUUID().toString();

// HMAC to bind to account
String hmac = HmacUtils.hmacSha256Hex(
secretKey,
accountId + "|" + organizationId + "|" + uuid
);

return uuid + "-" + hmac.substring(0, 16);
}

Credential Rotation

Temporary Credentials (1-hour expiry):

@Component
public class AwsCredentialProvider {

private final Map<String, SessionCredentials> credentialCache = new ConcurrentHashMap<>();

public AWSCredentials getCredentials(String accountId, String roleArn, String externalId) {
String cacheKey = accountId + ":" + roleArn;

// Check if cached credentials are still valid (with 5-minute buffer)
SessionCredentials cached = credentialCache.get(cacheKey);
if (cached != null && cached.getExpiration().isAfter(Instant.now().plusSeconds(300))) {
return cached;
}

// Assume role to get fresh credentials
AssumeRoleRequest request = AssumeRoleRequest.builder()
.roleArn(roleArn)
.roleSessionName("tuner-session-" + accountId)
.externalId(externalId)
.durationSeconds(3600) // 1 hour
.build();

AssumeRoleResponse response = stsClient.assumeRole(request);
Credentials credentials = response.credentials();

SessionCredentials sessionCreds = new SessionCredentials(
credentials.accessKeyId(),
credentials.secretAccessKey(),
credentials.sessionToken(),
credentials.expiration()
);

credentialCache.put(cacheKey, sessionCreds);
return sessionCreds;
}
}

Scheduler Write Permissions (Optional)

For customers using Tuner Scheduler, additional write permissions required:

{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "TunerSchedulerEC2Actions",
"Effect": "Allow",
"Action": [
"ec2:StartInstances",
"ec2:StopInstances"
],
"Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": {
"StringEquals": {
"aws:ResourceTag/Scheduler": "Enabled"
}
}
},
{
"Sid": "TunerSchedulerRDSActions",
"Effect": "Allow",
"Action": [
"rds:StartDBInstance",
"rds:StopDBInstance"
],
"Resource": "arn:aws:rds:*:*:db:*",
"Condition": {
"StringEquals": {
"aws:ResourceTag/Scheduler": "Enabled"
}
}
}
]
}

Safety Mechanisms:

  • ✅ Only resources tagged Scheduler=Enabled
  • ✅ Read-only policy remains for recommendation generation
  • ✅ Separate role or additional policy statement (customer choice)
  • ✅ Audit logging of all start/stop actions

Authentication & Authorization

JWT-Based Authentication

Token Structure:

{
"header": {
"alg": "RS256",
"typ": "JWT"
},
"payload": {
"sub": "user@company.com",
"userId": "usr_abc123",
"organizationId": "org_xyz789",
"roles": ["TUNER_USER", "TUNER_ADMIN"],
"iat": 1698336000,
"exp": 1698422400,
"iss": "cloudkeeper-authx",
"aud": "cloudkeeper-tuner"
},
"signature": "..." // RSA signature
}

Token Lifecycle:

JWT Token Lifecycle

Token Validation (Spring Security):

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {

String token = extractToken(request);

if (token != null) {
try {
// 1. Verify signature and decode
DecodedJWT jwt = jwtVerifier.verify(token);

// 2. Extract claims
String userId = jwt.getSubject();
List<String> roles = jwt.getClaim("roles").asList(String.class);
String organizationId = jwt.getClaim("organizationId").asString();

// 3. Create authentication object
UserPrincipal principal = new UserPrincipal(userId, organizationId, roles);
Authentication auth = new UsernamePasswordAuthenticationToken(
principal,
null,
principal.getAuthorities()
);

// 4. Set in security context
SecurityContextHolder.getContext().setAuthentication(auth);

} catch (JWTVerificationException e) {
log.warn("Invalid JWT token: {}", e.getMessage());
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
}

filterChain.doFilter(request, response);
}
}

Role-Based Access Control (RBAC)

Roles:

RolePermissionsUse Case
TUNER_VIEWERView recommendations (read-only)Finance team reviewing savings
TUNER_USERView + implement recommendationsEngineering implementing optimizations
TUNER_ADMINAll permissions + account managementCloud team managing Tuner configuration
ORGANIZATION_ADMINAll permissions across all accountsCTO overseeing cost optimization

Permission Matrix:

ActionViewerUserAdminOrg Admin
View recommendations
Snooze recommendations
Implement recommendations
Configure scheduler
Link AWS accounts
Manage users
Delete accounts

Authorization Enforcement:

@RestController
@RequestMapping("/api/recommendations")
public class RecommendationController {

@GetMapping
@PreAuthorize("hasAnyRole('TUNER_VIEWER', 'TUNER_USER', 'TUNER_ADMIN')")
public List<RecommendationDto> getRecommendations(
@RequestParam String accountId
) {
// Check if user has access to this account
if (!accountAccessService.hasAccess(getCurrentUser(), accountId)) {
throw new AccessDeniedException("User does not have access to account: " + accountId);
}

return recommendationService.getRecommendations(accountId);
}

@PostMapping("/{id}/implement")
@PreAuthorize("hasAnyRole('TUNER_USER', 'TUNER_ADMIN')")
public void implementRecommendation(
@PathVariable String id
) {
Recommendation recommendation = recommendationService.getById(id);

// Verify user has access to the account
if (!accountAccessService.hasAccess(getCurrentUser(), recommendation.getAccountId())) {
throw new AccessDeniedException("Cannot implement recommendation in inaccessible account");
}

recommendationService.implement(id);
}

@PostMapping("/accounts/{accountId}/link")
@PreAuthorize("hasRole('TUNER_ADMIN')")
public void linkAccount(
@PathVariable String accountId,
@RequestBody LinkAccountRequest request
) {
accountService.linkAccount(accountId, request);
}
}

Data Security

Encryption at Rest

Database Encryption:

DatabaseEncryption MethodKey Management
MongoDBAES-256 (MongoDB Enterprise)AWS KMS
MySQLAES-256 (InnoDB encryption)AWS KMS
RedisAES-256 (Redis 6+)AWS KMS
SnowflakeAES-256 (automatic)Snowflake-managed

Configuration Example (MySQL):

-- Enable encryption for MySQL
CREATE TABLE recommendations (
id VARCHAR(36) PRIMARY KEY,
account_id VARCHAR(12) NOT NULL,
resource_id VARCHAR(255) NOT NULL,
-- ...
) ENCRYPTION='Y';

Encryption in Transit

TLS 1.3 for all network communication:

# application.yml
server:
ssl:
enabled: true
protocol: TLS
enabled-protocols: TLSv1.3
ciphers:
- TLS_AES_256_GCM_SHA384
- TLS_AES_128_GCM_SHA256
key-store: classpath:keystore.p12
key-store-password: ${SSL_KEYSTORE_PASSWORD}
key-store-type: PKCS12

Database Connections:

# MongoDB
spring.data.mongodb.uri=mongodb+srv://user:pass@cluster.mongodb.net/tuner?ssl=true&tlsAllowInvalidCertificates=false

# MySQL
spring.datasource.url=jdbc:mysql://mysql-host:3306/tuner?useSSL=true&requireSSL=true&verifyServerCertificate=true

# Redis
spring.redis.ssl=true

Secrets Management

AWS Secrets Manager for all sensitive data:

@Configuration
public class SecretsConfig {

@Bean
public SecretsManagerClient secretsManagerClient() {
return SecretsManagerClient.builder()
.region(Region.US_EAST_1)
.build();
}

@Bean
public DatabaseCredentials getDatabaseCredentials() {
GetSecretValueRequest request = GetSecretValueRequest.builder()
.secretId("tuner/mysql/credentials")
.build();

GetSecretValueResponse response = secretsManagerClient().getSecretValue(request);
String secretString = response.secretString();

return new ObjectMapper().readValue(secretString, DatabaseCredentials.class);
}
}

No Hardcoded Secrets:

// ❌ BAD - Never do this
String dbPassword = "MyP@ssw0rd123!";

// ✅ GOOD - Load from environment
String dbPassword = System.getenv("DB_PASSWORD");

// ✅ BETTER - Load from Secrets Manager
String dbPassword = secretsManager.getSecret("tuner/db/password");

Data Retention & Deletion

Recommendation Data:

  • Active recommendations: Retained indefinitely
  • Implemented recommendations: Retained 12 months for ROI tracking
  • Dismissed recommendations: Retained 3 months
  • Snoozed recommendations: Retained until expiry + 30 days

User Data (GDPR Compliance):

  • Right to access: Export all user data via API
  • Right to erasure: Delete user and anonymize audit logs
  • Right to portability: JSON export of all recommendations

Data Deletion Procedure:

@Service
public class DataRetentionService {

@Scheduled(cron = "0 0 2 * * SUN") // Weekly Sunday 2 AM
public void cleanupOldData() {
Instant cutoff12Months = Instant.now().minus(365, ChronoUnit.DAYS);
Instant cutoff3Months = Instant.now().minus(90, ChronoUnit.DAYS);

// Delete implemented recommendations older than 12 months
recommendationRepository.deleteByStatusAndCreatedAtBefore(
"IMPLEMENTED",
cutoff12Months
);

// Delete dismissed recommendations older than 3 months
recommendationRepository.deleteByStatusAndCreatedAtBefore(
"DISMISSED",
cutoff3Months
);

log.info("Data retention cleanup completed");
}
}

Network Security

Web Application Firewall (WAF)

CloudFlare WAF Rules:

WAF Rules:
- SQL Injection Protection: ENABLED
- XSS Protection: ENABLED
- Rate Limiting:
- API endpoints: 100 requests/minute per IP
- Login endpoint: 5 attempts/minute per IP
- Bot Protection: ENABLED (challenge suspected bots)
- Geo-Blocking: DISABLED (global access required)

DDoS Protection

AWS Shield Standard (automatic):

  • Protection against common DDoS attacks
  • Always-on detection and mitigation
  • No additional cost

Rate Limiting (application level):

@Configuration
public class RateLimitConfig {

@Bean
public RateLimiter apiRateLimiter() {
return RateLimiter.create(100); // 100 requests/second
}
}

@RestController
@RequestMapping("/api")
public class ApiController {

@Autowired
private RateLimiter rateLimiter;

@GetMapping("/recommendations")
public ResponseEntity<?> getRecommendations() {
if (!rateLimiter.tryAcquire()) {
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS)
.body("Rate limit exceeded");
}

// Process request
}
}

API Security

CORS Configuration:

@Configuration
public class CorsConfig {

@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(Arrays.asList(
"https://app.cloudkeeper.com",
"https://demo-lens.cloudkeeper.com"
));
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
config.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type"));
config.setAllowCredentials(true);

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);

return new CorsFilter(source);
}
}

Content Security Policy (CSP):

Content-Security-Policy:
default-src 'self';
script-src 'self' 'unsafe-inline' https://cdn.cloudkeeper.com;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
img-src 'self' data: https:;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://api.cloudkeeper.com;
frame-ancestors 'none';

Compliance & Certifications

SOC 2 Type II (In Progress)

Control Objectives:

CategoryControlsStatus
SecurityAccess control, encryption, monitoring✅ Implemented
AvailabilityHigh availability, disaster recovery✅ Implemented
Processing IntegrityData validation, error handling✅ Implemented
ConfidentialityData classification, encryption✅ Implemented
PrivacyGDPR compliance, data retention✅ Implemented

GDPR Compliance

Data Subject Rights:

  1. Right to Access:

    @GetMapping("/api/users/{userId}/data-export")
    public ResponseEntity<UserDataExport> exportUserData(@PathVariable String userId) {
    return userDataService.exportAllData(userId);
    }
  2. Right to Erasure:

    @DeleteMapping("/api/users/{userId}")
    public void deleteUser(@PathVariable String userId) {
    userService.deleteUser(userId); // Anonymizes audit logs
    }
  3. Right to Portability:

    • JSON export of all user recommendations
    • CSV export of cost savings history

Audit Logging

All API Calls Logged:

@Component
public class AuditLogInterceptor implements HandlerInterceptor {

@Override
public boolean preHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handler
) {
AuditLog log = AuditLog.builder()
.userId(getCurrentUserId())
.action(request.getMethod() + " " + request.getRequestURI())
.ipAddress(request.getRemoteAddr())
.userAgent(request.getHeader("User-Agent"))
.timestamp(Instant.now())
.build();

auditLogRepository.save(log);
return true;
}
}

Audit Log Retention: 7 years (compliance requirement)


Security Best Practices

For Customers

1. Principle of Least Privilege:

✅ DO: Grant only required IAM permissions
❌ DON'T: Use AdministratorAccess policy

2. External ID Security:

✅ DO: Keep External ID secret (treat like password)
❌ DON'T: Share External ID publicly or in code repositories

3. Regular IAM Audits:

✅ DO: Review IAM role usage quarterly
✅ DO: Rotate External ID annually
❌ DON'T: Leave unused IAM roles active

4. Multi-Factor Authentication:

✅ DO: Enable MFA for all admin users
❌ DON'T: Use shared admin accounts

For Developers

1. Secure Coding:

// ✅ GOOD - Parameterized queries
String query = "SELECT * FROM recommendations WHERE account_id = ?";
jdbcTemplate.query(query, accountId);

// ❌ BAD - SQL injection vulnerability
String query = "SELECT * FROM recommendations WHERE account_id = '" + accountId + "'";

2. Input Validation:

@PostMapping("/api/accounts/{accountId}")
public void linkAccount(
@PathVariable @Pattern(regexp = "\\d{12}") String accountId, // Validate AWS account ID format
@RequestBody @Valid LinkAccountRequest request
) {
accountService.linkAccount(accountId, request);
}

3. Error Handling (Don't leak sensitive info):

// ✅ GOOD - Generic error message
throw new AccessDeniedException("Access denied");

// ❌ BAD - Leaks implementation details
throw new Exception("Database connection failed: mysql-prod-001.internal.cloudkeeper.com:3306");

Incident Response

Security Incident Procedure

1. Detection & Triage (0-15 minutes):

  • Automated alerts trigger PagerDuty
  • On-call engineer assesses severity
  • Escalate to security team if confirmed incident

2. Containment (15-60 minutes):

  • Isolate affected systems
  • Revoke compromised credentials
  • Block malicious IP addresses

3. Investigation (1-24 hours):

  • Analyze audit logs
  • Identify root cause
  • Determine scope of impact

4. Remediation (1-7 days):

  • Patch vulnerabilities
  • Restore from backups if needed
  • Update security controls

5. Post-Incident (7-30 days):

  • Document lessons learned
  • Update runbooks
  • Improve detection mechanisms

Breach Notification

Notification Timeline (GDPR requirement):

  • Discovery → 24 hours: Internal escalation
  • Discovery → 72 hours: Customer notification (if PII affected)
  • Discovery → 7 days: Public disclosure (if material breach)

Next Steps