Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AWS::Elasticsearch::Domain-AdvancedSecurityOptions #384

Closed
kenshinuesugi opened this issue Feb 17, 2020 · 15 comments
Closed

AWS::Elasticsearch::Domain-AdvancedSecurityOptions #384

kenshinuesugi opened this issue Feb 17, 2020 · 15 comments
Labels
analytics Athena, EMR, CloudSearch, Elasticsearch, Kinesis, etc.

Comments

@kenshinuesugi
Copy link

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

@luiseduardocolon luiseduardocolon added the analytics Athena, EMR, CloudSearch, Elasticsearch, Kinesis, etc. label Feb 18, 2020
@lawrencepit
Copy link

Duplicate of #201

@jdub
Copy link

jdub commented Mar 14, 2020

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

@awssenera
Copy link

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.

@valentine-calabrio
Copy link

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

@natefox
Copy link

natefox commented Apr 16, 2020

@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)

@cdenneen
Copy link

@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’ve done with custom resources like you suggested but the Advanced Security ones don’t allow for Update so this won’t work.

@awssenera
Copy link

awssenera commented Apr 17, 2020

@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
          }
        }
    }
    );
    
  }
}

@natefox
Copy link

natefox commented Apr 17, 2020

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

@vptech20nn
Copy link

vptech20nn commented May 1, 2020

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 ?

@crater9893
Copy link

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.

@nkhine
Copy link

nkhine commented Jun 3, 2020

Any update on this functionality?

@drock
Copy link

drock commented Jun 5, 2020

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.

@valentine-dev
Copy link

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

Will notify when I finish.

@valentine-dev
Copy link

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

@luiseduardocolon
Copy link
Contributor

Shipped on Aug 11 :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
analytics Athena, EMR, CloudSearch, Elasticsearch, Kinesis, etc.
Projects
None yet
Development

No branches or pull requests