aws / aws-cdk-rfcs

RFCs for the AWS CDK
Apache License 2.0
534 stars 83 forks source link

Stack Policy #72

Open Black742 opened 5 years ago

Black742 commented 5 years ago
PR Champion
#

Description

Cloudformation has a feature stack policy which prevent updates for the resource mentioned as a json. Can we make support this feature in cdk?

https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/protect-stack-resources.html

Progress

savvyintegrations commented 5 years ago

For now what I do is the following (for protecting my UserPool) after calling myApp.synth() in cloudformation.ts:

    const cf = new CloudFormation();
    await cf.setStackPolicy({
        StackName: myApp.cognitoStack.stackName,
        StackPolicyBody: JSON.stringify({
            Statement: [
                {
                  Effect: 'Deny',
                  Principal: '*',
                  Action: 'Update:*',
                  Resource: '*',
                  Condition: {
                    StringEquals: {
                      ResourceType: ['AWS::Cognito::UserPool'],
                    },
                  },
                },
                {
                  Effect: 'Allow',
                  Principal: '*',
                  Action: 'Update:*',
                  Resource: '*',
                },
            ]}),
        }).promise();
jeshan commented 5 years ago

@savvyintegrations Thanks for pointing us in the right direction. I think the above assumes default region and account. I think it can be improved by constructing the CF client the same way as cdk would.

