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

Stack Policy #72

Open
1 of 7 tasks
Black742 opened this issue Feb 6, 2019 · 16 comments
Open
1 of 7 tasks

Stack Policy #72

Black742 opened this issue Feb 6, 2019 · 16 comments
Labels
effort/small Minimal effort required for implementation status/proposed Newly proposed RFC

Comments

@Black742
Copy link

Black742 commented Feb 6, 2019

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

  • Tracking Issue Created
  • RFC PR Created
  • Core Team Member Assigned
  • Initial Approval / Final Comment Period
  • Ready For Implementation
    • implementation issue 1
  • Resolved
@savvyintegrations
Copy link

savvyintegrations commented Aug 8, 2019

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
Copy link

jeshan commented Sep 6, 2019

@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
Copy link

jeshan commented Sep 6, 2019

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

@NGL321
Copy link
Contributor

NGL321 commented Oct 9, 2019

@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
Copy link

kbessas commented Dec 5, 2019

any feedback on this?

@eladb eladb transferred this issue from aws/aws-cdk Jan 23, 2020
@eladb eladb added the effort/small Minimal effort required for implementation label Jan 23, 2020
@eladb eladb changed the title Enable stack policy of a stack Stack Policy Jan 27, 2020
@eladb eladb added the status/proposed Newly proposed RFC label Jan 27, 2020
@rehanvdm
Copy link

+1

@rehanvdm
Copy link

rehanvdm commented Jun 9, 2020

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

@eladb eladb removed their assignment Jun 24, 2020
@brentryan
Copy link

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...

akash1810 added a commit to guardian/cdk that referenced this issue May 5, 2021
A resource is a raw CloudFormation item.

A construct is CDK's L1 or L2 abstraction of a resource.

A stateful resource can be defined as something that holds state.
This could be a database, a bucket, load balancer, message queue etc.

This change will, upon stack synthesis, walk the tree of resources
and log a warning for all the stateful resources we have identified.

This does mean we end up keeping a list of these resources,
which is not ideal...

The `GuStatefulMigratableConstruct` mixin performs a similar role here,
however that only operates against the constructs that exist in the library.

Ideally we'd be able to use Stack Policies to protect these resources.
However they are not currently supported in CDK.

See:
  - https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html#protected-prepare
  - https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/protect-stack-resources.html
  - aws/aws-cdk-rfcs#72
akash1810 added a commit to guardian/cdk that referenced this issue May 5, 2021
A resource is a raw CloudFormation item.

A construct is CDK's L1 or L2 abstraction of a resource.

A stateful resource can be defined as something that holds state.
This could be a database, a bucket, load balancer, message queue etc.

This change will, upon stack synthesis, walk the tree of resources
and log a warning for all the stateful resources we have identified.

This does mean we end up keeping a list of these resources,
which is not ideal...

The `GuStatefulMigratableConstruct` mixin performs a similar role here,
however that only operates against the constructs that exist in the library.

Ideally we'd be able to use Stack Policies to protect these resources.
However they are not currently supported in CDK.

See:
  - https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html#protected-prepare
  - https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/protect-stack-resources.html
  - aws/aws-cdk-rfcs#72
akash1810 added a commit to guardian/cdk that referenced this issue May 5, 2021
A resource is a raw CloudFormation item.

A construct is CDK's L1 or L2 abstraction of a resource.

A stateful resource can be defined as something that holds state.
This could be a database, a bucket, load balancer, message queue etc.

This change will, upon stack synthesis, walk the tree of resources
and log a warning for all the stateful resources we have identified.

This does mean we end up keeping a list of these resources,
which is not ideal...

The `GuStatefulMigratableConstruct` mixin performs a similar role here,
however that only operates against the constructs that exist in the library.

Ideally we'd be able to use Stack Policies to protect these resources.
However they are not currently supported in CDK.

See:
  - https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html#protected-prepare
  - https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/protect-stack-resources.html
  - aws/aws-cdk-rfcs#72
akash1810 added a commit to guardian/cdk that referenced this issue May 5, 2021
A resource is a raw CloudFormation item.

A construct is CDK's L1 or L2 abstraction of a resource.

A stateful resource can be defined as something that holds state.
This could be a database, a bucket, load balancer, message queue etc.

This change will, upon stack synthesis, walk the tree of resources
and log a warning for all the stateful resources we have identified.

This does mean we end up keeping a list of these resources,
which is not ideal...

The `GuStatefulMigratableConstruct` mixin performs a similar role here,
however that only operates against the constructs that exist in the library.

Ideally we'd be able to use Stack Policies to protect these resources.
However they are not currently supported in CDK.

See:
  - https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html#protected-prepare
  - https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/protect-stack-resources.html
  - aws/aws-cdk-rfcs#72
akash1810 added a commit to guardian/cdk that referenced this issue May 5, 2021
A resource is a raw CloudFormation item.

A construct is CDK's L1 or L2 abstraction of a resource.

A stateful resource can be defined as something that holds state.
This could be a database, a bucket, load balancer, message queue etc.

This change will, upon stack synthesis, walk the tree of resources
and log a warning for all the stateful resources we have identified.

This does mean we end up keeping a list of these resources,
which is not ideal...

The `GuStatefulMigratableConstruct` mixin performs a similar role here,
however that only operates against the constructs that exist in the library.

Ideally we'd be able to use Stack Policies to protect these resources.
However they are not currently supported in CDK.

See:
  - https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html#protected-prepare
  - https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/protect-stack-resources.html
  - aws/aws-cdk-rfcs#72
akash1810 added a commit to guardian/cdk that referenced this issue May 6, 2021
A resource is a raw CloudFormation item.

A construct is CDK's L1 or L2 abstraction of a resource.

A stateful resource can be defined as something that holds state.
This could be a database, a bucket, load balancer, message queue etc.

This change will, upon stack synthesis, walk the tree of resources
and log a warning for all the stateful resources we have identified.

This does mean we end up keeping a list of these resources,
which is not ideal...

The `GuStatefulMigratableConstruct` mixin performs a similar role here,
however that only operates against the constructs that exist in the library.

Ideally we'd be able to use Stack Policies to protect these resources.
However they are not currently supported in CDK.

See:
  - https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html#protected-prepare
  - https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/protect-stack-resources.html
  - aws/aws-cdk-rfcs#72
@automartin5000
Copy link

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
Copy link

neelbshah18 commented Jul 3, 2023

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
Copy link

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
Copy link

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
Copy link

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

@awsmjs awsmjs removed the roadmap label Dec 18, 2023
@evgenyka evgenyka added roadmap and removed status/proposed Newly proposed RFC labels Dec 18, 2023
@evgenyka evgenyka added status/proposed Newly proposed RFC and removed status/accepted labels Dec 19, 2023
@Makeshift
Copy link

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
Copy link

TinoSM commented May 16, 2024

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
Copy link

Any progress?
Do we have any ETA for this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
effort/small Minimal effort required for implementation status/proposed Newly proposed RFC
Projects
None yet
Development

No branches or pull requests