PageSourceSearch

https://www.openpolicyagent.org/assets/js/4f579d4f.6feadfb3.js

js openpolicyagent.org collected 2026-09-24 08:27:56 UTC 28,885 bytes, 1 lines download raw bytes

1"use strict";(self.webpackChunkopa_website=self.webpackChunkopa_website||[]).push([[4944],{28453:(e,n,o)=>{o.d(n,{R:()=>a,x:()=>r});var t=o(96540);const i={},s=t.createContext(i);function a(e){const n=t.useContext(s);return t.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function r(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:a(e.components),t.createElement(s.Provider,{value:n},e.children)}},95870:(e,n,o)=>{o.r(n),o.d(n,{assets:()=>c,contentTitle:()=>r,default:()=>h,frontMatter:()=>a,metadata:()=>t,toc:()=>l});const t=JSON.parse('{"id":"aws-cloudformation-hooks","title":"AWS CloudFormation Hooks","description":"AWS CloudFormation Hooks allows users to","source":"@site/docs/aws-cloudformation-hooks.md","sourceDirName":".","slug":"/aws-cloudformation-hooks","permalink":"/docs/aws-cloudformation-hooks","draft":false,"unlisted":false,"tags":[],"version":"current","frontMatter":{"title":"AWS CloudFormation Hooks"},"sidebar":"docsSidebar","previous":{"title":"PR Check Policies","permalink":"/docs/cicd/pr-checks"},"next":{"title":"Docker","permalink":"/docs/docker-authorization"}}');var i=o(74848),s=o(28453);const a={title:"AWS CloudFormation Hooks"},r=void 0,c={},l=[{value:"Goals",id:"goals",level:2},{value:"Prerequisites",id:"prerequisites",level:2},{value:"Steps",id:"steps",level:2},{value:"1. Install the CloudFormation Hook",id:"1-install-the-cloudformation-hook",level:3},{value:"2. Configure the OPA AWS CloudFormation Hook",id:"2-configure-the-opa-aws-cloudformation-hook",level:3},{value:"3. Learn the Domain",id:"3-learn-the-domain",level:3},{value:"AWS CloudFormation Templates",id:"aws-cloudformation-templates",level:4},{value:"Input and Response Format",id:"input-and-response-format",level:4},{value:"4. Write a CloudFormation Hook Policy",id:"4-write-a-cloudformation-hook-policy",level:3},{value:"5. Policy Enforcement Testing",id:"5-policy-enforcement-testing",level:3},{value:"Further Improvements",id:"further-improvements",level:2},{value:"Dynamic Policy Composition",id:"dynamic-policy-composition",level:3},{value:"OPA Authentication via AWS Secrets",id:"opa-authentication-via-aws-secrets",level:3},{value:"OPA Configuration",id:"opa-configuration",level:4},{value:"OPA AWS CloudFormation Hook Configuration",id:"opa-aws-cloudformation-hook-configuration",level:4}];function d(e){const n={a:"a",admonition:"admonition",code:"code",em:"em",h2:"h2",h3:"h3",h4:"h4",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,s.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.a,{href:"https://docs.aws.amazon.com/cloudformation-cli/latest/hooks-userguide/what-is-cloudformation-hooks.html",children:"AWS CloudFormation Hooks"})," allows users to\nverify AWS infrastructure components defined in AWS CloudFormation\n",(0,i.jsx)(n.a,{href:"https://aws.amazon.com/cloudformation/resources/templates/",children:"templates"}),", like S3 Buckets or EC2 instances, prior to\ndeployment. This is done via ",(0,i.jsx)(n.strong,{children:"hooks"}),". Hooks are composed of custom code running in an AWS Lambda function, which is\ninvoked before a resource is created, updated or deleted."]}),"\n",(0,i.jsxs)(n.p,{children:["AWS currently supports hooks written in either Java or Python, and provides a\n",(0,i.jsx)(n.a,{href:"https://github.com/aws-cloudformation/aws-cloudformation-samples",children:"sample repository"}),", which includes example hooks\nwritten in both languages. Since OPA is preferred for this purpose, some code is needed to process the requests\nhandled by the hook and send them forward to OPA for policy decisions via its\n",(0,i.jsx)(n.a,{href:"https://www.openpolicyagent.org/docs/rest-api",children:"REST API"})," using\nthe ",(0,i.jsx)(n.a,{href:"https://github.com/StyraOSS/opa-aws-cloudformation-hook",children:"OPA AWS CloudFormation Hook"}),"."]}),"\n",(0,i.jsx)(n.h2,{id:"goals",children:"Goals"}),"\n",(0,i.jsx)(n.p,{children:"This tutorial shows how to deploy an AWS CloudFormation Hook that forwards requests to OPA for policy decisions,\nallowing policy to determine whether a request to create, update or delete a resource should be\nallowed or denied. This tutorial covers authoring policies that take the input structure of CloudFormation Templates into\naccount, and some special considerations to be aware of in this environment."}),"\n",(0,i.jsx)(n.p,{children:"In addition, this tutorial shows how dynamic policy composition can group and structure policies in a\nway that follows the domain to which they apply."}),"\n",(0,i.jsx)(n.h2,{id:"prerequisites",children:"Prerequisites"}),"\n",(0,i.jsx)(n.p,{children:"In order to complete this tutorial, the following prerequisites needs to be met:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"An AWS account, with permissions to deploy resources via AWS CloudFormation, and valid credentials available to the CLI commands"}),"\n",(0,i.jsxs)(n.li,{children:["The ",(0,i.jsx)(n.a,{href:"https://aws.amazon.com/cli/",children:"AWS CLI"})," (",(0,i.jsx)(n.code,{children:"aws"}),") tool"]}),"\n",(0,i.jsxs)(n.li,{children:["The ",(0,i.jsx)(n.a,{href:"https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/what-is-cloudformation-cli.html",children:"CloudFormation CLI"})," (",(0,i.jsx)(n.code,{children:"cfn"}),") tool"]}),"\n",(0,i.jsx)(n.li,{children:"Docker"}),"\n",(0,i.jsxs)(n.li,{children:["OPA server running at an endpoint reachable by the AWS Lambda function, either within the same AWS environment, or\nelsewhere. While developing your CloudFormation policies, a good option is to run OPA locally, but exposed to the\npublic via a service like ",(0,i.jsx)(n.a,{href:"https://tunnelmole.com/docs/",children:"tunnelmole"}),", an open source tunneling tool or ",(0,i.jsx)(n.a,{href:"https://ngrok.com/",children:"ngrok"}),",\na popular closed source tunneling tool."]}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"steps",children:"Steps"}),"\n",(0,i.jsx)(n.h3,{id:"1-install-the-cloudformation-hook",children:"1. Install the CloudFormation Hook"}),"\n",(0,i.jsx)(n.p,{children:"To start out, clone the OPA AWS CloudFormation Hook repository:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-shell",children:"git clone https://github.com/StyraOSS/opa-aws-cloudformation-hook.git\ncd opa-aws-cloudformation-hook\n"})}),"\n",(0,i.jsxs)(n.p,{children:["To install (but not activate) the hook provided in this repository into your AW
1S account, cd into the ",(0,i.jsx)(n.code,{children:"hooks"})," directory\nand run:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-shell",children:"cd hooks\ncfn submit --set-default\n"})}),"\n",(0,i.jsx)(n.p,{children:"When the command above is finished (this may take several minutes), you should see output similar to this:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"Successfully submitted type. Waiting for registration with token '16697881-de36-45b8-8bc4-d9744431fa82' to complete.\nRegistration complete.\n{\n  'ProgressStatus': 'COMPLETE',\n  'Description': 'Deployment is currently in DEPLOY_STAGE of status COMPLETED',\n  'TypeArn': 'arn:aws:cloudformation:eu-north-1:687803501377:type/hook/Styra-OPA-Hook',\n  ...\n}\n"})}),"\n",(0,i.jsx)(n.h3,{id:"2-configure-the-opa-aws-cloudformation-hook",children:"2. Configure the OPA AWS CloudFormation Hook"}),"\n",(0,i.jsxs)(n.p,{children:["The hook is now installed but needs to be configured for your environment. First, copy the value of the ",(0,i.jsx)(n.code,{children:"TypeArn"}),"\nattribute from the JSON output of the above command, and store it in an environment variable:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-shell",children:'export HOOK_TYPE_ARN="arn:aws:cloudformation:eu-north-1:687803501377:type/hook/Styra-OPA-Hook"\n'})}),"\n",(0,i.jsx)(n.p,{children:"Next, set the AWS region and the URL to use for calling OPA:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-shell",children:'export AWS_REGION="eu-north-1"\nexport OPA_URL="https://cfn-opa.example.com"\n'})}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.strong,{children:"(OPTIONAL):"})," If you want to use a bearer token to authenticate against OPA, provide an ARN pointing to the AWS Secret\ncontaining the token:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-shell",children:'export OPA_AUTH_TOKEN_SECRET="arn:aws:secretsmanager:eu-north-1:687803501377:secret:opa-cfn-token-l26bHK"\n'})}),"\n",(0,i.jsxs)(n.p,{children:["With the configuration variables set, push the configuration to AWS (remove ",(0,i.jsx)(n.code,{children:"opaAuthTokenSecret"})," if you don't intend to\nuse it):"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-shell",children:'aws cloudformation --region "$AWS_REGION" set-type-configuration \\\n  --configuration "{\\"CloudFormationConfiguration\\":{\\"HookConfiguration\\":{\\"TargetStacks\\":\\"ALL\\",\\"FailureMode\\":\\"FAIL\\",\\"Properties\\":{\\"opaUrl\\": \\"$OPA_URL\\",\\"opaAuthTokenSecret\\":\\"$OPA_AUTH_TOKEN_SECRET\\"}}}}" \\\n  --type-arn $HOOK_TYPE_ARN\n'})}),"\n",(0,i.jsx)(n.p,{children:"The hook is now installed, configured and activated!"}),"\n",(0,i.jsx)(n.h3,{id:"3-learn-the-domain",children:"3. Learn the Domain"}),"\n",(0,i.jsx)(n.p,{children:"Before writing the first policy, take a closer look at the data used in this tutorial."}),"\n",(0,i.jsx)(n.h4,{id:"aws-cloudformation-templates",children:"AWS CloudFormation Templates"}),"\n",(0,i.jsxs)(n.p,{children:["A template file is commonly a YAML or JSON file, describing a set of AWS resources. While a template may describe\nmultiple resources, the hook will send each resource for validation separately. Important to note here is that the\nresource presented to the hook will be shown ",(0,i.jsx)(n.strong,{children:"exactly"}),' as provided in the template file. The hook does not perform any\ntype preprocessing, such as adding default values where missing, or providing auto-generated names. Policy authors must\nhence take into account that even "obvious" attributes like name might not be present in the resource provided for\nevaluation. As an example, a template to deploy an S3 Bucket with default attributes may be as minimal as this:']}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-yaml",children:"Resources:\n  ExampleS3Bucket:\n    Type: AWS::S3::Bucket\n"})}),"\n",(0,i.jsxs)(n.p,{children:["For more information on templates, see the\n",(0,i.jsx)(n.a,{href:"https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-guide.html",children:"AWS 
1User Guide"})," on that topic."]}),"\n",(0,i.jsx)(n.h4,{id:"input-and-response-format",children:"Input and Response Format"}),"\n",(0,i.jsx)(n.p,{children:"The OPA configured to receive requests from the CFN hook will have its input provided in this format:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",children:'{\n  "action": "CREATE",\n  "hook": "Styra::OPA::Hook",\n  "resource": {\n    "id": "MyS3Bucket",\n    "name": "AWS::S3::Bucket",\n    "type": "AWS::S3::Bucket",\n    "properties": {\n      "Tags": [{ "Key": "Owner", "Value": "Platform Team" }],\n      "BucketName": "platform-bucket-1"\n    }\n  }\n}\n'})}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["The ",(0,i.jsx)(n.code,{children:"action"})," is either ",(0,i.jsx)(n.code,{children:"CREATE"}),", ",(0,i.jsx)(n.code,{children:"UPDATE"})," or ",(0,i.jsx)(n.code,{children:"DELETE"})]}),"\n",(0,i.jsxs)(n.li,{children:["The ",(0,i.jsx)(n.code,{children:"id"})," is the key of the resource, as provided in the template"]}),"\n",(0,i.jsxs)(n.li,{children:["The ",(0,i.jsx)(n.code,{children:"type"}),' is divided by "resource domain" and the specific type, so e.g. the S3 domain may contain ',(0,i.jsx)(n.code,{children:"Bucket"}),",\n",(0,i.jsx)(n.code,{children:"BucketPolicy"}),", and so on."]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["The hook expects the response to contain a boolean ",(0,i.jsx)(n.code,{children:"allow"})," attribute, and a list of (potential) ",(0,i.jsx)(n.code,{children:"violations"}),":"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",children:'{\n  "allow": false,\n  "violations": [\n    "bucket must not be public",\n    "bucket name must follow naming standard"\n  ]\n}\n'})}),"\n",(0,i.jsxs)(n.p,{children:["Any request denied will be logged in ",(0,i.jsx)(n.a,{href:"https://aws.amazon.com/cloudwatch/",children:"AWS CloudWatch"})," for the same account."]}),"\n",(0,i.jsx)(n.h3,{id:"4-write-a-cloudformation-hook-policy",children:"4. Write a CloudFormation Hook Policy"}),"\n",(0,i.jsxs)(n.p,{children:["With knowledge of the domain and the data model, it is time to write the first CloudFormation Hook policy. Since\na single OPA endpoint services requests for all types of resources, the tutorial uses the\n",(0,i.jsx)(n.a,{href:"./configuration/#miscellaneous",children:"default decision"})," policy, which by default queries the ",(0,i.jsx)(n.code,{children:"system.main"})," rule. Add a\nsimple policy to block an S3 Bucket unless it has an ",(0,i.jsx)(n.code,{children:"AccessControl"})," attribute set to ",(0,i.jsx)(n.code,{children:"Private"}),":"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-rego",children:'package system\n\nmain := {\n    "allow": count(deny) == 0,\n    "violations": deny,\n}\n\ndeny contains msg if {\n    bucket_create_or_update\n    not bucket_is_private\n\n    msg := sprintf("S3 Bucket %s \'AccessControl\' attribute value must be \'Private\'", [input.resource.id])\n}\n\nbucket_create_or_update if {\n    input.resource.type == "AWS::S3::Bucket"\n    input.action in {"CREATE", "UPDATE"}\n}\n\nbucket_is_private if {\n    input.resource.properties.AccessControl == "Private"\n}\n'})}),"\n",(0,i.jsxs)(n.p,{children:["Since CloudFormation Templates may contain only the bare minimum of information, it is not safe to assume that there\nwill be an ",(0,i.jsx)(n.code,{children:"AccessControl"})," attribute present in the input at all. Using negation of boolean rules inside the ",(0,i.jsx)(n.code,{children:"deny"}),"\nrules help alleviate the problem of values potentially being undefined. Compare to the following deny rule, which might\nlook correct at a first glance:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-rego",children:"deny contains msg if {\n    bucket_create_or_update\n\n    input.resource.properties.AccessControl != \"Private\"\n\n    msg := sprintf(\"S3 Bucket %s 'AccessControl' attribute value must be 'Private'\", [input.resource.id])\n}\n"})}),"\n",(0,i.jsxs)(n.p,{children:["This rule would work fine as long as there ",(0,i.jsx)(n.em,{children:"is"})," an ",(0,i.jsx)(n.code,{children:"AccessControl"}
1)," attribute present in the input resource, but would\nfail (i.e. not evaluate) as soon as the property was missing, leading to the resource being allowed! Using helper rules\nand negation is a good way to work with data that might or might not be present, and results in more readable policies,\ntoo."]}),"\n",(0,i.jsxs)(n.admonition,{type:"danger",children:[(0,i.jsxs)(n.p,{children:["Surprisingly, boolean values from CloudFormation Templates are provided to the hook in the form of ",(0,i.jsx)(n.strong,{children:"strings"}),' (i.e.\n"true" and "false"). Policy authors must take this into account, and explicitly check for the value of these\nattributes. An example S3 bucket policy might for example want to check that public ACLs are blocked:']}),(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-rego",children:'# Wrong: will allow both "true" and "false" values as both are considered "truthy"\nblock_public_acls if {\n    input.resource.properties.PublicAccessBlockConfiguration.BlockPublicAcls\n}\n'})}),(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-rego",children:'# Correct: will allow only when property set to "true"\nblock_public_acls if {\n    input.resource.properties.PublicAccessBlockConfiguration.BlockPublicAcls == "true"\n}\n'})})]}),"\n",(0,i.jsx)(n.h3,{id:"5-policy-enforcement-testing",children:"5. Policy Enforcement Testing"}),"\n",(0,i.jsxs)(n.p,{children:["With the above policy loaded into OPA, deploy the minimal S3 Bucket from the\nprevious template example. Save the below minimal template to a file called ",(0,i.jsx)(n.code,{children:"s3bucket.yaml"}),":"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-yaml",children:"Resources:\n  ExampleS3Bucket:\n    Type: AWS::S3::Bucket\n"})}),"\n",(0,i.jsxs)(n.p,{children:["Since the S3 bucket doesn't have an ",(0,i.jsx)(n.code,{children:"AccessControl"})," attribute, it should be denied by the hook.\nDeploy a template by creating a ",(0,i.jsx)(n.strong,{children:"stack"}),":"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-shell",children:"aws cloudformation create-stack --stack-name cfn-s3 --template-body file://s3bucket.yaml\n"})}),"\n",(0,i.jsx)(n.p,{children:"The output of the above command will simply be a confirmation that the stack was deployed. It won't tell us whether the\ndeployment was successful or not. In order to know that, check the stack events:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-shell",children:"aws cloudformation describe-stack-events --stack-name cfn-s3\n"})}),"\n",(0,i.jsxs)(n.p,{children:["The output of the above command will be a list of all events associated with the ",(0,i.jsx)(n.code,{children:"cfn-s3"})," stack. Among the events, you\nshould now find an item describing that the hook denied the request, and its reason for doing so:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",children:'{\n  "StackEvents": [\n    {\n      "StackId": "arn:aws:cloudformation:eu-north-1:55523455647:stack/cfn-s3/4f605f70-b1ca-12ec-b4d8-0a63e869dfee",\n      "EventId": "ExampleS3Bucket-c243efd6-bfe7-3f10-8304-a0e40fe5f6f4",\n      "StackName": "cfn-s3",\n      "LogicalResourceId": "ExampleS3Bucket",\n      "PhysicalResourceId": "",\n      "ResourceType": "AWS::S3::Bucket",\n      "Timestamp": "2022-03-31T08:41:15.946000+00:00",\n      "ResourceStatus": "CREATE_IN_PROGRESS",\n      "HookType": "Styra::OPA::Hook",\n      "HookStatus": "HOOK_COMPLETE_FAILED",\n      "HookStatusReason": "Hook failed with message: S3 Bucket ExampleS3Bucket \'AccessControl\' attribute value must be \'Private\'",\n      "HookInvocationPoint": "PRE_PROVISION",\n      "HookFailureMode": "FAIL"\n    }\n  ]\n}\n'})}),"\n",(0,i.jsx)(n.p,{children:"The policy is now enforced. Update the template so that it passes the policy requirement:"}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.strong,{children:"s3bucket.yaml"})}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-yaml",children:"Resources:\n  ExampleS3Bucket:\n    Type: AWS::S3::Bucket\n    Properties:\n      AccessControl: Private\n"})}),"\n",(0,i.jsxs)(n.p,{children:["Even though the stack did not create an S3 bucket (as the change got rolled back), the ",(0,i.jsx)(n.strong,{children:"stack"})," still exists.\nTo try again, first delete the existing stack:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-shell",children:"aws cloudformation delete-stack --stack-name cfn-s3\n"})}),"\n",(0,i.jsx)(n.p,{children:"Try again:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-shell",children:"aws cloudformation create-stack --stack-name cfn-s3 --template-body file://s3bucket.yaml\n"})}),"\n",(0,i.jsxs)(n.p,{children:["Checking the output of ",(0,i.jsx)(n.code,{children:"aws cloudformation describe-stack-events --stack-name cfn-s3"})," once more will now show that the\nresource was created. Do note that this could take up to a minute, so if you don't see it immediately, rerun the command\na bit later."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",children:'{\n  "StackEvents": [\n    {\n      "StackId": "arn:aws:cloudformation:eu-north-1:55523455647:stack/cfn-s3/4f605f70-b1ca-12ec-b4d8-0a63e869dfee",\n      "EventId": "e20fdfa0-b0d0-11ec-b669-0e70f1f560a6",\n      "StackName": "cfn-s3",\n      "LogicalResourceId": "cfn-s3",\n      "PhysicalResourceId": "arn:aws:cloudformation:eu-north-1:55523455647:stack/cfn-s3/cf418141-b0d9-11bc-b421-0a1244c68dd1",\n      "ResourceType": "AWS::CloudFormation::Stack",\n      "Timestamp": "2022-03-31T08:59:33.392000+00:00",\n      "ResourceStatus": "CREATE_COMPLETE"\n    }\n  ]\n}\n'})}),"\n",(0,i.jsxs)(n.p,{children:["Note: once the stack is successfully deployed, the ",(0,i.jsx)(n.code,{children:"update-stack"})," command can be used after changes are made to\ntemplates:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-shell",children:"aws cloudformation update-stack --stack-name cfn-s3 --template-body file://s3bucket.yaml`\n"})}),"\n",(0,i.jsx)(n.h2,{id:"further-improvements",children:"Further Improvements"}),"\n",(0,i.jsx)(n.h3,{id:"dynamic-policy-composition",children:"Dynamic Policy Composition"}),"\n",(0,i.jsx)(n.p,{children:'Having a single policy file for all rules will quickly become unwieldy. Is there room for improvement? One way of doing\nthat would be to use dynamic policy composition, where a single main policy acts as a "router", and forwards queries to\nother packages based on attributes from the input. A natural attribute to use for CloudFormation templates might for\nexample be the resource type, allowing policies to be grouped by the resource type they are meant to act on.\nHere is what such a main policy might look like:'}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-rego",metastring:'title="main.rego"',children:'# METADATA\n# description: |\n#   Dynamic routing to policy based in input.resource.type,\n#   aggregating the deny rules found in all policies with a\n#   matching package name\n#\npackage system\n\nmain := {\n    "allow": count(violations) == 0,\n    "violations": violations,\n}\n\n# METADATA\n# description: |\n#   Main routing logic, simply convert
1ing input.resource.type, e.g.\n#   AWS::S3::Bucket to data.aws.s3.bucket and returning that document.\n#\n#   By default, only input.action == "CREATE" | "UPDATE" will be routed\n#   to the data.aws.s3.bucket document. If handling "DELETE" actions is\n#   desirable, one may create a special policy for that by simply appending\n#   "delete" to the package name, e.g. data.aws.s3.bucket.delete\n#\nroute := document(lower(component), lower(type)) if {\n    ["AWS", component, type] = split(input.resource.type, "::")\n}\n\nviolations contains msg if {\n    # Aggregate all deny rules found in routed document\n    some msg in route.deny\n}\n\n#\n# Basic input validation to avoid having to do this in each resource policy\n#\n\nviolations contains "Missing input.resource" if {\n    not input.resource\n}\n\nviolations contains "Missing input.resource.type" if {\n    not input.resource.type\n}\n\nviolations contains "Missing input.resource.id" if {\n    not input.resource.id\n}\n\nviolations contains "Missing input.action" if {\n    not input.action\n}\n\n#\n# Helpers\n#\n\ndocument(component, type) := data.aws[component][type] if {\n    input.action != "DELETE"\n}\n\ndocument(component, type) := data.aws[component][type].delete if {\n    input.action == "DELETE"\n}\n'})}),"\n",(0,i.jsxs)(n.p,{children:["The above policy will invoke the ",(0,i.jsx)(n.code,{children:"route"})," rule to determine which package should be evaluated based on the\n",(0,i.jsx)(n.code,{children:"input.resource.type"}),", transforming a value such as ",(0,i.jsx)(n.code,{children:"AWS::S3::Bucket"})," into a call to the ",(0,i.jsx)(n.code,{children:"data.aws.s3.bucket"})," package,\nwhere each rule named ",(0,i.jsx)(n.code,{children:"deny"})," will be evaluated, and the result aggregated into the final decision."]}),"\n",(0,i.jsxs)(n.p,{children:["Since most policies only deal with ",(0,i.jsx)(n.code,{children:"CREATE"})," or ",(0,i.jsx)(n.code,{children:"UPDATE"})," actions, it is better to avoid checking\nfor this in all rules. Instead, the router appends ",(0,i.jsx)(n.code,{children:".delete"})," to the package name for ",(0,i.jsx)(n.code,{children:"DELETE"}),"\noperations, so that a request to delete e.g. an S3 bucket would invoke the ",(0,i.jsx)(n.code,{children:"data.aws.s3.bucket.delete"})," package (if it\nexists)."]}),"\n",(0,i.jsx)(n.p,{children:"Additionally, some simple input validation is done at this stage, to avoid repeating it in each\nresource-specific policy."}),"\n",(0,i.jsx)(n.p,{children:"Modify the original policy to verify S3 bucket resources only:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-rego",children:"package aws.s3.bucket\n\ndeny contains sprintf(\"S3 Bucket %s 'AccessControl' attribute value must be 'Private'\", [input.resource.id]) if {\n    not bucket_is_private\n}\n\nbucket_is_private if {\n    input.resource.properties.AccessControl == \"Private\"\n}\n"})}),"\n",(0,i.jsxs)(n.p,{children:["Note that the ",(0,i.jsx)(n.code,{children:"bucket_create_or_update"})," rule is no longer needed, as that is already asserted by the main policy.\nQuite an improvement in terms of readability, and a good foundation for further policy authoring. If you'd like to see\nmore examples of policy utilizing this pattern, check out the\n",(0,i.jsx)(n.a,{href:"https://github.com/StyraOSS/opa-aws-cloudformation-hook/tree/main/examples/policy",children:"policy directory"})," in the OPA AWS\nCloudFormation Hook repo."]}),"\n",(0,i.jsx)(n.h3,{id:"opa-authentication-via-aws-secrets",children:"OPA Authentication via AWS Secrets"}),"\n",(0,i.jsx)(n.h4,{id:"opa-configuration",children:"OPA Configuration"}),"\n",(0,i.jsxs)(n.p,{children:["Since the OPA server does not run inside the AWS Lambda, it is a good idea to require authentication to access its REST\nAPI, as described in the OPA ",(0,i.jsx)(n.a,{href:"https://www.openpolicyagent.org/docs/latest/security/#authentication-and-authorization",children:"documentation"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"A simple authz policy for checking the bearer token might look something like this:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-rego",metastring:'title="authz.rego"',children:'package system.authz\n\ndefault allow := false\n\nallow if {\n    input.identity == "my_secret_token"\n}\n'})}),"\n",(0,i.jsxs)(n.p,{children:["Once created, remember to pass the appropriate flags to ",(0,i.jsx)(n.code,{children:"opa run"})," to enable authentication / authorization:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-shell",children:"opa run --server --authentication=token --authorization=basic .\n"})}),"\n",(0,i.jsx)(n.h4,{id:"opa-aws-cloudformation-hook-configuration",children:"OPA AWS CloudFormation Hook Configuration"}),"\n",(0,i.jsxs)(n.p,{children:["If configured to use a bearer token for authenticating against OPA (by setting the ",(0,i.jsx)(n.code,{children:"OPA_AUTH_TOKEN_SECRET"}
1)," environment\nvariable as described in the section on configuring the hook), the hook will try to fetch the token from the\nsecret provided in the ",(0,i.jsx)(n.code,{children:"opaAuthTokenSecret"})," (ARN) configuration attribute. Note that the token should be provided\nas a plain string in the secret (i.e. the ",(0,i.jsx)(n.code,{children:"SecretString"}),") and not wrapped in a JSON object."]}),"\n",(0,i.jsxs)(n.p,{children:["In order to fetch the token, the hook will need to be permitted to perform the ",(0,i.jsx)(n.code,{children:"secretsmanager:GetSecretValue"}),"\noperation. Note that the hook will ",(0,i.jsx)(n.strong,{children:"only"})," read the secret provided by ",(0,i.jsx)(n.code,{children:"opaAuthTokenSecret"}),", but it's recommended\nto limit the ",(0,i.jsx)(n.code,{children:"HookTypePolicy"})," on the IAM role to the specific secret accessed, i.e. the same ARN provided in\n",(0,i.jsx)(n.code,{children:"opaAuthTokenSecret"}),". Example ",(0,i.jsx)(n.code,{children:"HookTypePolicy"})," to allow the hook access to a specific secret:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",children:'{\n  "Version": "2012-10-17",\n  "Statement": [\n    {\n      "Sid": "VisualEditor0",\n      "Effect": "Allow",\n      "Action": "secretsmanager:GetSecretValue",\n      "Resource": "arn:aws:secretsmanager:eu-north-1:673240551671:secret:opa-cfn-token-l26bHK"\n    }\n  ]\n}\n'})}),"\n",(0,i.jsx)(n.p,{children:"If you aren't planning to use bearer tokens for authentication, you may remove the permission entirely."})]})}function h(e={}){const{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(d,{...e})}):d(e)}}}]);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.