Cloudformation-coverage-roadmap: AWS::Elasticsearch::Domain-AdvancedSecurityOptions

Created on 17 Feb 2020  路  15Comments  路  Source: aws-cloudformation/cloudformation-coverage-roadmap

2. Scope of request

Cloudformation support for the newly released Fine-Grained Access Control feature

3. Expected behavior

Requires that Node to Node encryption, encryption at rest, and HTTPS only endpoint be enabled. HTTPS only endpoint is also raised as an issue which has not yet been delivered.

AWS::Elasticsearch::Domain-DomainEndpointOptions

5. Helpful Links to speed up research and evaluation

Updated API docs at https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-configuration-api.html#es-configuration-api-datatypes-advancedsec

6. Category

Analytics

analytics

Most helpful comment

Shipped on Aug 11 :)

All 15 comments

Duplicate of #201

Support for AdvancedSecurityOptions is not a duplicate of #201, it just depends on it.

Hi, any news on the release of this feature, fine grained access is an important feature which skyrockets the use cases of Amazon Elasticsearch Service. Depending on custom resources is a decision which creates maintenance difficulties.

Three years have passed, still no actions. Any workaround for me to be able to still use Cloudformation?

@valentine-calabrio yes. You can resort to a custom resource. Here's what I have. I _think_ this is full featured (eg: All the !Ref's are accounted for in the code).

# The variables used in the elasticsearch template are named with the following convention
#   Variables that start with a c are conditions
#   Variables that start with a r are resources
#   Variables that start with a p are parameters
#   Variables that start with a o are outputs

Resources:
  rElasticsearchDomain:
    Type: AWS::Elasticsearch::Domain
    Properties:
        # SET YOUR ES PROPERTIES HERE

#############################################################################################
#
# Creates a custom ES UpdateDomainConfig so we can update ES where CloudFormation is lacking
#
# Note: Disable this once DomainEndpointOptions are part of CFN
# https://github.com/aws-cloudformation/aws-cloudformation-coverage-roadmap/issues/201
#
#############################################################################################

  rEsUpdateDomainConfig:
    Type: Custom::EsUpdatedomainConfig
    Properties:
      ServiceToken: !GetAtt rEsUpdateDomainConfigFunction.Arn
      ElasticsearchClusterName: !Ref rElasticsearchDomain
      # ElasticsearchClusterConfig: # disabled for now, may extend this function to use this at some point, notably for UltraWarm
      DomainEndpointOptions:
        EnforceHTTPS: true
        TLSSecurityPolicy: Policy-Min-TLS-1-2-2019-07

  rEsUpdateDomainConfigRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: root
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - lambda:InvokeFunction
                Resource: !Sub 'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:*'
              - Effect: Allow
                Action:
                  - es:UpdateElasticsearchDomainConfig
                Resource:
                  - !GetAtt rElasticsearchDomain.Arn

  rEsUpdateDomainConfigFunction:
    Type: AWS::Lambda::Function
    Properties:
      Description: "Updates Elasticsearch Domain Config"
      Handler: index.handler
      Runtime: python3.7
      MemorySize: 128
      Timeout: 120
      Role: !GetAtt rEsUpdateDomainConfigRole.Arn
      Code:
        ZipFile: |
          import boto3
          import cfnresponse
          import json
          import logging

          LOG = logging.getLogger()
          LOG.setLevel(logging.INFO)

          def get_es_client():
              """es client"""
              return boto3.client("es")


          def put_es_config(elasticsearch_cluster_name, domain_endpoint_options):
              get_es_client().update_elasticsearch_domain_config(
                DomainName=elasticsearch_cluster_name,
                DomainEndpointOptions=domain_endpoint_options
              )


          def handler(event, context):
              """primary lambda handler"""
              LOG.info(json.dumps(event))
              elasticsearch_cluster_name = event['ResourceProperties']['ElasticsearchClusterName']
              custom_resource_name = "{}-updatedomainconfig".format(elasticsearch_cluster_name)
              domain_endpoint_options = event['ResourceProperties']['DomainEndpointOptions']
              # set this to a bool cause it comes across as a string
              if 'EnforceHTTPS' in domain_endpoint_options:
                domain_endpoint_options['EnforceHTTPS'] = (domain_endpoint_options['EnforceHTTPS'].lower() == "true")
              try:
                  if event['RequestType'] in ["Create", "Update"]:
                      put_es_config(elasticsearch_cluster_name, domain_endpoint_options)
              except Exception as err:  # pylint: disable=broad-except,undefined-variable
                  LOG.error(err)
                  cfnresponse.send(event, context, cfnresponse.FAILED, {"Data": str(err)}, custom_resource_name)
              else:
                  cfnresponse.send(event, context, cfnresponse.SUCCESS, {"Data": "Success"}, custom_resource_name)

@natefox have you confirmed this works because Advanced Security Options like FGAC cannot be added other than at creation time. Other options like Logging Options I鈥檝e done with custom resources like you suggested but the Advanced Security ones don鈥檛 allow for Update so this won鈥檛 work.

@natefox you cannot update Advanced Securiy options after creation Time.
I have ended up creating a AWS Custom resource with Create update and delete conditions in CDK. The biggest gap is the fact that this fire and forget. I have to handle the waiting behaviour via Code pipeline to deploy dashboards and Kibana security roles to Elasticsearch after the cluster is really active.

import { Aws, Construct,CfnOutput}  from '@aws-cdk/core';
import { PolicyStatement } from '@aws-cdk/aws-iam';
import { AwsCustomResource } from '@aws-cdk/custom-resources';

