constructor()

in lib/redis-rbac-stack.ts [35:306]


  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // -----------------------------------------------------------------------------------------------------------
    // This constructor will deploy resources required to link ElastiCache Redis, with SecretsManager and IAM
    // -----------------------------------------------------------------------------------------------------------
    // Steps:
    // Step 1) create a VPC into which the ElastiCache replication group will be placed
    // Step 2) create Redis RBAC users
    //    a) one secret in Secrets Manager will be created for each
    // Step 3) create IAM roles and grant them read access to the appropriate secret
    // Step 4) create an ElastiCache Redis replication group
    // Step 5) create test functions

    let producerName = 'producer'
    let consumerName = 'consumer'
    let noAccessName = 'outsider'
    let elasticacheReplicationGroupName = 'RedisReplicationGroup'

    // ------------------------------------------------------------------------------------
    // Step 1) Create a VPC into which the ElastiCache replication group will be placed
    //     a) only private subnets will be used
    //     b) a Secrets Manager VPC endpoint will be added to allow access to Secrets Manager
    // ------------------------------------------------------------------------------------

    const vpc = new ec2.Vpc(this, "Vpc", {
      subnetConfiguration: [
        {
          cidrMask: 24,
          name: 'Isolated',
          subnetType: ec2.SubnetType.ISOLATED,
        }
      ]
    });

    const flowLog = new ec2.FlowLog(this, 'VpcFlowLog', {
      resourceType: ec2.FlowLogResourceType.fromVpc(vpc)
    })

    const lambdaSecurityGroup = new ec2.SecurityGroup(this, 'LambdaSG', {
      vpc: vpc,
      description: 'SecurityGroup into which Lambdas will be deployed',
      allowAllOutbound: false
    });

    const secretsManagerVpcEndpointSecurityGroup = new ec2.SecurityGroup(this, 'SecretsManagerVPCeSG', {
      vpc: vpc,
      description: 'SecurityGroup for the VPC Endpoint Secrets Manager',
      allowAllOutbound: false,

    });

    secretsManagerVpcEndpointSecurityGroup.connections.allowFrom(lambdaSecurityGroup, ec2.Port.tcp(443));

    const secretsManagerEndpoint = vpc.addInterfaceEndpoint('SecretsManagerEndpoint', {
      service: ec2.InterfaceVpcEndpointAwsService.SECRETS_MANAGER,
      subnets: {
        subnetType: ec2.SubnetType.ISOLATED
      },
      open: false,
      securityGroups: [secretsManagerVpcEndpointSecurityGroup]
    });

    const ecSecurityGroup = new ec2.SecurityGroup(this, 'ElastiCacheSG', {
      vpc: vpc,
      description: 'SecurityGroup associated with the ElastiCache Redis Cluster',
      allowAllOutbound: false,
    });

    ecSecurityGroup.connections.allowFrom(lambdaSecurityGroup, ec2.Port.tcp(6379), 'Redis ingress 6379');
    ecSecurityGroup.connections.allowTo(lambdaSecurityGroup, ec2.Port.tcp(6379), 'Redis egress 6379');

    // ------------------------------------------------------------------------------------
    // Step 2) Create IAM roles
    //     a) each IAM role will be assumed by a lambda function
    //     b) each IAM role will be granted read and decrypt permissions to a matching secret
    // ------------------------------------------------------------------------------------
    const producerRole = new iam.Role(this, producerName+'Role', {
      assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
      description: 'Role to be assumed by producer lambda',
    });

    producerRole.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole"));
    producerRole.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaVPCAccessExecutionRole"));


    const consumerRole = new iam.Role(this, consumerName+'Role', {
      assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
      description: 'Role to be assumed by mock application lambda',
    });
    consumerRole.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole"));
    consumerRole.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaVPCAccessExecutionRole"));


    const noAccessRole = new iam.Role(this, noAccessName+'Role', {
      assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
      description: 'Role to be assumed by mock application lambda',
    });
    noAccessRole.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole"));
    noAccessRole.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaVPCAccessExecutionRole"));


    // ------------------------------------------------------------------------------------
    // Step 3) Create Redis RBAC users
    //     a) access strings will dictate operations that can be performed
    //     b) RedisRbacUser is a class defined in redis-rbac-secret-manager.ts
    //     c) RedisRbacUser is composed of an AWS::ElastiCache::User and a Secret
    // ------------------------------------------------------------------------------------
    const commonKmsKey = new kms.Key(this, 'commonCredentialKey', {
      alias: 'redisRbacUser/common',
      enableKeyRotation: true
    });

    const producerRbacUser = new RedisRbacUser(this, producerName+'RBAC', {
      redisUserName: producerName,
      redisUserId: producerName,
      accessString: 'on ~* -@all +SET',
      kmsKey: commonKmsKey,
      principals: [producerRole]
    });

    const consumerRbacUser = new RedisRbacUser(this, consumerName+'RBAC', {
      redisUserName: 'consumer',
      redisUserId: 'consumer',
      accessString: 'on ~* -@all +GET',
      kmsKey: commonKmsKey,
      principals: [consumerRole]
    });

    const groupDefaultRbacUser = new RedisRbacUser(this, "groupDefaultUser"+'RBAC', {
      redisUserName: 'default',
      redisUserId: 'groupdefaultuser',
      kmsKey: commonKmsKey
    });

    // Create RBAC user group
    const mockAppUserGroup = new elasticache.CfnUserGroup(this, 'mockAppUserGroup', {
      engine: 'redis',
      userGroupId: 'mock-app-user-group',
      userIds: [producerRbacUser.getUserId(), groupDefaultRbacUser.getUserId(), consumerRbacUser.getUserId()]
    })

    mockAppUserGroup.node.addDependency(producerRbacUser);
    mockAppUserGroup.node.addDependency(groupDefaultRbacUser);
    mockAppUserGroup.node.addDependency(consumerRbacUser);


    // ------------------------------------------------------------------------------------
    // Step 4) Create an ElastiCache Redis Replication group and associate the RBAC user group
    //     a) an ElastiCache subnet group will be created
    //     b) the ElastiCache replication group will be associated with the RBAC user group
    // ------------------------------------------------------------------------------------

    let isolatedSubnets: string[] = []

    vpc.isolatedSubnets.forEach(function(value){
      isolatedSubnets.push(value.subnetId)
    });

    const ecSubnetGroup = new elasticache.CfnSubnetGroup(this, 'ElastiCacheSubnetGroup', {
      description: 'Elasticache Subnet Group',
      subnetIds: isolatedSubnets,
      cacheSubnetGroupName: 'RedisSubnetGroup'
    });

    const elastiCacheKmsKey = new kms.Key(this, 'kmsForSecret', {
      alias: 'redisReplicationGroup/'+elasticacheReplicationGroupName,
      enableKeyRotation: true
    });

    // elastiCacheKmsKey.grantEncrypt(producerRole);
    // elastiCacheKmsKey.grantDecrypt(consumerRole);

    const ecClusterReplicationGroup = new elasticache.CfnReplicationGroup(this, elasticacheReplicationGroupName, {
      replicationGroupDescription: 'RedisReplicationGroup-RBAC-Demo',
      atRestEncryptionEnabled: true,
      multiAzEnabled: true,
      cacheNodeType: 'cache.m6g.large',
      cacheSubnetGroupName: ecSubnetGroup.cacheSubnetGroupName,
      engine: "Redis",
      engineVersion: '6.x',
      numNodeGroups: 1,
      kmsKeyId: elastiCacheKmsKey.keyId,
      replicasPerNodeGroup: 1,
      securityGroupIds: [ecSecurityGroup.securityGroupId],
      transitEncryptionEnabled: true,
      userGroupIds: [mockAppUserGroup.userGroupId]
    })

    ecClusterReplicationGroup.node.addDependency(ecSubnetGroup)
    ecClusterReplicationGroup.node.addDependency(mockAppUserGroup)

    // ------------------------------------------------------------------------------------
    // Step 5) Create test functions
    //     a) one producer
    //     b) one consumer
    //     c) one that cannot access Redis
    // ------------------------------------------------------------------------------------
    const redisPyLayer = new lambda.LayerVersion(this, 'redispy_Layer', {
      code: lambda.Code.fromAsset(path.join(__dirname, 'lambda/lib/redis_module/redis_py.zip')),
      compatibleRuntimes: [lambda.Runtime.PYTHON_3_8, lambda.Runtime.PYTHON_3_7, lambda.Runtime.PYTHON_3_6],
      description: 'A layer that contains the redispy module',
      license: 'MIT License'
    });


    const producerLambda = new lambda.Function(this, producerName+'Fn', {
      runtime: lambda.Runtime.PYTHON_3_7,
      handler: 'redis_connect.lambda_handler',
      code: lambda.Code.fromAsset(path.join(__dirname, 'lambda/mock_app.zip')),
      layers: [redisPyLayer],
      role: producerRole,
      vpc: vpc,
      vpcSubnets: {subnetType: ec2.SubnetType.ISOLATED},
      securityGroups: [lambdaSecurityGroup],
      environment: {
        redis_endpoint: ecClusterReplicationGroup.attrPrimaryEndPointAddress,
        redis_port: ecClusterReplicationGroup.attrPrimaryEndPointPort,
        secret_arn: producerRbacUser.getSecret().secretArn,
      }
    });

    producerLambda.node.addDependency(redisPyLayer);
    producerLambda.node.addDependency(ecClusterReplicationGroup);
    producerLambda.node.addDependency(vpc);
    producerLambda.node.addDependency(producerRole);

    // Create a function that can only read from Redis
    const consumerFunction = new lambda.Function(this, consumerName+'Fn', {
      runtime: lambda.Runtime.PYTHON_3_7,
      handler: 'redis_connect.lambda_handler',
      code: lambda.Code.fromAsset(path.join(__dirname, 'lambda/mock_app.zip')),
      layers: [redisPyLayer],
      role: consumerRole,
      vpc: vpc,
      vpcSubnets: {subnetType: ec2.SubnetType.ISOLATED},
      securityGroups: [lambdaSecurityGroup],
      environment: {
        redis_endpoint: ecClusterReplicationGroup.attrPrimaryEndPointAddress,
        redis_port: ecClusterReplicationGroup.attrPrimaryEndPointPort,
        secret_arn: consumerRbacUser.getSecret().secretArn,
      }
    });

    consumerFunction.node.addDependency(redisPyLayer);
    consumerFunction.node.addDependency(ecClusterReplicationGroup);
    consumerFunction.node.addDependency(vpc);
    consumerFunction.node.addDependency(consumerRole);

    // Create a function that cannot access Redis
    const noAccessFunction = new lambda.Function(this, noAccessName+'Fn', {
      runtime: lambda.Runtime.PYTHON_3_7,
      handler: 'redis_connect.lambda_handler',
      code: lambda.Code.fromAsset(path.join(__dirname, 'lambda/mock_app.zip')),
      layers: [redisPyLayer],
      role: consumerRole,
      vpc: vpc,
      vpcSubnets: {subnetType: ec2.SubnetType.ISOLATED},
      securityGroups: [lambdaSecurityGroup],
      environment: {
        redis_endpoint: ecClusterReplicationGroup.attrPrimaryEndPointAddress,
        redis_port: ecClusterReplicationGroup.attrPrimaryEndPointPort,
        secret_arn: producerRbacUser.getSecret().secretArn,
      }
    });

    noAccessFunction.node.addDependency(redisPyLayer);
    noAccessFunction.node.addDependency(ecClusterReplicationGroup);
    noAccessFunction.node.addDependency(vpc);
    noAccessFunction.node.addDependency(noAccessRole);

  }