const { SDK } = require('aws-cdk/lib/api/util/sdk');
const cf = new SDK({profile: yourProfile}.cloudFormation(yourAccount, yourRegion);

This way, the cf client will be loaded with the appropriate credentials.

jeshan commented 5 years ago

Also, note that this policy application will be run on any cdk command since it's outside the cdk lifeycle.

NGL321 commented 5 years ago

@savvyintegrations, thank you for providing a workaround that is a really helpful addition!

I just wanted to update the issue to assure that this is still on our radar!

😸

kbessas commented 4 years ago

any feedback on this?

rehanvdm commented 4 years ago

+1

rehanvdm commented 4 years ago

It has been more than a year since this ticket opened, I took @savvyintegrations AWS call and threw it into an AwsCustomResource. Then deploy it as a second stack that depends on the "main" stack, this is so that the Policy stack can get the ARN from the main stack.

Contents of the PolicyStack (/lib/cdk-stack-policy.ts):

import * as cdk from '@aws-cdk/core';
import * as cr from '@aws-cdk/custom-resources';
import {RetentionDays} from "@aws-cdk/aws-logs";

export class CdkStackPolicy extends cdk.Stack
{
    constructor(scope: cdk.Construct, id: string, props: cdk.StackProps, forStackName: string, forStackId: string, policy: string)
    {
        super(scope, id, props);

        new cr.AwsCustomResource(this, "StackPolicy-"+forStackName, {
            timeout: cdk.Duration.minutes(2),
            logRetention: RetentionDays.ONE_WEEK,
            onCreate: {
                service: 'CloudFormation',
                action: 'setStackPolicy',
                parameters: {
                    StackName: forStackName,
                    StackPolicyBody: policy,
                },
                physicalResourceId: cr.PhysicalResourceId.of("StackPolicy-"+forStackName)
            },
            onUpdate: {
                service: 'CloudFormation',
                action: 'setStackPolicy',
                parameters: {
                    StackName: forStackName,
                    StackPolicyBody: policy,
                },
                physicalResourceId: cr.PhysicalResourceId.of("StackPolicy"+forStackName)
            },
            policy: cr.AwsCustomResourcePolicy.fromSdkCalls({resources: [forStackId]})
        });

    }
}

Then deployed as secondary stack next to the "main" stack (/bin/cdk.ts):

import * as cdk from '@aws-cdk/core';
import { CdkStack } from '../lib/cdk-stack';
import { CdkStackPolicy } from '../lib/cdk-stack-policy';

// Initialize the CDK App
const app = new cdk.App();

    ... ... ...

let cdkStack = new CdkStack(app, applicationName, {
                    stackName: applicationName,
                    env: {
                        account: config.awsAccountID,
                        region: config.awsProfileRegion
                    }
                }, config);

let cdkPolicyStack = new CdkStackPolicy(app, applicationName+"-stackpolicy", {
        stackName: applicationName+"-stackpolicy",
        env: {
            account: config.awsAccountID,
            region: config.awsProfileRegion
        }
    }, applicationName, cdkStack.stackId,
    JSON.stringify({
        "Statement": [
            {
                "Effect": "Deny",
                "Action": ["Update:Replace", "Update:Delete"],
                "Principal": "*",
                "Resource": "*",
                "Condition": {
                    "StringEquals": {
                        "ResourceType": ["AWS::DynamoDB::Table", "AWS::ApiGateway::RestApi"]
                    }
                }
            },
            {
                "Effect": "Allow",
                "Action": "Update:*",
                "Principal": "*",
                "Resource": "*"
            }
        ]
    }));
cdkPolicyStack.addDependency(cdkStack);
brentryan commented 3 years ago

Is any work being done to allow --stack-policy-during-update-body option on cdk deploy? I'd rather have this then need to jump through hoops with synth, changeset, execute commands...

automartin5000 commented 2 years ago

I'm wondering why after all this time, this hasn't been prioritized? Given the nature of the CDK and how easy it is for developers to make unintended changes to code that could cause a stateful resource to be replaced, it seems like allowing stack policies to be set would be a top priority (and seemingly not a ton of work).

neelbshah18 commented 1 year ago

based on the above comments I implemented the stack policy

 const cf = new CloudFormation();
    cf.setStackPolicy({
      StackName: this.stackName,
      StackPolicyBody: JSON.stringify({
        Statement: [
          {
            "Effect" : "Deny",
            "Principal" : "*",
            "Action" : "Update:*",
            "Resource" : "*",
          },
        ],
      }),
    });

But when I deploy my stack and check the Cloud formation console I could not see the stack_policy. do I need to do something else along with this? Sorry if this sounds stupid I'm new with this any help is appreciated

uncledru commented 1 year ago

I was able to accomplish this using a lambda function in the same stack + EventBridge event rule:

export interface StackPolicyProps {
    policy?: string;
}
export class StackPolicyFunction extends Construct {
    constructor(scope: Construct, id: string, props?: StackPolicyProps) {
        super(scope, id);

        const { stackName, stackId } = Stack.of(scope);
        // define a lambda function triggered by stack create and update completed
        const fn = new NodejsFunction(this, "handler", {
            runtime: Runtime.NODEJS_16_X,
            handler: "index.handler",
            architecture: Architecture.ARM_64,
            environment: {
                STACK_NAME: stackName,
                POLICY: props?.policy || DEFAULT_POLICY,
            },
            initialPolicy: [
                new PolicyStatement({
                    effect: Effect.ALLOW,
                    actions: ["cloudformation:SetStackPolicy"],
                    resources: [stackId],
                }),
            ],
        });

        new Rule(this, "CloudFormationRule", {
            description: "Trigger a lambda function on CloudFormation status updates.",
            enabled: true,
            // eventBus: "default" // by default associates with the accounts default eventbus
            eventPattern: {
                source: ["aws.cloudformation"],
                detailType: ["CloudFormation Stack Status Change"],
                detail: {
                    // we cannot set the policy while the stack is creating or updating
                    "status-details": { status: ["CREATE_COMPLETE", "UPDATE_COMPLETE"] },
                },
            },
            targets: [new LambdaFunction(fn.currentVersion, {})],
        });
    }
}

// https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/protect-stack-resources.html#stack-policy-reference
const DEFAULT_POLICY = JSON.stringify({
    Statement: [
        // deny any stack updates the attempt to delete or replace
        // resources of type AWS::DynamoDB::Table
        {
            Effect: "Deny",
            Action: ["Update:Replace", "Update:Delete"],
            Principal: "*",
            Resource: "*",
            Condition: {
                StringEquals: {
                    // Be default we prevent any dynamodb tables and s3 buckets from being replaced
                    ResourceType: ["AWS::DynamoDB::Table", "AWS::S3::Bucket"],
                },
            },
        },
        // allow updates to all other resources
        {
            Effect: "Allow",
            Action: "Update:*",
            Principal: "*",
            Resource: "*",
        },
    ],
});

Handler:

export interface CloudFormation {
    version: string;
    source: string;
    account: string;
    id: string;
    region: string;
    "detail-type": string;
    time: string;
    resources: string[];
    detail: Detail;
}

export interface Detail {
    "stack-id": string;
    "status-details": StatusDetails;
}

export interface StatusDetails {
    status: string;
    "status-reason": string;
}

const STACK_NAME = process.env.STACK_NAME as string;
const POLICY = process.env.POLICY as string;

export const handler = async (event: EventBridgeEvent<"CloudFormation", Detail>) => {
    console.log(JSON.stringify({ event }));
    // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-describing-stacks.html#cli-stack-status-codes
    const status = event["detail"]["status-details"]["status"];
    const stackArn = event["resources"];
    if (
        // sanity check to avoid failures in case the rule is changed
        (status === "CREATE_COMPLETE" || status === "UPDATE_COMPLETE") &&
        // only apply policy to stacks that create this resource
        stackArn.findIndex(arn => arn.includes(STACK_NAME)) != -1
    ) {
        console.log("Updating stack policy...");
        const client = new CloudFormationClient({});
        const command = new SetStackPolicyCommand({
            StackName: STACK_NAME,
            StackPolicyBody: POLICY,
        });
        const result = await client.send(command);
        console.log(JSON.stringify({ result }));
    }
};
automartin5000 commented 1 year ago

I was able to accomplish this using a lambda function in the same stack + EventBridge event rule:


export interface StackPolicyProps {

    policy?: string;

}

export class StackPolicyFunction extends Construct {

    constructor(scope: Construct, id: string, props?: StackPolicyProps) {

        super(scope, id);

        const { stackName, stackId } = Stack.of(scope);

        // define a lambda function triggered by stack create and update completed

        const fn = new NodejsFunction(this, "handler", {

            runtime: Runtime.NODEJS_16_X,

            handler: "index.handler",

            architecture: Architecture.ARM_64,

            environment: {

                STACK_NAME: stackName,

                POLICY: props?.policy || DEFAULT_POLICY,

            },

            initialPolicy: [

                new PolicyStatement({

                    effect: Effect.ALLOW,

                    actions: ["cloudformation:SetStackPolicy"],

                    resources: [stackId],

                }),

            ],

        });

        new Rule(this, "CloudFormationRule", {

            description: "Trigger a lambda function on CloudFormation status updates.",

            enabled: true,

            // eventBus: "default" // by default associates with the accounts default eventbus

            eventPattern: {

                source: ["aws.cloudformation"],

                detailType: ["CloudFormation Stack Status Change"],

                detail: {

                    // we cannot set the policy while the stack is creating or updating

                    "status-details": { status: ["CREATE_COMPLETE", "UPDATE_COMPLETE"] },

                },

            },

            targets: [new LambdaFunction(fn.currentVersion, {})],

        });

    }

}

// https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/protect-stack-resources.html#stack-policy-reference

const DEFAULT_POLICY = JSON.stringify({

    Statement: [

        // deny any stack updates the attempt to delete or replace

        // resources of type AWS::DynamoDB::Table

        {

            Effect: "Deny",

            Action: ["Update:Replace", "Update:Delete"],

            Principal: "*",

            Resource: "*",

            Condition: {

                StringEquals: {

                    // Be default we prevent any dynamodb tables and s3 buckets from being replaced

                    ResourceType: ["AWS::DynamoDB::Table", "AWS::S3::Bucket"],

                },

            },

        },

        // allow updates to all other resources

        {

            Effect: "Allow",

            Action: "Update:*",

            Principal: "*",

            Resource: "*",

        },

    ],

});

Handler:


export interface CloudFormation {

    version: string;

    source: string;

    account: string;

    id: string;

    region: string;

    "detail-type": string;

    time: string;

    resources: string[];

    detail: Detail;

}

export interface Detail {

    "stack-id": string;

    "status-details": StatusDetails;

}

export interface StatusDetails {

    status: string;

    "status-reason": string;

}

const STACK_NAME = process.env.STACK_NAME as string;

const POLICY = process.env.POLICY as string;

export const handler = async (event: EventBridgeEvent<"CloudFormation", Detail>) => {

    console.log(JSON.stringify({ event }));

    // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-describing-stacks.html#cli-stack-status-codes

    const status = event["detail"]["status-details"]["status"];

    const stackArn = event["resources"];

    if (

        // sanity check to avoid failures in case the rule is changed

        (status === "CREATE_COMPLETE" || status === "UPDATE_COMPLETE") &&

        // only apply policy to stacks that create this resource

        stackArn.findIndex(arn => arn.includes(STACK_NAME)) != -1

    ) {

        console.log("Updating stack policy...");

        const client = new CloudFormationClient({});

        const command = new SetStackPolicyCommand({

            StackName: STACK_NAME,

            StackPolicyBody: POLICY,

        });

        const result = await client.send(command);

        console.log(JSON.stringify({ result }));

    }

};

I solved this the exact same way but added the use of StackSets to enforce this globally across our org.

Still think it's something we should be able to configure within the CDK.

alexbaileyuk commented 1 year ago

Kind of crazy we have to have such a huge workaround for something which is so core to Cloudformation and protecting resources!

Makeshift commented 8 months ago

It's sometimes very difficult to ascertain whether CFN is going to replace a resource or not with just a changeset ("conditional", thanks cfn), and stack policies are essential for any kind of automated deployments via CI/CD. First-party support would give a lot more credence to CDK being ready for production use.

TinoSM commented 4 months ago

Just as an idea, for people using AWS CDK, cant you create a test which ensures the ID of the element doesn't change? (or whatever CDK uses to detect drift)

this is what I did (python CDK), in my case I'm deploying some lambdas which will be later on modified by the CICD pipelines (we dont want to create the lambdas, APIGW... in the pipelines themselves). I also have something to ensure the dummy code I publish in the initial lambda does not get modified

`

# Synthesize the stack to generate a CloudFormation template
template = Template.from_stack(target)
previous_lambda_ids = ["xxbda53669C62", 
                       "xxxda5B00F081"]

# Extract the ID of the Lambda function from the CloudFormation template
found_lambdas = (template.find_resources(type="AWS::Lambda::Function", props={}))

assert len(found_lambdas) == len(previous_lambda_ids), ( 
    "There is more/less lambdas that the ones in previous_lambda_ids, "
    "when adding a new lambda make sure you add its physical id "
    f"to previous_lambda_ids. Lambdas found {found_lambdas.keys()}"
    f"Missing lambdas {set(found_lambdas.keys()).difference(previous_lambda_ids)}"
)
for id in previous_lambda_ids:
    assert id in found_lambdas.keys(), ("Lambda physical id not found, this means you modified a lambda, "
    "this will redeploy it and cause the code deployed by CICD to break,"
    "revert it or make sure you redeploy the code just after this gets deployed. "
    f"Missing lambdas {set(found_lambdas.keys()).difference(previous_lambda_ids)}")

`

emportella commented 4 months ago

Any progress? Do we have any ETA for this?

masso00 commented 2 months ago

Adding my name to the pile of customers who need this feature. It's becoming increasingly dangerous running CDK deployements in production pipelines without this. It's been over 5 years, I think this feature is way overdue.

SergioTrilogy commented 2 months ago

How can such a core feature be sitting here waiting to be done since 2019 escapes my comprehension. I mean, why would we want to have a simple properly exposed and integrated way to ensure we don't delete things we don't want to delete by mistake, because of wrongly changing the logical id of something for example, right? 😆

emportella commented 2 months ago

Terraform has the Lifecycle Ignore changes... Come on that is essential.

automartin5000 commented 2 months ago

I think it's prudent to tone down a bit of the hyperbole here. Just because this feature isn't baked into cdk deploy, doesn't mean it's impossible (or even that complex) to implement. It's just something that "should" be baked in.

SergioTrilogy commented 2 months ago

Given that it's been 5 years since this was brought up I think everyone has the right to be, at least, quite expectant about it. Specially given this repo has barely 15 open issues counting this, which makes it feel like it's being deliberately being ignored to be honest.