export interface ElasticSearchResourceProps {
  domainName: string;
  identityPoolId: string;
  roleArn: string;
  userPoolId: string;
  kmsKeyId: string;
  stackName: string;
  accountId: string;
  masterUserARN: string;
  SubnetIds: string[];
  SecurityGroupId: string;
}

export class ElasticSearchResource extends Construct {

  public readonly searchClusterResource: AwsCustomResource;

  constructor (scope: Construct, id: string, props: ElasticSearchResourceProps) {
    super(scope, id);

    const dailyIngestedIotDataVolume=40;
    const totalDataRetention=90;
    const requiredIotDataStorage=dailyIngestedIotDataVolume*totalDataRetention*1.1*1.15;
    const instanceCount=Math.ceil(requiredIotDataStorage/1024); //4,554/1,024= 5 instances
    const requiredVolumeSize=Math.ceil(requiredIotDataStorage/instanceCount);

    var createESParams = {
      DomainName: props.domainName, 
      AccessPolicies: '{\"Version\": \"2012-10-17\",\"Statement\": [{\"Effect\": \"Allow\",\"Principal\": {\"AWS\": \"*\"},\"Action\": [\"es:*\"],\"Resource\": \"arn:aws:es:' + Aws.REGION+ ':'+ Aws.ACCOUNT_ID+ ':domain/'+props.domainName+'/*\"}]}',
      AdvancedOptions: {
        'rest.action.multi.allow_explicit_index':'true'
      },
      AdvancedSecurityOptions: {
        Enabled: true,
        InternalUserDatabaseEnabled: false,
        MasterUserOptions: {
          MasterUserARN: props.masterUserARN
        }
      },
      CognitoOptions: {
        Enabled: true,
        IdentityPoolId: props.identityPoolId,
        RoleArn: props.roleArn,
        UserPoolId: props.userPoolId
      },
      DomainEndpointOptions: {
        EnforceHTTPS: true,
        TLSSecurityPolicy: 'Policy-Min-TLS-1-2-2019-07'
      },
      EBSOptions: {
        EBSEnabled: true,
        VolumeSize: requiredVolumeSize.toString(),
        VolumeType: 'gp2'
      },
      ElasticsearchClusterConfig: {
        DedicatedMasterCount: '3',
        DedicatedMasterEnabled: true ,
        DedicatedMasterType: 'c5.large.elasticsearch',
        InstanceCount: instanceCount.toString(),
        InstanceType: 'r5.large.elasticsearch',
        WarmEnabled: false,
        ZoneAwarenessConfig: {
          AvailabilityZoneCount: '3'
        },
        ZoneAwarenessEnabled: true 
      },
      ElasticsearchVersion: '7.4',
      EncryptionAtRestOptions: {
        Enabled: true,
        KmsKeyId: props.kmsKeyId
      },
      NodeToNodeEncryptionOptions: {
        Enabled: true
      },
      VPCOptions: {
        SecurityGroupIds: [
        props.SecurityGroupId
        ],
        SubnetIds: props.SubnetIds
      }
    };

  var updateESParams={...createESParams}
  delete updateESParams.EncryptionAtRestOptions;
  delete updateESParams.ElasticsearchVersion;
  delete updateESParams.NodeToNodeEncryptionOptions;

   this.searchClusterResource=new AwsCustomResource(this,'esCreationAwsCustomResource',{
      policy:{statements:[new PolicyStatement({
        actions: ['es:createElasticsearchDomain','es:deleteElasticsearchDomain','es:updateElasticsearchDomainConfig'],
        resources:['*']
      }),
      new PolicyStatement({
        actions: ['iam:PassRole'],
        resources:[props.masterUserARN,props.roleArn]
      }),
      new PolicyStatement({
        actions: ['kms:*'],
        resources:['arn:aws:kms:'+Aws.REGION+':'+Aws.ACCOUNT_ID+':key/'+props.kmsKeyId]
      })
      ]},
      onCreate: {
        physicalResourceId:{id:props.domainName},
        service: "ES",
        action: "createElasticsearchDomain",
        parameters: createESParams
        },
        onUpdate: {
            physicalResourceId:{id:props.domainName},
            service: "ES",
            action: "updateElasticsearchDomainConfig",
            parameters: updateESParams
            },
        onDelete: {
          service: "ES",
          action: "deleteElasticsearchDomain",
          parameters: {
           DomainName: props.domainName
          }
        }
    }
    );

  }
}

You know what, I missed that AdvancedSecurityOptions isnt a dupe of #201. This code applies to DomainEndpointOptions.

Would like to see this build at we rely on CF template. We will end up custom resource only to throw it away. Any idea on timing of this ?

We need to re-write an entire existing resource ouselves as a custom resource , that creates an entire E ES because of this one missing functionality that MUST be done at CREATION time. This product can not be used by enterprise like this.

Any update on this functionality?

Please implement this. Fine grained access controls finally makes AWS Elasticsearch a real viable solution for our security minded ELK stack. However, the inability to launch it using an infrastructure as code approach goes against our principles. Making our own custom resource is just painful and a big waste of effort.

I am implementing this using Amazon CloudFormation Custom Resource at:
https://github.com/valentine-dev/aws-cloudformation.

Will notify when I finish.

Please implement this. Fine grained access controls finally makes AWS Elasticsearch a real viable solution for our security minded ELK stack. However, the inability to launch it using an infrastructure as code approach goes against our principles. Making our own custom resource is just painful and a big waste of effort.

Here is my implementation using custom resource + lambda function + node.js:
https://github.com/valentine-dev/aws-cloudformation/tree/master/custom-resource/create-aes-with-fgac

Shipped on Aug 11 :)

Was this page helpful?
0 / 5 - 0 ratings