Jekyll2024-01-13T09:19:40+00:00https://onecloudplease.com/feed.xmlOne Cloud PleaseThe ramblings of Ian Mckay, a DevOps dude from AustraliaIan MckayHTTPS Endpoints and more tricks with AWS Step Functions2024-01-13T00:00:00+00:002024-01-13T00:00:00+00:00https://onecloudplease.com/blog/https-endpoints-and-more-tricks-with-aws-step-functions<p><img src="/images/posts/https-endpoints-step-functions.jpg" alt="" /></p>
<p>AWS re:Invent 2023 is now behind us and one of my favourite announcements was the introduction of <a href="https://docs.aws.amazon.com/step-functions/latest/dg/connect-third-party-apis.html">HTTPS Endpoints</a> to AWS Step Functions. In this post, I explain the feature, test its limits and also show off some other tricks for data manipulation within your state machines.</p>
<p>For the impatient, <a href="https://github.com/iann0036/chess-dot-com-state-machine-sample/blob/main/template.yml">here</a> is the final result.</p>
<h2 id="https-endpoints-feature">HTTPS Endpoints feature</h2>
<p>HTTPS endpoints use <a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-api-destinations.html#eb-api-destination-connection">Amazon EventBridge API destination connections</a> to determine the authentication mechanism used. This service subsequently uses Secrets Manager to store the credentials that will be included to authenticate requests.</p>
<p>Then within the state machine, you reference this connection and specify your own URL and HTTP method. You can also optionally include your own query parameters, headers and/or request body.</p>
<p>There are some limitations though. Firstly, there is a 60 second timeout (hard limit) for the totality of the request. There are additional mandatory headers which Step Functions sets and you cannot override. These are:</p>
<ul>
<li>Host (value: <em>hostname of the URL</em>)</li>
<li>User-Agent (value: <code class="language-plaintext highlighter-rouge">Amazon|StepFunctions|HttpInvoke|us-east-1</code>, where <code class="language-plaintext highlighter-rouge">us-east-1</code> is replaced by your region)</li>
<li>Range (value: <code class="language-plaintext highlighter-rouge">bytes=0-262144</code>)</li>
</ul>
<p>Note that the request will still fail if the response exceeds 256kb even though the Range header is set. The presence of the header can also cause confusion as some servers will respond with a <code class="language-plaintext highlighter-rouge">206 Partial Content</code> status code even if all data is returned, so be aware of that.</p>
<p>The client IP address for the requests are different for each request and appear to lie within the standard EC2 public IP range <a href="https://docs.aws.amazon.com/vpc/latest/userguide/aws-ip-ranges.html">published by AWS</a>. There is no capability to use Elastic IPs or other networking constructs within your account.</p>
<p>Your state machine IAM role will need to include actions that allow access to the connection and its associated secret, as well as the <code class="language-plaintext highlighter-rouge">states:InvokeHTTPEndpoint</code> action which has the optional conditionals of <code class="language-plaintext highlighter-rouge">states:HTTPEndpoint</code> and <code class="language-plaintext highlighter-rouge">states:HTTPMethod</code> to help scope down what endpoints and HTTP methods the state machine can call. I have included an example of a granular policy in the CloudFormation template at the end of this post.</p>
<h2 id="gathering-the-data">Gathering the data</h2>
<p><img src="/images/posts/sfunc4.png" alt="" /></p>
<p>In order to demonstrate the capabilities of the new feature, I’ve chosen to consume the <a href="https://www.chess.com/news/view/published-data-api">Chess.com API</a>. This is a free and anonymous API which retrieves metadata about games and players on their platform.</p>
<p>I will retrieve a list of all <a href="https://www.chess.com/terms/grandmaster-chess">grandmasters</a>, their country of origin, and aggregate these details by country.</p>
<p>Because this is a public endpoint, there is no need for an Authorization or similar header when accessing the endpoint, however EventBridge API destinations <em>require</em> the use of Basic Authorization, OAuth or API Key header. One creative way of avoiding sending an unnecessary header is to create your connection using the API Key type but set the header to one of the immutable headers, such as <code class="language-plaintext highlighter-rouge">User-Agent</code>.</p>
<p><img src="/images/posts/sfunc2.png" alt="" /></p>
<p>I created the step to gather the list of grandmasters by hitting the URL <code class="language-plaintext highlighter-rouge">https://api.chess.com/pub/titled/GM</code>. Because I am only interested in the content of the response body, I apply an OutputPath filter of <code class="language-plaintext highlighter-rouge">$.ResponseBody</code>. This provides me with the list of grandmaster usernames, but not their origin country or actual name. For that, we need to retrieve their details using additional individual HTTPS calls.</p>
<p>To do this efficiently, we use the <a href="https://docs.aws.amazon.com/step-functions/latest/dg/use-dist-map-orchestrate-large-scale-parallel-workloads.html">Distributed Map</a> type within Step Functions. To ensure we do not overload the Chess.com API, we limit the concurrency to 40. We also use a standard exponential backoff for the inner HTTPS call to allow for retries in the event of an occasional error.</p>
<p>This brings us to a state where we have an array of the individual grandmaster details.</p>
<h2 id="aggregating-the-data">Aggregating the data</h2>
<p><img src="/images/posts/sfunc3.png" alt="" /></p>
<p>Aggregating data (using map-reduce style methods) within a state machine is not a native function, however with some clever usage it is possible.</p>
<p>To do this, we first need to ensure all fields are present in the individual grandmaster details. Unfortunately, the <code class="language-plaintext highlighter-rouge">name</code> field isn’t always present on these responses so to fix that we add the following <code class="language-plaintext highlighter-rouge">ResultSelector</code> to the HTTPS endpoint step within the distributed map:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{
"output.$": "States.JsonMerge(States.StringToJson('{\"name\":\"Unknown Player\"}'), $.ResponseBody, false)"
}
</code></pre></div></div>
<p>This takes the resulting detail from the HTTP response, and performs a JSON merge with the static object we defined with a default name. If the name is not present, this field will be used.</p>
<p>Next, we format the resulting name in the way we would like it, as well as extract the 2-letter country code from the URL which looks like <code class="language-plaintext highlighter-rouge">https://api.chess.com/pub/country/US</code>. To do this, we use a Pass state. The Parameters of the Pass state are as follows:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{
"displayName.$": "States.Format('{} ({})', $.output.name, $.output.username)",
"country.$": "States.ArrayGetItem(States.StringSplit($.output.country, '/'), 4)"
}
</code></pre></div></div>
<p>Note that the array index used is 4 and not 5. This is because empty segments (the one in between <code class="language-plaintext highlighter-rouge">http:/</code> and the next <code class="language-plaintext highlighter-rouge">/</code>) get discarded during the <code class="language-plaintext highlighter-rouge">States.StringSplit</code> operation.</p>
<p>Using the output of the distributed map, we apply a new Pass state with the following parameters:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{
"original.$": "$",
"countries.$": "States.ArrayUnique($[*].country)",
"countriesCount.$": "States.ArrayLength(States.ArrayUnique($[*].country))",
"iterator": 0,
"output": {}
}
</code></pre></div></div>
<p>The <code class="language-plaintext highlighter-rouge">original</code> key contains the distributed map output, the <code class="language-plaintext highlighter-rouge">countries</code> key uses JSONPath and <code class="language-plaintext highlighter-rouge">States.ArrayUnique</code> to select the unique list of countries, the <code class="language-plaintext highlighter-rouge">countriesCount</code> key is the length of the countries, the <code class="language-plaintext highlighter-rouge">iterator</code> key is initialised at 0, and the <code class="language-plaintext highlighter-rouge">output</code> key is initialised with an empty map.</p>
<p>Then we enter a loop. The loop will continue whilst the iterator is less than the length of countries. We then use a Pass state to set the <code class="language-plaintext highlighter-rouge">country</code> key to the country at the <code class="language-plaintext highlighter-rouge">iterator</code> index of the <code class="language-plaintext highlighter-rouge">countries</code> list. We then use one more Pass state increase the iterator with:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>States.MathAdd($.iterator, 1)
</code></pre></div></div>
<p>We also set the <code class="language-plaintext highlighter-rouge">output</code> key to the following (spaced for visibility):</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>States.JsonMerge(
States.StringToJson(
States.Format(
'\{"{}":{}\}',
$.country,
States.JsonToString(
$.original[?(@.country == $.country)]['displayName']
)
)
),
$.output
, false)
</code></pre></div></div>
<p>The above performs the following transformations:</p>
<ol>
<li>Retrieve the list of all <code class="language-plaintext highlighter-rouge">displayName</code> strings within the <code class="language-plaintext highlighter-rouge">original</code> key, filtering where the <code class="language-plaintext highlighter-rouge">country</code> key is equal to the country within the <code class="language-plaintext highlighter-rouge">original</code> key entries which we previously created using JSONPath</li>
<li>Convert that list to a JSON string</li>
<li>Create a new JSON-compatible string where the key is the <code class="language-plaintext highlighter-rouge">country</code> and the value is the above string-encoded array of names</li>
<li>Convert the string to a JSON object</li>
<li>Merge that object with the <code class="language-plaintext highlighter-rouge">output</code> variable</li>
</ol>
<p>We’re basically adding the country code as a key of the <code class="language-plaintext highlighter-rouge">output</code> JSON object one at a time, then increasing the iterator to reference the next country in the list.</p>
<p>Once it has completed the loop, we are left with our final output.</p>
<p><img src="/images/posts/sfunc5.png" alt="" /></p>
<h2 id="finishing-up">Finishing up</h2>
<p>I have provided a CloudFormation template that contains the full state machine and associated connection <a href="https://github.com/iann0036/chess-dot-com-state-machine-sample/blob/main/template.yml">here</a>. Feel free to deploy this into your own AWS account and try it yourself.</p>
<p>The HTTPS Endpoints feature is a very useful addition to the Step Functions service that I believe will have huge uptake. I personally want to do more with the Step Functions service as I believe more architectures can be more than serverless, they can be “functionless” (i.e. no Lambda functions). I would however like to see more useful intrinsics become available in the service. As you can see from this post, developers are often pushing the limits of what is available. Consider this my <a href="https://twitter.com/search?q=%23awswishlist">#awswishlist</a> item.</p>
<p>A big thank you to <a href="https://twitter.com/__steele">Aidan Steele</a> for helping review this post. If you liked what I’ve written, or want to hear more on this topic, reach out to me on 𝕏 at <a href="https://twitter.com/iann0036">@iann0036</a>.</p>Ian MckaySwiping right on the AWS WAF CAPTCHA challenge2023-07-25T00:00:00+00:002023-07-25T00:00:00+00:00https://onecloudplease.com/blog/swiping-right-on-the-aws-waf-captcha-challenge<p><img src="/images/posts/captcha.jpg" alt="" /></p>
<p>In 2021, AWS WAF <a href="https://aws.amazon.com/about-aws/whats-new/2021/11/aws-waf-captcha-support/">introduced</a> a new CAPTCHA feature to help protect sites against bot traffic. The release had some <a href="https://twitter.com/iann0036/status/1457908922925256704">mixed</a> <a href="https://twitter.com/iann0036/status/1457911175094538248">reviews</a> but the idea was that it was an effective protection against programmatic solvers or “bots”.</p>
<p>In this post, I walk through my methodology for beating one of the CAPTCHA challenges presented programmatically. If you’d like to follow along, you can try the CAPTCHA challenges yourself <a href="https://efw47fpad9.execute-api.us-east-1.amazonaws.com/latest">here</a>.</p>
<h2 id="the-aws-waf-captcha-system">The AWS WAF CAPTCHA system</h2>
<p>The CAPTCHA <a href="https://docs.aws.amazon.com/waf/latest/developerguide/waf-captcha-and-challenge.html">feature</a> in AWS WAF is an optional action as a result of a match against customer-defined rules. It is intended to be an option to help bridge the difficult decision of a hard deny or hard allow when client heuristics may appear suspicious but not outright bot-like.</p>
<p>When triggered, the action prompts viewers of a website with interactive challenges designed to test that a human viewer is real and block bots seeking to crawl or disrupt human traffic. At launch, and to this day, there are two challenges available which I will call the “car maze” and “shape match” challenges.</p>
<p>I created a Twitter (𝕏?) thread about beating the car maze challenge when it was originally released which you can read here:</p>
<div style="max-width: 60%; padding-left: 0; padding-right: 0; margin-left: auto; margin-right: auto; margin-top: 15px;"><blockquote class="twitter-tweet"><p lang="en" dir="ltr">Had a bit of fun today with the WAF CAPTCHA thing. The car maze turned into a fun programming challenge! 1/ <a href="https://t.co/D6Rf4SZGy4">pic.twitter.com/D6Rf4SZGy4</a></p>— Ian Mckay (@iann0036) <a href="https://twitter.com/iann0036/status/1459770171581550593?ref_src=twsrc%5Etfw">November 14, 2021</a></blockquote><script async="" src="https://platform.twitter.com/widgets.js" charset="utf-8"></script></div>
<p>I will note that there have been some changes since writing the thread and discussing my findings with the AWS WAF service team that make the car maze challenge slightly more complex, though the same concepts still broadly apply.</p>
<p>Let’s go through the same process with the shape match challenge!</p>
<h2 id="shape-matching">Shape matching</h2>
<p>The shape match challenge features an image of 5 random 3D shapes lined up horizontally which has been split across the vertical axis and reordered. The interface gives you a slider which you can move to match usually only one shape at a time and gives you instructions as to which shape to match up and submit. The bottom section wraps as you drag the slider.</p>
<p><img src="/images/posts/captcha1.png" alt="" /></p>
<p>The available shapes are: <code class="language-plaintext highlighter-rouge">ball</code>, <code class="language-plaintext highlighter-rouge">cone</code>, <code class="language-plaintext highlighter-rouge">cube</code>, <code class="language-plaintext highlighter-rouge">cylinder</code>, <code class="language-plaintext highlighter-rouge">donut</code>, <code class="language-plaintext highlighter-rouge">knot</code> and <code class="language-plaintext highlighter-rouge">pyramid</code>.</p>
<p>The challenge presents both halves of the shapes as a single JPEG image, always at a 320x160 resolution. Taking a similar approach as the car maze solve, I’m using HTML canvas to inspect the image, extract pixel data and draw for my own visualization. For my first step, I sample the top-left pixel colour and eliminate these pixels from consideration. Because the challenge is a JPEG, some colour blending and artifacts are present so in most of the below steps I check for colour closeness by ensuring the RGB channels are within a small boundary (in this case, no more than 7 away). The top and bottom 80 pixels of the Y-axis represent the top and bottom sections, respectively.</p>
<p><img src="/images/posts/captcha2.png" alt="" /></p>
<p>I now want to identify the location and width of the shapes at the midline for the top and bottom sections. The shapes in the challenge always have a clear separation between them, so in order to do this I move left-to-right at just above and below the midline (skipping the exact pixels on the midline, as JPEG artifacting can sometimes merge the pixels at y=79 and y=80). When I hit a non-background pixel, I mark the starting point of the shape. Once I hit a background pixel again, I can presume the start and stop points on the X-axis.</p>
<p>This gives me a set of values which intersect at the midline, however there are typically more values than the 5 shapes that are present. This is because shapes like the donut and knot intersect the midline at multiple points. To overcome this, we need to find any space in between where the shapes hit the midline where there isn’t a clear path to the relative extremes of the axis (i.e. where it is presumed to be in the center of the donut / knot). We take the middle of each of the clear spaces and start drawing a line towards the extreme of the axis, allowing a deviation to the left or right if clear space is present. Any line that does not reach the axis extreme is considered to be within the shapes, so these points are aggregated with regard to the shape boundary at the midline. This finally provides us with 5 positions and widths for both the top and bottom sections.</p>
<p><img src="/images/posts/captcha3.png" alt="" /></p>
<p>Because the donut always has two midline points which are of roughly equal width, we can mark this as a high probability match straight away. Additionally, if we see a single shape with more than 2 midline point intersections we can safely assume it is of the knot as this is the only shape that does this. At this point, I can start drawing the resulting shapes on individual canvases and mark those which are assumed during development.</p>
<p><img src="/images/posts/captcha4.png" alt="" /></p>
<p>We can then use the widths of the top and bottom shape midline intersections and find roughly matching widths. This gives us strong candidates for matching top and bottom section shapes, allowing us to calculate the relative X-axis offset needed to create the shapes. Under good circumstances, we now have 5 completed shapes but no way of identifying at least 3 of them.</p>
<p>In order to discover more information about the potential shapes, we calculate more landmark points to gain additional heuristics on the shape type. These points are calculated by the following:</p>
<ul>
<li><strong>Point 1:</strong> From the extreme left side at the midline, move towards the Y-axis extreme</li>
<li><strong>Point 2:</strong> From the extreme right side at the midline, move towards the Y-axis extreme</li>
<li><strong>Point 3:</strong> From the X-axis center at the midline, move towards the Y-axis extreme - if blocked, deviate left if able</li>
<li><strong>Point 4:</strong> From the X-axis center at the midline, move towards the Y-axis extreme - if blocked, deviate right if able</li>
</ul>
<p>Here are the paths that discovery takes to find the landmark points:</p>
<p><img src="/images/posts/captcha5.png" alt="" /></p>
<p>A ball shape always has a short Y-axis travel for points 1 and 2 for both sections, as well as a short X-axis travel from the center of the midline for points 3 and 4. The Y-axis travel for points 3 and 4 are generally identical and have roughly the same value as the X-axis travel for points 1 and 2.</p>
<p>A cone or pyramid shape typically also has a short Y-axis travel for points 1 and 2 in the top section, but a large Y-axis travel for all points in the bottom section.</p>
<p>A cube or cylinder generally has a roughly matching X-axis and Y-axis for the diametrically opposing points (point 1 in the top and point 2 in the bottom, and vice-versa).</p>
<p>Although it is challenging to decide between a cone/pyramid and cube/cylinder due to their shape similarities, there is one more trick we can use. Taking a path across the X-axis just below the midline, track the colours during movement. If the colour always gradually changes slightly, we can assume there is a gradient and the shape is a cone or cylinder. If there is exactly one or two colours, these represent the visible faces of a pyramid or cube.</p>
<p>We’ve now successfully identified each shape and their offsets.</p>
<p><img src="/images/posts/captcha6.png" alt="" /></p>
<h2 id="solving-the-challenge">Solving the challenge</h2>
<p>The challenge generally accepts an offset value as its answer and so without any UI interference we could simply respond with a network request programmatically. However, I wanted to see the actual solution occur so I looked into actually performing the sliding action.</p>
<p>I had never programmatically moved a slider before and it turns out it is actually a rare automation to achieve, but it is possible. I came across <a href="https://stackoverflow.com/a/61547444/546911">this StackOverflow answer</a> which showed I can create custom <code class="language-plaintext highlighter-rouge">mousedown</code>, <code class="language-plaintext highlighter-rouge">mousemove</code> and <code class="language-plaintext highlighter-rouge">mouseup</code> Mouse Events which worked in order to drag the slider. Notably, there was some math required to slide to the correct position, as the image width was 320 pixels, the slider would drag a maximum of 274 pixels, and the challenge solution endpoint accepted an answer between 0 and 255.</p>
<p>Occasionally, identification would fail due to an edge case or similar, however this simply meant that a new challenge would load and the automation could try again immediately. There seems to be no lockout or escalation of difficulty.</p>
<h2 id="the-road-not-travelled">The road not travelled</h2>
<p>There were a few approaches I could have taken during the development of this solution, however I took what I thought was the simplest and easiest to understand solution. I did look into using the JavaScript version of OpenCV, which I could pretty easily use to find the contours of the shapes and I could have used this to assist with some edge case resolution.</p>
<p><img src="/images/posts/captcha7.png" alt="" /></p>
<p>Additionally, the audio-based accessibility CAPTCHA alternative still remains for those in the speech recognition space looking for a fun challenge.</p>
<h2 id="final-thoughts">Final thoughts</h2>
<p>The AWS WAF CAPTCHA remains an effective deterrent for all but the most determined of bot authors. I don’t envy the position the AWS WAF service team members are in. They are charged with creating a novel, interactive CAPTCHA challenge that has little cognitive load for users but remains challenging enough that it isn’t easily toppled by bots. I believe that if there were a constantly evolving rotation of new WAF challenge types we would have an effective protection purely based on the bot authors ability to adapt. Sadly this hasn’t yet happened. Features like <a href="https://aws.amazon.com/waf/features/bot-control/">Bot Control</a> seem to be a far more effective way of dealing with bot traffic without generally affecting users, so I’d recommend that instead.</p>
<p>If you liked what I’ve written, or want to hear more on this topic, reach out to me on Twitter (or whatever it’s called now) at <a href="https://twitter.com/iann0036">@iann0036</a>.</p>Ian MckayCedar: Avoiding the cracks2023-07-06T00:00:00+00:002023-07-06T00:00:00+00:00https://onecloudplease.com/blog/cedar-avoiding-the-cracks<p><img src="/images/posts/cedar1.jpg" alt="" /></p>
<p>With the <a href="https://aws.amazon.com/about-aws/whats-new/2023/05/cedar-open-source-language-access-control/">open-source release</a> of the Cedar engine and the <a href="https://aws.amazon.com/about-aws/whats-new/2023/06/amazon-verified-permissions-generally-available/">general availability release</a> of Amazon Verified Permissions, more and more engineers are considering integrating Cedar into their own systems for authorization, but what do policy authors need to consider to avoid unexpected outcomes?</p>
<p>In this post, I’ll walk through my experiences in where policy authoring can go wrong and the steps you can take to overcome these issues. This post will walk through some advanced evaluation scenarios, so if you’re new to the Cedar language I highly recommend you first read my introductory post on the topic, <a href="https://onecloudplease.com/blog/cedar-a-new-policy-language">Cedar: A new policy language</a>.</p>
<h2 id="non-unique-entity-identifiers">Non-unique entity identifiers</h2>
<p>Though I mentioned it in my previous post, it’s important to always use unique identifiers for entities to ensure they do not get re-used in the future. The reason this may be a problem is that a reliance may start to occur on the entity, the entity goes away at some point in time, then a new entity of the same name comes into existence at a later point.</p>
<p>For example, consider the following statement:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>permit(
principal == User::"John",
action,
resource == Account::"Corporate"
);
</code></pre></div></div>
<p>If the user named John leaves the company, and then another John joins the company and happens to take the same entity identifier, it’s possible for the new John to inherit some privileges he should not be entitled to. The <a href="https://cedarland.blog/design/why-no-entity-wildcards/content.html">Cedarland blog</a> has some more detail on the reasoning behind this.</p>
<h3 id="solutions">Solutions</h3>
<p>Always use unique identifiers, such as the identifiers your IdP provider uses, to uniquely identify principals. Additionally, use resource identifiers which are also unique for the context provided. Comments and annotations can help you keep track of identifiers where necessary.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>permit(
principal == User::"9a6afab1-5a37-4c90-aa40-24277b93ca28", // John Smith
action,
resource == Account::"710f18bc-b8ab-4313-b362-8e6264cfcf91" // Corporate Account
);
</code></pre></div></div>
<h2 id="invalid-statements">Invalid statements</h2>
<p>Invalid statements not being evaluated is in my opinion one of the easiest ways to get an unexpected result from your policy evaluations. Consider the following policy:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>permit(
principal,
action == Action::"Connect",
resource
);
forbid(
principal,
action == Action::"Connect",
resource == Endpoint::"AdminEndpoint"
) unless {
context.viaAdminNetwork == true
};
</code></pre></div></div>
<p>The intention behind the policy is to allow connections to all endpoints except the admin endpoint unless the context object has the <code class="language-plaintext highlighter-rouge">viaAdminNetwork</code> key set to true. Unfortunately, the implementation of the context object in this example is that the <code class="language-plaintext highlighter-rouge">viaAdminNetwork</code> key is omitted, not <code class="language-plaintext highlighter-rouge">false</code>, if the call does not come from the admin network.</p>
<p>The result of this is that the forbid statement is not processed as there is an evaluation error due to the missing key. However, as the permit statement has been evaluated, and there are no other valid forbid statements, the result is an allow of the call. Even though the evaluated result is allow, there will be errors in the diagnostic return, as you can see from this Cedar playground screenshot:</p>
<p><img src="/images/posts/cedar2.png" alt="" /></p>
<p>There is more discussion on the reasoning for this behaviour over at the <a href="https://cedarland.blog/design/why-ignore-errors/content.html#what-about-forbid-statements">Cedlarland blog</a>.</p>
<h3 id="solutions-1">Solutions</h3>
<p>Cedar has a validation engine that uses a schema to define the properties of entities within your system. This allows Cedar to warn you during the authoring phase when policies may not be valid. It is a best practice that you always construct a schema for your system.</p>
<p>The following schema would allow a developer to catch the unsafe usage of the attribute:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{
"": {
"entityTypes": {
"Endpoint": {
"shape": {
"type": "Record",
"attributes": {}
}
}
},
"actions": {
"Connect": {
"appliesTo": {
"resourceTypes": ["Endpoint"],
"context": {
"type": "Record",
"attributes": {
"viaAdminNetwork": { "type": "Boolean", "required": false }
}
}
}
}
}
}
}
</code></pre></div></div>
<p>Where possible, the inputs provided by the context object should be predictable. The developer may consider always setting the <code class="language-plaintext highlighter-rouge">viaAdminNetwork</code> key to simplify.</p>
<p>Alternatively, we can also modify the policy to test for the presence of the key itself, as shown:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>permit(
principal,
action,
resource
);
forbid(
principal,
action,
resource
) unless {
context has "viaAdminNetwork" && context.viaAdminNetwork == true
};
</code></pre></div></div>
<p>Developers might also consider overriding an allow result if any evaluation errors are present in the evaluation response, if that outcome is more desirable.</p>
<h2 id="dangers-of-short-circuiting">Dangers of short-circuiting</h2>
<p>Short-circuiting is a performance feature of the Cedar language which allows it to skip evaluation of specific expressions that should not affect the result of the policy evaluation. It is present under the following conditions:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">expression1 && expression2</code>: expression2 is not evaluated when expression1 is false</li>
<li><code class="language-plaintext highlighter-rouge">expression1 || expression2</code>: expression2 is not evaluated when expression1 is true</li>
<li><code class="language-plaintext highlighter-rouge">if expression1 then expression2 else expression3</code>: expression2 is not evaluated when expression1 is false and expression3 is not evaluated when expression1 is true</li>
</ul>
<p>This is typically a good thing, however it will <em>not</em> produce an error due to an invalid expression unless it actually evaluates that expression. For example, consider the below policy:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>permit (
principal,
action == Action::"login",
resource
)
when { context.isPrimarySite == true || principal.isBreakGlasEntity == true };
</code></pre></div></div>
<p>Note that this policy has the typo <code class="language-plaintext highlighter-rouge">isBreakGlasEntity</code>, which is missing an ‘s’. The intention behind the policy is that the login action is permitted only when accessing from the primary site under normal conditions, or if the principal is a special “break glass” entity under any conditions. This policy works under normal conditions, but due to the typo will error and not permit the break glass entity when they are most needed.</p>
<h3 id="solutions-2">Solutions</h3>
<p>A Cedar schema should again be used to determine the valid entity attributes during the entity modelling process and warn of inconsistencies during the policy authoring phase.</p>
<p>The following Cedar schema should be used to help find the typo during the authoring time of the policy:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{
"": {
"entityTypes": {
"User": {
"shape": {
"type": "Record",
"attributes": {
"isBreakGlassEntity": { "type": "Boolean", "required": true }
}
}
}
},
"actions": {
"login": {
"appliesTo": {
"principalTypes": [ "User" ],
"context": {
"type": "Record",
"attributes": {
"isPrimarySite": { "type": "Boolean", "required": true }
}
}
}
}
}
}
}
</code></pre></div></div>
<p>In addition to schema validation, it is also important to perform positive and negative testing against your policies (in a local or non-production environment) to ensure the policies will act in the way you expect for critical paths.</p>
<h2 id="ambiguous-entity-type">Ambiguous entity type</h2>
<p>When writing condition statements which interact with an entity store, entities don’t have an inherit type associated with them. Consider the following entity store:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[
{
"uid": "User::\"alice\"",
"attrs": {
"active": true
}
},
{
"uid": "Action::\"redeemValidTicket\""
},
{
"uid": "Ticket::\"someTicketID\"",
"attrs": {
"active": false
}
}
]
</code></pre></div></div>
<p>and the policy:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>permit (
principal,
action == Action::"redeemValidTicket",
resource
)
when { resource.active == true };
</code></pre></div></div>
<p>The intention behind this is to allow ticketholders redeem active tickets. The implementing developer allowed the full resource entity ID (<code class="language-plaintext highlighter-rouge">"Ticket::\"someTicketID\""</code>) be passed in as the resource input. Alice can’t redeem the <code class="language-plaintext highlighter-rouge">"Ticket::\"someTicketID\""</code> resource as it is marked as not active, however Alice can perform a successful redemption with the resource entity ID <code class="language-plaintext highlighter-rouge">"User::\"alice\""</code>. Even though her user active attribute was never intended for that purpose, it nonetheless can lead to an unexpected allow.</p>
<h3 id="solutions-3">Solutions</h3>
<p>The developer could enforce that the “Ticket::” prefix is used (or perform the concatenation themselves).</p>
<p>The entity store could be modified to provide a unique attribute that the policy could match on using the <code class="language-plaintext highlighter-rouge">has</code> operator (<code class="language-plaintext highlighter-rouge">resource has "ticketIssueDate"</code>).</p>
<p>The entity store could also be modified to place tickets in a new entity type “TicketGroup” using the parents construct and enforce via policy that the resource is within this group (<code class="language-plaintext highlighter-rouge">resource in TicketGroup::"IssuedTickets"</code>).</p>
<p>Additionally, there is also a <a href="https://github.com/cedar-policy/rfcs/blob/feature/khieta/is-operator/text/0005-is-operator.md">pending RFC</a> that is discussing introducing an <code class="language-plaintext highlighter-rouge">is</code> operator to perform entity matching.</p>
<h2 id="unexpected-order-of-operations">Unexpected order of operations</h2>
<p>Like other languages, Cedar has a de-facto order of operations due to the way the <a href="https://docs.cedarpolicy.com/syntax-grammar.html">grammar</a> is constructed. This means that operations such as math works as you would expect:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>permit (
principal,
action,
resource
)
when { 1 + 2 * 3 + 4 * 5 == 27 }; // always true
</code></pre></div></div>
<p>It’s important to read and understand the grammar before constructing complex and ambiguous policies to avoid unintended effects. Consider the below policy:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>permit (
principal,
action,
resource
)
when {
if resource.owner == principal then true else false &&
resource.isRestricted == false
};
</code></pre></div></div>
<p>The intention behind the policy is to allow access when the principal is the resource owner and the resource is not restricted, however the effect of the policy is that a principal who is the resource owner is permitted access even when the resource is marked as restricted.</p>
<p>This is because the order of operations for an <code class="language-plaintext highlighter-rouge">if-then-else</code> operation is higher than that of the <code class="language-plaintext highlighter-rouge">&&</code> operation and so the evaluation of the above condition is intrinsically like so:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>if (resource.owner == principal) then (true) else (false && resource.isRestricted)
</code></pre></div></div>
<h3 id="solutions-4">Solutions</h3>
<p>Read the <a href="https://docs.cedarpolicy.com/syntax-grammar.html">grammar</a> when in doubt of the order of operations.</p>
<p>If you are ever in doubt, or simply want to be more explicit, use parentheses to explicitly show the intended grouping of operations:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>permit (
principal,
action,
resource
)
when {
(if resource.owner == principal then true else false) &&
resource.isRestricted == false
};
</code></pre></div></div>
<h2 id="side-channels">Side channels</h2>
<p>Issues can often arise from the specific implementation that surrounds the use of Cedar, whether via Amazon Verified Permissions or a direct engine implementation. The engine can only evaluate against the inputs you have provided and if those inputs are not sanitized or invalid, it can lead to a compromise.</p>
<p>Late last year, the popular json5 library released a <a href="https://github.com/json5/json5/security/advisories/GHSA-9c47-m6qq-7p4h">security advisory</a> regarding the potential for prototype pollution. If you were to allow a user to specify their own context object, but override certain keys which were used in sensitive operations, an attacker could use this vulnerability to manipulate the inputs the Cedar engine receives.</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// userInput = '{"foo": "bar", "__proto__": {"isAdmin": true}}'</span>
<span class="kd">const</span> <span class="nx">ctx</span> <span class="o">=</span> <span class="nx">JSON5</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">userInput</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">secCheckKeysSet</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="p">[</span><span class="dl">'</span><span class="s1">isAdmin</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">isMod</span><span class="dl">'</span><span class="p">]))</span> <span class="p">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="dl">'</span><span class="s1">Forbidden...</span><span class="dl">'</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">return</span> <span class="nx">avpclient</span><span class="p">.</span><span class="nx">isAuthorized</span><span class="p">({</span>
<span class="dl">'</span><span class="s1">context</span><span class="dl">'</span><span class="p">:</span> <span class="nx">ctx</span><span class="p">,</span>
<span class="p">...</span>
<span class="p">});</span>
</code></pre></div></div>
<h3 id="solutions-5">Solutions</h3>
<p>As always, a healthy supply-chain security program is recommended for organizations who make heavy use of external libraries. Input sanitization is also an important step to ensure that the engine can make appropriate authorization decisions.</p>
<p>As more and more built-in integrations become available, take advantage of these to shift more of the burden outside of your responsibility and avoid side-channel issues.</p>
<h2 id="wrapping-up">Wrapping up</h2>
<p>As new language bindings, AWS integrations, external integrations, and even <a href="https://github.com/cedar-policy/rfcs/pulls">changes to the Cedar language itself</a> continue to be produced, the overall community and ecosystem is growing. The scenarios above highlight the importance of a solid understanding of the language, but also solutions to help you overcome these hurdles and scale your authorization logic faster than would otherwise be possible.</p>
<p>If you liked what I’ve written, or want to hear more on this topic, reach out to me on Twitter at <a href="https://twitter.com/iann0036">@iann0036</a>. You can also join the discussion over at the official <a href="https://communityinviter.com/apps/cedar-policy/cedar-policy-language">Cedar Slack workspace</a>.</p>Ian MckayExploring Amazon VPC Lattice2023-04-01T00:00:00+00:002023-04-01T00:00:00+00:00https://onecloudplease.com/blog/exploring-amazon-vpc-lattice<p><img src="/images/posts/crispix.jpg" alt="" /></p>
<p><small><em>(yes, that is a picture of my <a href="https://www.kelloggs.com.au/en_AU/products/crispix-product.html">breakfast</a>)</em></small></p>
<p>Today, AWS has <a href="https://aws.amazon.com/blogs/aws/simplify-service-to-service-connectivity-security-and-monitoring-with-amazon-vpc-lattice-now-generally-available/">released</a> Amazon VPC Lattice to General Availability. This post walks through creating a simple VPC Lattice service using CloudFormation, and takes a look at the service overall.</p>
<p>VPC Lattice was my <a href="https://twitter.com/iann0036/status/1599318778709704711">#1 favourite announcement</a> of AWS re:Invent 2022, so I’m excited to see it released today. As of the time of writing, it’s available in US East (Ohio), US East (N. Virginia), US West (Oregon), Asia Pacific (Singapore), Asia Pacific (Sydney), Asia Pacific (Tokyo), and Europe (Ireland).</p>
<h2 id="how-it-works">How it works</h2>
<p>VPC Lattice is a service that enables you to connect clients to services within a VPC. It is very similar to AWS PrivateLink (also known as private VPC Endpoints), but with a key difference.</p>
<p>Whilst PrivateLink works by placing Elastic Network Interfaces within your subnet, which your clients can hit to tunnel network traffic through to the destination service, VPC Lattice works by exposing endpoints as link-local addresses. <a href="https://en.wikipedia.org/wiki/Link-local_address">Link-local addresses</a> are (generally) only accessible by software that runs on the client instance itself.</p>
<p>AWS has carved out the range <code class="language-plaintext highlighter-rouge">169.254.171.0/24</code> for VPC Lattice’s use, typically routing directly to <code class="language-plaintext highlighter-rouge">169.254.171.0</code> (there’s also an IPv6 equivalent). This is not the first network that AWS exposes via link-local addresses. You may know of:</p>
<ul>
<li>EC2’s Instance Metadata Service, which is located at <code class="language-plaintext highlighter-rouge">169.254.169.254</code></li>
<li>Route 53’s DNS Resolver, which is located at <code class="language-plaintext highlighter-rouge">169.254.169.253</code></li>
<li>ECS’s Task Metadata Endpoint, which is located at <code class="language-plaintext highlighter-rouge">169.254.170.2</code></li>
<li>Amazon Time Sync Service (NTP), which is located at <code class="language-plaintext highlighter-rouge">169.254.169.123</code></li>
</ul>
<p>Generally, these endpoints are automatically available to clients within the VPC network without any special routing or security rules. VPC Lattice differs from this slightly, as it requires Security Groups and NACLs to allow traffic to and from the VPC Lattice data plane at <code class="language-plaintext highlighter-rouge">169.254.171.0/24</code> on whichever port the destination service exposes. I was pretty surprised by this requirement when I saw it as it’s the first link-local address to need this, but it does give network administrators some basic control. Generally, it’s advised to use a <a href="https://docs.aws.amazon.com/vpc-lattice/latest/ug/security-groups.html#managed-prefix-list">managed prefix list</a> instead of the exact range above, as it’s subject to change.</p>
<p>Targets which VPC Lattice connects to closely match that of load balancing target groups, including EC2 instances, VPC IP addresses (both IPv4 and IPv6), Lambda functions, and ALBs. An EKS-specific target type is in private beta as of the time of writing.</p>
<h2 id="a-walkthrough">A walkthrough</h2>
<p><img src="/images/posts/vpclattice.drawio.png" alt="" /></p>
<p>For this walkthrough, we’ll discuss the various components needed for a VPC Lattice setup. For simplicity, we’ll be creating a Lambda function as a client (initiates a HTTPS request), and another Lambda function as a server (responds to the HTTPS request). If you want to skip ahead, here’s the <a href="https://github.com/iann0036/vpc-lattice-demo/blob/main/template.yaml">completed template</a>.</p>
<p>Let’s begin by creating a basic VPC. The VPC will have two private subnets, but we won’t add any direct routing between them. For simplicity, we’ll also skip adding Network ACLs.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Resources:
# Basic VPC
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
EnableDnsHostnames: true
EnableDnsSupport: true
PrivateSubnet1:
Type: AWS::EC2::Subnet
Properties:
CidrBlock: 10.0.0.0/24
MapPublicIpOnLaunch: false
VpcId: !Ref VPC
Tags:
- Key: Name
Value: Private Subnet (Source Subnet)
AvailabilityZone: !Select
- 0
- Fn::GetAZs: !Ref AWS::Region
PrivateSubnet2:
Type: AWS::EC2::Subnet
Properties:
CidrBlock: 10.0.1.0/24
MapPublicIpOnLaunch: false
VpcId: !Ref VPC
Tags:
- Key: Name
Value: Private Subnet (Destination Subnet)
AvailabilityZone: !Select
- 1
- Fn::GetAZs: !Ref AWS::Region
RouteTablePrivate1:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: Private Route Table (Source Subnet)
RouteTablePrivate1Association1:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref RouteTablePrivate1
SubnetId: !Ref PrivateSubnet1
RouteTablePrivate2:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: Private Route Table (Destination Subnet)
RouteTablePrivate2Association1:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref RouteTablePrivate2
SubnetId: !Ref PrivateSubnet2
</code></pre></div></div>
<p>Next, we’ll create the service itself. The service will be a Lambda function which performs a basic successful response to any requests, whilst including it’s own event payload in its response body. The function will be within the second private subnet within the VPC, and its security group will only have a single inbound rule from the VPC Lattice service on the port in which it serves.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> # Inbound Lambda (Service)
InboundLambdaFunctionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Action: sts:AssumeRole
Effect: Allow
Principal:
Service: lambda.amazonaws.com
Policies:
- PolicyName: root
PolicyDocument:
Statement:
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
- xray:PutTraceSegments
- xray:PutTelemetryRecords
- ec2:CreateNetworkInterface
- ec2:DescribeNetworkInterfaces
- ec2:DeleteNetworkInterface
Resource: '*'
InboundLambdaFunction:
Type: AWS::Lambda::Function
Properties:
Handler: index.handler
Role: !GetAtt InboundLambdaFunctionRole.Arn
TracingConfig:
Mode: Active
Runtime: python3.9
Timeout: 10
Code:
ZipFile: |
import os
import json
import http.client
def handler(event, context):
print(event)
return {
"statusCode": 200,
"body": json.dumps({
"success": "true",
"capturedEvent": event
}),
"headers": {
"Content-Type": "application/json"
}
}
VpcConfig:
SecurityGroupIds:
- !Ref InboundLambdaFunctionSecurityGroup
SubnetIds:
- !Ref PrivateSubnet2
InboundLambdaFunctionSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for InboundLambdaFunction
VpcId: !Ref VPC
SecurityGroupEgress: []
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 169.254.171.0/24 # should be the prefix list instead, this'll work though
GroupName: demo-inboundsg
</code></pre></div></div>
<p>Next up, we’ll create the components of the VPC Lattice service itself. This includes:</p>
<ul>
<li>The service network</li>
<li>A security group which controls which clients may access the service network</li>
<li>The service we are creating</li>
<li>A listener for the service (HTTPS on port 443)</li>
<li>A target group for the listener to point to, with an initial target of the previously created Lambda function</li>
</ul>
<p>To keep things simple, we’re not adding an auth policy for the service network or the service itself.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> # VPC Lattice
VPCLatticeServiceNetwork:
Type: AWS::VpcLattice::ServiceNetwork
Properties:
Name: demo-servicenetwork
AuthType: NONE
VPCLatticeServiceNetworkSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for service network access
VpcId: !Ref VPC
SecurityGroupEgress: []
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: !GetAtt VPC.CidrBlock
GroupName: demo-servicenetworksg
VPCLatticeServiceNetworkVPCAssociation:
Type: AWS::VpcLattice::ServiceNetworkVpcAssociation
Properties:
SecurityGroupIds:
- !Ref VPCLatticeServiceNetworkSecurityGroup
ServiceNetworkIdentifier: !Ref VPCLatticeServiceNetwork
VpcIdentifier: !Ref VPC
VPCLatticeService:
Type: AWS::VpcLattice::Service
Properties:
Name: demo-service
AuthType: NONE
VPCLatticeServiceNetworkServiceAssociation:
Type: AWS::VpcLattice::ServiceNetworkServiceAssociation
Properties:
ServiceNetworkIdentifier: !Ref VPCLatticeServiceNetwork
ServiceIdentifier: !Ref VPCLatticeService
VPCLatticeListener:
Type: AWS::VpcLattice::Listener
Properties:
Name: demo-listener
Port: 443
Protocol: HTTPS
ServiceIdentifier: !Ref VPCLatticeService
DefaultAction:
Forward:
TargetGroups:
- TargetGroupIdentifier: !Ref VPCLatticeTargetGroup
Weight: 100
VPCLatticeTargetGroup:
Type: AWS::VpcLattice::TargetGroup
Properties:
Name: demo-targetgroup
Type: LAMBDA
Targets:
- Id: !GetAtt InboundLambdaFunction.Arn
</code></pre></div></div>
<p>It’s important to note that by associating the service network to the VPC, there are routes created within the VPCs route table to correctly send traffic destined towards <code class="language-plaintext highlighter-rouge">169.254.171.0/24</code> to the VPC Lattice service.</p>
<p><img src="/images/posts/vpclattice-1.png" alt="" /></p>
<p>The target group also automatically adds a resource-based policy statement to the Lambda function for you (some other services require you to explicitly add an <code class="language-plaintext highlighter-rouge">AWS::Lambda::Permission</code>).</p>
<p><img src="/images/posts/vpclattice-3.png" alt="" /></p>
<p>Finally, we’ll create the client which will send requests to the VPC Lattice service. Again, this will be driven via a basic Lambda function. Note that this time, the security group requires an outbound rule towards the VPC Lattice service.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> # Outbound Lambda (Client)
OutboundLambdaFunctionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Action: sts:AssumeRole
Effect: Allow
Principal:
Service: lambda.amazonaws.com
Policies:
- PolicyName: root
PolicyDocument:
Statement:
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
- xray:PutTraceSegments
- xray:PutTelemetryRecords
- ec2:CreateNetworkInterface
- ec2:DescribeNetworkInterfaces
- ec2:DeleteNetworkInterface
Resource: '*'
OutboundLambdaFunction:
Type: AWS::Lambda::Function
Properties:
Handler: index.handler
Role: !GetAtt OutboundLambdaFunctionRole.Arn
TracingConfig:
Mode: Active
Runtime: python3.9
Environment:
Variables:
ENDPOINT: !GetAtt VPCLatticeServiceNetworkServiceAssociation.DnsEntry.DomainName
Timeout: 10
Code:
ZipFile: |
import os
import json
import http.client
def handler(event, context):
conn = http.client.HTTPSConnection(os.environ["ENDPOINT"])
conn.request("POST", "/", json.dumps(event), {
"Content-Type": 'application/json'
})
res = conn.getresponse()
data = res.read()
print(data.decode("utf-8"))
VpcConfig:
SecurityGroupIds:
- !Ref OutboundLambdaFunctionSecurityGroup
SubnetIds:
- !Ref PrivateSubnet1
OutboundLambdaFunctionSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for OutboundLambdaFunction
VpcId: !Ref VPC
SecurityGroupEgress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 169.254.171.0/24 # should be the prefix list instead, this'll work though
SecurityGroupIngress: []
GroupName: demo-outboundsg
</code></pre></div></div>
<p>Now that our template is done, we can deploy it via CloudFormation. If you got stuck anywhere, try the pre-made version <a href="https://github.com/iann0036/vpc-lattice-demo">here</a>.</p>
<p>Once deployed, navigate to the Lambda console and find the function named something similar to “OutboundLambdaFunction”. Create a test event using any JSON object and invoke it. You should see the results from the service come back to you by observing the logs.</p>
<p><img src="/images/posts/vpclattice-2.png" alt="" /></p>
<h2 id="a-note-on-pricing">A note on pricing</h2>
<p>It’s worth noting that the pricing model for VPC Lattice is different to that of PrivateLink and will probably end up costing you more overall. For N. Virginia, a PrivateLink service costs $0.01/hour <em>per availability zone</em>, plus $0.01/GB with volume discounts. For the same region, a VPC Lattice service costs $0.025/hour <em>regardless of AZs</em>, plus $0.025/GB with no volume discounts, plus $0.10 per million requests (with the first 300k requests per hour free).</p>
<h2 id="wrapping-up">Wrapping up</h2>
<p>I’m interested to see how architectures will evolve with this new technology. Whilst PrivateLink remains more affordable and already widespread, I can see architects reaching for this new technology to improve their security posture and reduce the load on networking engineers.</p>
<p>If you liked what I’ve written, or want to hear more on this topic, reach out to me on Twitter at <a href="https://twitter.com/iann0036">@iann0036</a>.</p>Ian MckayCedar: A new policy language2023-01-11T00:00:00+00:002023-01-11T00:00:00+00:00https://onecloudplease.com/blog/cedar-a-new-policy-language<p><img src="/images/posts/cedar-photo.png" alt="" /></p>
<p>Cedar is a new language created by AWS to define access permissions using policies, similar to the way IAM policies work today. In this post, we’ll look at why this language was created, how to author the policies, and some additional features of the language. The language was designed by the <a href="https://www.amazon.science/blog/a-gentle-introduction-to-automated-reasoning">Amazon automated reasoning team</a> for use in new services such as <a href="https://aws.amazon.com/verified-permissions/">Amazon Verified Permissions</a>, <a href="https://aws.amazon.com/verified-access/">AWS Verified Access</a> and likely other future services and integrations.</p>
<h2 id="why-write-a-new-language">Why write a new language?</h2>
<p>IAM policies, introduced <a href="https://aws.amazon.com/blogs/aws/iam-identity-access-management/">over 11 years ago</a>, have been integrated into the AWS ecosystem as the fundamental way to control both human and system access to AWS resources. IAM policies are highly optimized for AWS and have constructs (like ARNs) which make it not suitable for usage on principals and resources outside of AWS.</p>
<p>Cedar is a <em>generalist</em> language which has no implicit AWS constructs within it, and this allows it to be used as an authorization engine for non-AWS applications. This is why it’s used at the core of the Amazon Verified Permissions service, where AWS manages the policy dataset and allows systems to directly make authorization calls against the evaluation engine. Incidentally, the name “Cedar” was coined as a follow on from the internal policy language of IAM, “Balsa”.</p>
<p>Cedar is written in Rust, which makes it run in milliseconds, and was designed to be simple to reason about the effect of policies. For example, it allows for the creation of tooling which takes two policies and determines whether they are exactly equivalent, or whether there are authorization requests that would differ in the result when evaluated against each policy.</p>
<h2 id="how-it-works">How it works</h2>
<p>The policy evaluation engine for the Cedar language takes one or more policies, and evaluates whether a requested action is permitted or forbidden (allowed or denied). Cedar requires the principal making the request, the action being taken, the resource being accessed, and optionally additional request context at the time of the authorization call. Cedar also consumes the policies to be evaluated and may also use a list of entities (principals, actions and resources) that exist within your application, however these may be provided ahead of time or indirectly depending upon the service integration.</p>
<p>The request context object may be set by the requesting application or, in the case of AWS Verified Access, <a href="https://docs.aws.amazon.com/verified-access/latest/ug/trust-data-default-context.html">defined</a> by the service.</p>
<p>Cedar has a <a href="https://www.cedarpolicy.com/playground">playground</a> which allows you to play with the engine itself. It is also currently integrated into the Amazon Verified Permissions and AWS Verified Access services. As of the time of writing, Cedar is not available as an open-source or otherwise downloadable library.</p>
<h3 id="syntax">Syntax</h3>
<p>A typical Cedar policy statement looks like the following:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>permit(
principal == User::"John",
action == Action::"view",
resource
)
when {
resource in Folder::"John's Stuff" &&
context.authenticated == true
};
</code></pre></div></div>
<p>A policy can contain a number of statements by simply appending them onto the policy document. The syntax is not whitespace dependent and may be compressed into a single line. Typically, principals and resources should use immutable identifiers and not names. The examples in this post use simple names for readability purposes only.</p>
<p>The policy contains the following parts:</p>
<ol>
<li>The effect, which will always be either <code class="language-plaintext highlighter-rouge">permit</code> or <code class="language-plaintext highlighter-rouge">forbid</code></li>
<li>The scope, which specifies the principals, actions, and resources to which the effect applies</li>
<li>Optionally, condition clauses, which may either be a <code class="language-plaintext highlighter-rouge">when</code> or an <code class="language-plaintext highlighter-rouge">unless</code> condition</li>
</ol>
<p>Entities (principals, actions or resources) will always follow the format <code class="language-plaintext highlighter-rouge">TypeOfEntity::"UniqueIdentifier"</code>. The type of entity may be further namespaced, for example, <code class="language-plaintext highlighter-rouge">Company::Account::Department::Person::"John"</code>.</p>
<p>Entity types are ambiguous and not determined by their namespace. This means a single entity can be either a principal, action or resource, depending upon the specific context. The only exception is that actions must have their rightmost namespace use the keyword <code class="language-plaintext highlighter-rouge">Action</code> (i.e. <code class="language-plaintext highlighter-rouge">Action::"MyAction"</code>, <code class="language-plaintext highlighter-rouge">CustomNamespace::Action::"MyAction"</code>).</p>
<h3 id="evaluation-logic">Evaluation logic</h3>
<p>When evaluating a request, Cedar will consider all statements within the policy, and in the case of Amazon Verified Permissions, all policies provided in a policy store (as if it were one big policy). If <em>any</em> <code class="language-plaintext highlighter-rouge">forbid</code> statement matches the request, the request will be denied, regardless of any <code class="language-plaintext highlighter-rouge">permit</code> statements. If <em>at least one</em> <code class="language-plaintext highlighter-rouge">permit</code> statement matches the request (and no <code class="language-plaintext highlighter-rouge">forbid</code> statements match), the request will be allowed. If no statements match, the request will be implicitly denied.</p>
<p>If you’ve worked with AWS IAM, you’ll recognize Cedar’s policy evaluation logic is the same. This also means that ordering of statements in a policy is irrelevant and has no effect on the outcome of an authorization request.</p>
<p>Because <code class="language-plaintext highlighter-rouge">forbid</code> statements are applied universally without the ability to override, they are commonly used to craft guardrails across the entire policy store.</p>
<h3 id="the-scope">The scope</h3>
<p>The scope is written in a way that almost looks like a set of arguments in a function. It always consists of the keywords <code class="language-plaintext highlighter-rouge">principal</code>, <code class="language-plaintext highlighter-rouge">action</code> and <code class="language-plaintext highlighter-rouge">resource</code>. Each of these keywords may optionally be followed by either an <code class="language-plaintext highlighter-rouge">== Some::"Entity" </code> or an <code class="language-plaintext highlighter-rouge">in Some::"Group"</code> to scope down the principals, actions or resources in which the statement applies to. In addition, an inline set in the form <code class="language-plaintext highlighter-rouge">in [ Some::"Entity", SomeOther::"Entity", ... ]</code> can be used for the <code class="language-plaintext highlighter-rouge">action</code> keyword only. When no keywords have this suffix, the policy applies to all requests, so long as the conditions are met.</p>
<p>The scope is generally used for role-based access control, where you would like to apply policies scoped to a specific defined or set of resources, actions, principals, or combination thereof.</p>
<h3 id="condition-clauses">Condition clauses</h3>
<p>Condition clauses further limit whether a policy takes effect for the specific request. Typically policy statements will either have no condition clauses or one condition clause, however the syntax does allow for any number of condition clauses to form a statement.</p>
<p>Condition clauses are more flexible than the scope, featuring a basic set of <a href="https://docs.aws.amazon.com/verified-access/latest/ug/built-in-policy-operators.html">operators</a> to allow you to form a boolean result of acceptance based off of the principal, action, resource or context of the request, as well as the attributes or nested hierarchy of these entities where a list of entities has been defined. The use of logical operators such as <code class="language-plaintext highlighter-rouge">&&</code> and <code class="language-plaintext highlighter-rouge">||</code> allow you to form long, complex conditions to match your specific requirements. The <code class="language-plaintext highlighter-rouge">like</code> operator allows you to perform string matching with the use of a <code class="language-plaintext highlighter-rouge">*</code> wildcard character.</p>
<p>Condition clauses are intended to perform attribute-based access control. Though it is possible to include scope conditions within a condition clause, exactly the way you would in the scope, it’s recommended that you retain those scope conditions in the scope for both readability and performance reasons.</p>
<h2 id="additional-language-features">Additional language features</h2>
<p>Using the above syntax is all you need to start writing basic statements to permit or forbid access to your application, however there are some more features of the language which we’ll go through. Some of these features may not be available or useful depending upon the service in which Cedar is integrated into.</p>
<h3 id="comments">Comments</h3>
<p>Policies may contain the <code class="language-plaintext highlighter-rouge">//</code> operator to add comments, which are particularly useful for indicating an abstract identifier, for example:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// the following was added by the accounts team
// it was approved by Jane Doe
permit(
principal == User::"9a6afab1-5a37-4c90-aa40-24277b93ca28", // John Smith
action,
resource == Account::"710f18bc-b8ab-4313-b362-8e6264cfcf91" // MyCorp Dev Account
);
</code></pre></div></div>
<h3 id="entities">Entities</h3>
<p>Cedar supports accepting a list of known entities (resources, actions or principals) within a system. This is helpful as you may author policies which interact with the hierarchy or attributes of the entities within condition clauses. When an authorization request is made, the principal, action and resource identifiers will correlate to the defined entity of the same identifier when present in the entity list.</p>
<p>The structure of the entity list differs from service to service. In the Cedar playground, the entity list looks like the following:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[
{
"uid": "User::\"john\"",
"parents": [
"UserGroup::\"Staff\""
],
"attrs": {
"department": "Hardware Engineering",
"age": 30
}
},
{
"uid": "UserGroup::\"Staff\""
}
]
</code></pre></div></div>
<p>In Amazon Verified Permissions (for an <code class="language-plaintext highlighter-rouge">IsAuthorized</code> call), the same entity list would look like this:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[
{
"EntityId": {
"EntityType": "User",
"EntityId": "john"
},
"Parents": [
{
"EntityType": "UserGroup",
"EntityId": "Staff"
}
],
"Attributes": {
"department": {
"String": "Hardware Engineering"
},
"age": {
"Long": 30
}
}
},
{
"EntityId": {
"EntityType": "UserGroup",
"EntityId": "Staff"
}
}
]
</code></pre></div></div>
<p>We can use the known attributes in the entity to construct policies that permit or forbid access. For example:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>permit(
principal,
action == Action::"Access",
resource == Room::"Drinks Lounge"
) when {
principal.age >= 18
};
</code></pre></div></div>
<p>This policy allows access only when the principal has the attribute “age”, and its value is equal to or greater than the number 18. If the age attribute wasn’t set, or the principal wasn’t defined at all in the entities list, this statement wouldn’t permit access.</p>
<p>The entities can also have the concept of a hierarchy, at any nesting level, to act based on this. For example:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>permit(
principal,
action == Action::"Access",
resource == Room::"Common Area"
) when {
principal in UserGroup::"Staff"
};
</code></pre></div></div>
<p>This policy allows any entity which has a parent of the <code class="language-plaintext highlighter-rouge">UserGroup::"Staff"</code> entity access. Once again, if the entity isn’t defined or isn’t a child of <code class="language-plaintext highlighter-rouge">UserGroup::"Staff"</code>, this statement wouldn’t permit access. The <code class="language-plaintext highlighter-rouge">in</code> operator applies to both direct children, as well as all descendants of those children. Additionally, the <code class="language-plaintext highlighter-rouge">in</code> operator also applies to the referenced parent, i.e. if the principal was <code class="language-plaintext highlighter-rouge">UserGroup::"Staff"</code> in the above example the policy would permit access.</p>
<h3 id="extensions">Extensions</h3>
<p>In addition to the base data types of strings, booleans, integers and sets/arrays, Cedar supports the additional data types of IP addresses, and decimals. These two data types can only be declared using a function call-like syntax, and can only be operated on using their in-built methods. These data types are known as extensions.</p>
<p>In the case of IP addresses, the syntax looks like the following:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>permit(
principal,
action,
resource
) when {
ip(context.client_ip).isInRange("10.0.0.0/8")
};
</code></pre></div></div>
<p>The IP address type is created using the <code class="language-plaintext highlighter-rouge">ip(...)</code> syntax, and calls the <code class="language-plaintext highlighter-rouge">isInRange(...)</code> function to return a boolean. A similar effect is seen for the use of the decimal types:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>forbid(
principal,
action,
resource
) when {
decimal(context.risk_score).greaterThan(decimal("7.2"))
};
</code></pre></div></div>
<p>Because Cedar does not allow any floating point types to be passed in, inputs must be in the form of a string (i.e. “8.24”). Decimal supports up to 4 digits after the decimal point.</p>
<p>Both extensions have a number of other methods available, all of which currently return a boolean result.</p>
<h3 id="policy-templates">Policy templates</h3>
<p>Policy templates is a Cedar feature useful for applying a common policy to a large group of principals or resources. A policy template allows you to add a variable substitution to the equality operators in the scope block for the <code class="language-plaintext highlighter-rouge">principal</code> and/or <code class="language-plaintext highlighter-rouge">resource</code> keywords. A policy template by itself is not effective, but allows policies to be created by simply providing the variable values instead of duplicating the full syntax. Policies generated from policy templates will automatically update if a policy template changes. A policy template may look like this:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>permit(
principal == ?principal,
action == Action::"download",
resource in ?resource
) when {
context.mfa == true
};
</code></pre></div></div>
<p>The <code class="language-plaintext highlighter-rouge">?principal</code> and <code class="language-plaintext highlighter-rouge">?resource</code> keywords represent the variables that may be substituted. A policy created from this template would allow the principal to download all children of the resource when accessing using MFA.</p>
<h2 id="examples">Examples</h2>
<p>The following is a set of examples to help you get started and understand the language.</p>
<h3 id="allow-all">Allow all</h3>
<p><em>Policy:</em></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>permit(
principal,
action,
resource
);
</code></pre></div></div>
<p>This statement permits all requests. It may be restricted by <code class="language-plaintext highlighter-rouge">forbid</code> statements elsewhere in the policy set.</p>
<h3 id="deny-all">Deny all</h3>
<p><em>Policy:</em></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>forbid(
principal,
action,
resource
);
</code></pre></div></div>
<p>This statement forbids all requests. It cannot be overridden and renders all other statements in the policy set useless.</p>
<h3 id="specific-rbac-policy">Specific RBAC policy</h3>
<p><em>Policy:</em></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>permit(
principal == Customer::"John",
action == Action::"checkout",
resource == CheckoutCounter::"12"
);
</code></pre></div></div>
<p>This statement allows customer “John” to checkout at checkout counter 12.</p>
<h3 id="when-condition-clause">When condition clause</h3>
<p><em>Policy:</em></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>permit(
principal,
action == Action::"connectDatabase",
resource == Database::"db1"
) when {
context.port == 5432
};
</code></pre></div></div>
<p><em>Context:</em></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{
"port": 5432
}
</code></pre></div></div>
<p>This statement allows any principal to connect to database “db1”, so long as the “port” attribute in their request context is 5432.</p>
<h3 id="unless-condition-clause">Unless condition clause</h3>
<p><em>Policy:</em></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>permit(
principal,
action in [HTTPMethod::Action::"GET", HTTPMethod::Action::"POST", HTTPMethod::Action::"DELETE"],
resource
) unless {
[Viewer::"anonymous", Viewer::"unknown"].contains(principal) ||
context.waf_risk_rating >= 7
};
</code></pre></div></div>
<p><em>Context:</em></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{
"waf_risk_rating": 8.5
}
</code></pre></div></div>
<p>This statement allows any principal to perform a HTTP GET, POST or DELETE against any resource unless they are identified as an anonymous or unknown viewer or their WAF risk rating is greater than or equal to 7.</p>
<h3 id="ip-and-decimal-usage">IP and decimal usage</h3>
<p><em>Policy:</em></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>permit(
principal,
action == HTTPMethod::Action::"GET",
resource
) when {
(
// local subnet or same machine
ip(context.http_request.client_ip).isInRange(ip("10.0.0.0/8")) ||
ip(context.http_request.client_ip).isLoopback()
) &&
decimal(context.risk_score).lessThan(decimal("6.5"))
};
</code></pre></div></div>
<p><em>Context:</em></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{
"http_request": {
"client_ip": "10.0.1.54"
},
"risk_score": "4.7"
}
</code></pre></div></div>
<p>This statement allows any principal to perform a HTTP GET against any resource when their IP address is within the 10.0.0.0/8 or loopback CIDR range and the value of the string-encoded risk score is less than 6.5.</p>
<h3 id="entity-attributes">Entity attributes</h3>
<p><em>Policy:</em></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>permit(
principal,
action == SecuritySystem::Action::"swipeCardAccess",
resource == Room::"Sydney Boardroom"
) when {
principal.location like "Sydney*" ||
principal.training.contains("All Access")
};
</code></pre></div></div>
<p><em>Entities:</em></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[
{
"uid": "Employee::\"1453\"",
"attrs": {
"location": "Sydney East",
"training": [
"General"
]
}
},
{
"uid": "Employee::\"325\"",
"attrs": {
"location": "Los Angeles",
"training": [
"General",
"All Access"
]
}
}
]
</code></pre></div></div>
<p>This statement allows any principal to swipe card access to the Sydney Boardroom if their location attribute starts with “Sydney” or their training attribute contains the “All Access” item. Both employees 1453 and 325 would be permitted under this statement.</p>
<h3 id="entity-attributes-relationship">Entity attributes relationship</h3>
<p><em>Policy:</em></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>permit(
principal,
action == HTTP::Action::"GET",
resource
) when {
resource.owner == principal.username
};
</code></pre></div></div>
<p><em>Entities:</em></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[
{
"uid": "User::\"Josh\"",
"attrs": {
"username": "josh1"
}
},
{
"uid": "File::\"blogpost.txt\"",
"attrs": {
"owner": "josh1"
}
}
]
</code></pre></div></div>
<p>This statement allows any principal to HTTP GET a file which they have ownership of. The entity <code class="language-plaintext highlighter-rouge">User::"Josh"</code> would be permitted to perform a <code class="language-plaintext highlighter-rouge">HTTP::Action::"GET"</code> on the <code class="language-plaintext highlighter-rouge">File::"blogpost.txt"</code> entity.</p>
<h3 id="entity-inheritance">Entity inheritance</h3>
<p><em>Policy:</em></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>forbid(
principal,
action,
resource == Application::"oracle"
) unless {
principal in Group::"Admins"
};
</code></pre></div></div>
<p><em>Entities:</em></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[
{
"uid": "User::\"Ian\"",
"parents": [
"Group::\"Admins\"",
"Group::\"Users\""
]
}
]
</code></pre></div></div>
<p>This statement forbids any principal to perform any action against the oracle application unless they are a part of the Admins group. The entity <code class="language-plaintext highlighter-rouge">User::"Ian"</code> would be exempt from this forbid statement.</p>
<h3 id="policy-template">Policy template</h3>
<p><em>Policy Template:</em></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>permit(
principal == ?principal,
action == Action::"Connect",
resource == ?resource
);
</code></pre></div></div>
<p><em>Policy Variables:</em></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>principal: User::"Harry"
resource: VPN::"vpn1"
</code></pre></div></div>
<p>The policy created from the policy template allows the user Harry to connect to the VPN “vpn1”.</p>
<h2 id="wrapping-up">Wrapping up</h2>
<p>The Cedar language is both excitingly new and comfortingly familiar. It opens a new world of possible use cases and, of course, a new set of challenges and considerations. I look forward to seeing how the language gets used in real world scenarios and the ways people will architect their applications around the services Cedar supports.</p>
<p>A big thank you to members from the identity and automated reasoning teams for helping answer some questions I had during the creation of this post. If you liked what I’ve written, or want to hear more on this topic, reach out to me on Twitter at <a href="https://twitter.com/iann0036">@iann0036</a>.</p>Ian MckayPatching the AWS JavaScript SDK for Service Workers2022-01-11T00:00:00+00:002022-01-11T00:00:00+00:00https://onecloudplease.com/blog/patching-the-aws-js-sdk<p><img src="/images/posts/nodejssw.png" alt="" /></p>
<p>The AWS JavaScript SDK supports Node.js, React Native and web browsers, but what if you’re running in a <a href="https://developers.google.com/web/fundamentals/primers/service-workers">service worker</a>? In this post, I’ll explain how I modified version 2 of the AWS JavaScript SDK to run within a service worker context.</p>
<h2 id="background">Background</h2>
<p>For the <a href="https://onecloudplease.com/project/former2">Former2</a> project, I produce browser extensions for most major browsers in order to bypass the lack of CORS for the <a href="https://github.com/aws/aws-sdk-js/blob/master/SERVICES.md">majority</a> of AWS services. This means that I embed a copy of the AWS JavaScript SDK in order to make the calls needed via the browser extension, which has authority to ignore the lack of CORS.</p>
<p>The browser extensions use a “manifest”, which details the functionality of the extension and what actions are permitted. Google is <a href="https://developer.chrome.com/docs/extensions/mv3/mv2-sunset/">sunsetting</a> version 2 of the manifest for Google Chrome and requires all extensions to move to manifest version 3 by the end of 2022. Along with some <a href="https://developer.chrome.com/docs/extensions/mv3/intro/mv3-migration/">structural</a> differences, one of the major changes required is to move from background pages (logic that runs in the background of an extension) to service workers.</p>
<p>Service workers (which are a subset of JavaScript workers) have greater limitations than background pages, including the lack of access to the DOM and its features, as well as the replacement of <a href="https://developer.mozilla.org/en-US/docs/Web/API/Worker">XMLHttpRequest for fetch</a>. Service workers will also move to an inactive state if unused in a short period of time, meaning initialized variable data isn’t persisted, though I’ve skipped talking about my specific remediations to this in this article (hint: use IndexedDB).</p>
<h2 id="the-challenge">The Challenge</h2>
<p>Version 3 of the AWS JavaScript SDK is written in a way that it’s supported in a service worker context, but version 2 does not due to a variety of reasons. If you’re already using version 3 of the SDK, or are starting development on a service worker from scratch using version 3, you won’t have a problem.</p>
<p>As the Former2 project heavily relies on the syntax of version 2 of the SDK, as well as the fact that the service calls a majority of available services in the SDK, I wanted to avoid a migration effort to version 3 of the SDK. Others with existing projects making heavy use of SDK version 2 that are seeking to move to service workers (or <a href="https://workers.cloudflare.com/">CloudFlare Workers</a>) might also benefit from this.</p>
<p>Note that this is not an official change, and these changes could break current or future functionality in unintended ways, so I don’t recommend you use this in a production context.</p>
<h2 id="attempting-to-import">Attempting to import</h2>
<p>After performing the changes to the browser extension manifest, my first issue was that the SDK script could no longer be directly loaded into the shared DOM model.</p>
<p><strong>Before:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>"background": {
"scripts": [
"aws-sdk-2.1046.0.js",
"bg.js"
]
},
</code></pre></div></div>
<p><strong>After:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>"background": {
"service_worker": "bg.js"
},
</code></pre></div></div>
<p>Service workers come with a way to load scripts using the <a href="https://developer.mozilla.org/en-US/docs/Web/API/WorkerGlobalScope/importScripts">importScripts()</a> function. So I added the following to the top of my <code class="language-plaintext highlighter-rouge">bg.js</code> script:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>importScripts("aws-sdk-2.1046.0.js");
</code></pre></div></div>
<p>This addition now silently failed the AWS calls I requested the extension make, without much debugging information.</p>
<p>It’s at this point that I’d like to call out <a href="https://github.com/sk16">Saurav Kushwaha</a> for his <a href="https://github.com/aws/aws-sdk-js/issues/1902">prior work</a> in this area, which overrides the XHRClient class used in the AWS namespace with <a href="https://github.com/iann0036/aws-sdk-serviceworker/blob/master/lib/http/xhr.js#L48">fetch</a>. I did need to perform a couple of slight modifications to properly return correct error codes however.</p>
<p>After replacing the XHRClient class, I was happy to see that some calls were successfully returning, but for some reason there was still some failures.</p>
<h2 id="xml-is-hard">XML is hard</h2>
<p>The failures I was seeing were coming from STS and S3, and I quickly realised that these were APIs that returned XML-based responses.</p>
<p>One immediate problem that actually showed error logs was that <code class="language-plaintext highlighter-rouge">window</code> was not defined, where parts of the SDK expected it to be available.</p>
<p><img src="/images/posts/sw1.png" alt="" /></p>
<p>I quickly added a one-liner to make that available during initialisation:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>if(!window){var window = {}};
</code></pre></div></div>
<p>After that change, I was now receiving an error that it could not load the XML parser.</p>
<p><img src="/images/posts/sw2.png" alt="" /></p>
<p>Digging into the SDK, the logic looked like the following:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>if (window.DOMParser) {
// use the native DOM parser library
} else if (window.ActiveXObject) {
// use the ActiveXObject to parse, a fallback for IE8 and lower
} else {
throw new Error("Cannot load XML parser");
}
</code></pre></div></div>
<p>The SDK relies on the native DOM parser to interpret XML responses from those services, so in order to alleviate this I decided to find a polyfill to replace it. I came across <a href="https://www.npmjs.com/package/@xmldom/xmldom">xmldom</a> module on npm and found it suitable for my needs. I did need to bundle this into a browser-compatible library, so used <a href="https://github.com/browserify/browserify">browserify</a> to achieve this.</p>
<p>After importing the new DOM parser library for use by the SDK, I re-tested the calls which produced a valid response end-to-end. All done, or so I thought.</p>
<h2 id="something-strange">Something strange</h2>
<p>Though my application now seemed to be working well, producing no errors and always returning valid responses, I noticed that many of my list calls (for example, <code class="language-plaintext highlighter-rouge">S3.ListBucket</code>) weren’t returning the resources within my account I expected.</p>
<p>I suspected some issues with the XML parser and dumped both the response of the HTTP call, and the object immediately after xmldom had parsed it. Both of these correctly showed the bucket names I was expecting, yet the response produced an empty array.</p>
<p><img src="/images/posts/sw3.png" alt="" /></p>
<p><img src="/images/posts/sw4.png" alt="" /></p>
<p>This one hurt my head. After debugging for probably a few hours, I found the issue. During the process of constructing the response in a clean format, the SDK requests the properties <a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/firstElementChild"><code class="language-plaintext highlighter-rouge">Element.firstElementChild</code></a> and <a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/nextElementSibling"><code class="language-plaintext highlighter-rouge">Element.nextElementSibling</code></a> from the parsed object, however xmldom <a href="https://github.com/xmldom/xmldom/issues/328">had not yet implemented</a> these properties and so the iterators were silently failing.</p>
<p>After having a look at the xmldom library to investigate whether it could be easily patched, I instead simply implemented these properties as methods directly and replaced the SDK code which accesses these properties with my implementation, as shown below:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>function getFirstElementChild(xml) {
for (var i = 0; i < xml.childNodes.length; i++) {
if (xml.childNodes[i].hasOwnProperty('tagName')) {
return xml.childNodes[i];
}
}
return null;
}
function getNextElementSibling(xml) {
var foundSelf = false;
for (var i = 0; i < xml.parentNode.childNodes.length; i++) {
if (xml.parentNode.childNodes[i] === xml) {
foundSelf = true;
continue;
}
if (foundSelf && xml.parentNode.childNodes[i].hasOwnProperty('tagName')) {
return xml.parentNode.childNodes[i];
}
}
return null;
}
</code></pre></div></div>
<h2 id="wrapping-up">Wrapping up</h2>
<p>After all the above changes were made, I was able to produce a version of the version 2 SDK which, from all the tests I’ve made, seems to work as intended within a service worker context.</p>
<p>I’ve made a version of the service worker-compatible SDK available on <a href="https://github.com/iann0036/aws-sdk-serviceworker">GitHub</a>, should you want to compile your own. Refer to the <a href="https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/building-sdk-for-browsers.html">official docs</a> for specific compilation options, as they should work the same.</p>
<p>I got pretty close to abandoning this experiment, but I’m glad I persisted. I learned a lot about the internals of the SDK and got a working alternative in the end. If you liked what I’ve written, or want to tell me how terrible of an idea this was, reach out to me on Twitter at <a href="https://twitter.com/iann0036">@iann0036</a>.</p>Ian MckayMigrating to OpenSearch with CloudFormation2021-10-05T00:00:00+00:002021-10-05T00:00:00+00:00https://onecloudplease.com/blog/migrating-to-opensearch-with-cloudformation<p><img src="/images/posts/os-main.png" alt="" /></p>
<p>Last month, AWS <a href="https://aws.amazon.com/blogs/aws/amazon-elasticsearch-service-is-now-amazon-opensearch-service-and-supports-opensearch-10/">announced</a> that the Amazon Elasticsearch Service has become Amazon OpenSearch Service. This change has effectively stopped any further updates to the Elasticsearch product line within the service due to <a href="https://www.elastic.co/blog/why-license-change-AWS">changes</a> to Elastic’s licensing model, and the <a href="https://aws.amazon.com/blogs/opensource/stepping-up-for-a-truly-open-source-elasticsearch/">forked</a> OpenSearch will become the only product line to receive updates in the future.</p>
<p>In this post, we’ll walk through how to migrate from an Elasticsearch domain to an OpenSearch domain if you already have your domain defined within CloudFormation. If you don’t have your domain defined in CloudFormation but would like to, we’ll also cover that.</p>
<h2 id="the-changes">The changes</h2>
<p>Let’s go through the changes in the service. The service name itself has changed from Amazon Elasticsearch Service to Amazon OpenSearch Service or, annoyingly, its current full canonical name “Amazon OpenSearch Service (successor to Amazon Elasticsearch Service)”. The Kibana equivilent is also now known as OpenSearch Dashboards.</p>
<p><img src="/images/posts/os-2.png" alt="" /></p>
<p>The 18 Elasticsearch versions currently supported in the service will be all the Elasticsearch versions there will ever be, with only OpenSearch versions being included in the future. This also includes the possibility for the addition of features exclusive to OpenSearch, though Elastic could easily incorporate OpenSearch features into Elasticsearch if they wanted, despite the reverse no longer being an option. OpenSearch has added 3 of these exclusive features in their first version: <a href="https://opensearch.org/docs/im-plugin/index-transforms/index/">Transforms</a>, <a href="https://opensearch.org/docs/opensearch/data-streams/">Data Streams</a>, and <a href="https://opensearch.org/docs/dashboards/notebooks/">Notebooks</a>.</p>
<p><img src="/images/posts/os-1.png" alt="" /></p>
<p>Though AWS has provided an easy upgrade path from Elasticsearch to OpenSearch within the console, the same cannot be said about CloudFormation which has created a new resource type for OpenSearch. You <b>must not</b> simply update the CloudFormation type in your template, as this will lead to the deletion of your domain and all data within it.</p>
<p>We will be migrating from one stack from another in this case (which is helpful as many of you may have used “elasticsearch” or “es” in your stack name), although you could follow a similar approach entirely within an existing stack. Despite the <a href="https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/resource-import-supported-resources.html">CloudFormation docs</a> indicating OpenSearch resources are not supported for this operation (as of the time this was published), almost all resources created from about the end of 2019 should have CloudFormation import support, as is the case here.</p>
<p>Before beginning, you should first confirm that your configuration won’t be affected by the minor <a href="https://docs.aws.amazon.com/opensearch-service/latest/developerguide/version-migration.html">breaking changes</a> and you should <a href="https://docs.aws.amazon.com/opensearch-service/latest/developerguide/managedomains-snapshots.html#managedomains-snapshot-create">take a manual snapshot</a> for safety, as the process is irreversable. This is a dangerous process if you don’t take care so please follow all steps and precautions carefully if your data is critical.</p>
<h2 id="preparing-your-existing-stack">Preparing your existing stack</h2>
<p>To begin, we will prepare the existing stack to be deprecated. If you do not currently have your domain within a CloudFormation stack (but would like to), you can instead generate a template for your new domain using <a href="https://aws.amazon.com/blogs/opensource/accelerate-infrastructure-as-code-development-with-open-source-former2/">Former2</a>.</p>
<p>My existing template looks like this:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Resources:
...
MyElasticsearchDomain:
Type: AWS::Elasticsearch::Domain
Properties:
DomainName: mydomain
ElasticsearchClusterConfig:
InstanceCount: 3
InstanceType: t3.medium.elasticsearch
DedicatedMasterEnabled: true
DedicatedMasterType: t3.medium.elasticsearch
DedicatedMasterCount: 3
ZoneAwarenessEnabled: true
ZoneAwarenessConfig:
AvailabilityZoneCount: 3
EBSOptions:
EBSEnabled: true
VolumeSize: 20
VolumeType: gp2
ElasticsearchVersion: "7.10"
...
</code></pre></div></div>
<p>Your template may have a number of different properties and other resources not shown here. Take a copy of the contents of your template now, and save this for later.</p>
<p>Now, we’re going to add the <code class="language-plaintext highlighter-rouge">DeletionPolicy</code> and <code class="language-plaintext highlighter-rouge">UpdateReplacePolicy</code> attributes to the resource with the value <code class="language-plaintext highlighter-rouge">Retain</code>, like so:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Resources:
...
MyElasticsearchDomain:
DeletionPolicy: Retain
UpdateReplacePolicy: Retain
Type: AWS::Elasticsearch::Domain
Properties:
DomainName: mydomain
ElasticsearchClusterConfig:
InstanceCount: 3
InstanceType: t3.medium.elasticsearch
DedicatedMasterEnabled: true
DedicatedMasterType: t3.medium.elasticsearch
DedicatedMasterCount: 3
ZoneAwarenessEnabled: true
ZoneAwarenessConfig:
AvailabilityZoneCount: 3
EBSOptions:
EBSEnabled: true
VolumeSize: 20
VolumeType: gp2
ElasticsearchVersion: "7.10"
...
</code></pre></div></div>
<p>By doing this, we are telling CloudFormation to not touch the connected resource (the domain) when this resource is deleted from the stack. In my case, I also had a number of CloudWatch alarms and even some custom resources for index and search template creation defined in my stack. These aren’t important to me during this short migration exercise, so I simply deleted them outright from my template as they will be recreated based on the template copy we took previously. My template now consists of only the <code class="language-plaintext highlighter-rouge">AWS::Elasticsearch::Domain</code> resource, as well as any parameters and outputs that existed previously.</p>
<p>Update your stack now with the new content. As expected, the supporting resources such as CloudWatch Alarms will be deleted during this process (if any), however the Elasticsearch domains remain untouched.</p>
<h2 id="upgrading-your-domain-in-place">Upgrading your domain in-place</h2>
<p>Next up, we’ll use the defined process to upgrade the domain in-place. Note this can produce a minor downtime so plan around this. My upgrade with a small amount of documents took approximately 30 minutes.</p>
<p>Open the <a href="https://console.aws.amazon.com/esv3/">AWS Management Console</a>, choose the domain that you want to upgrade, choose <strong>Actions</strong>, and then select <strong>Upgrade</strong>.</p>
<p><img src="/images/posts/os-8.png" alt="" /></p>
<p>Choose <strong>OpenSearch 1.0</strong> as the version to upgrade to, and I highly recommend selecting the <strong>Enable compatibility mode</strong> option to reduce the risk of any incompatibility issues. Check upgradeability and once verified, you can select the <strong>Upgrade</strong> operation.</p>
<p><img src="/images/posts/os-9.png" alt="" /></p>
<p>You can prepare the next steps whilst waiting for the upgrade to complete.</p>
<h2 id="preparing-your-new-template">Preparing your new template</h2>
<p>We’ll now prepare our new template for our new OpenSearch-specific stack. If you previously used <a href="https://aws.amazon.com/blogs/opensource/accelerate-infrastructure-as-code-development-with-open-source-former2/">Former2</a> to define your stack, you’ll only need to make the <code class="language-plaintext highlighter-rouge">DeletionPolicy</code> change from the steps below.</p>
<p>Using the copy of your original template, carefully make the following adjustments:</p>
<ul>
<li>Change the domain resource type from <code class="language-plaintext highlighter-rouge">AWS::Elasticsearch::Domain</code> to <code class="language-plaintext highlighter-rouge">AWS::OpenSearchService::Domain</code></li>
<li>Add the <code class="language-plaintext highlighter-rouge">DeletionPolicy</code> and <code class="language-plaintext highlighter-rouge">UpdateReplacePolicy</code> attributes to the resource, as previously performed</li>
<li>In the domain resource, change the <code class="language-plaintext highlighter-rouge">ElasticsearchVersion</code> property to <code class="language-plaintext highlighter-rouge">EngineVersion</code> and set its value to <code class="language-plaintext highlighter-rouge">OpenSearch_1.0</code></li>
<li>In the domain resource, change the <code class="language-plaintext highlighter-rouge">ElasticsearchClusterConfig</code> property to <code class="language-plaintext highlighter-rouge">ClusterConfig</code>, if set</li>
<li>In the domain resource, for everywhere an instance type is defined (<code class="language-plaintext highlighter-rouge">InstanceType</code> and <code class="language-plaintext highlighter-rouge">DedicatedMasterType</code>), change the <code class="language-plaintext highlighter-rouge">.elasticsearch</code> suffix to <code class="language-plaintext highlighter-rouge">.search</code></li>
<li>In the domain resource, under <code class="language-plaintext highlighter-rouge">ClusterConfig</code>, if you have specified <code class="language-plaintext highlighter-rouge">ColdStorageOptions</code> you must remove it as it is not currently supported</li>
<li>If there are any Fn::GetAtt / !GetAtt references to your domains <code class="language-plaintext highlighter-rouge">DomainArn</code> (i.e. <code class="language-plaintext highlighter-rouge">!GetAtt MyDomain.DomainArn</code>), change these to instead use <code class="language-plaintext highlighter-rouge">!GetAtt MyDomain.Arn</code></li>
<li>Replace the domains logical ID and references to it, if needed due to naming conventions</li>
<li>Comment out any resources not currently within the current stack (probably everything but the <code class="language-plaintext highlighter-rouge">AWS::OpenSearchService::Domain</code>)</li>
<li>Rename any output export names to be unique within the region, if needed (we will update references to these later)</li>
<li>Update the template <code class="language-plaintext highlighter-rouge">Description</code> and any comments to remove Elasticsearch references, if needed</li>
</ul>
<p>My new template looks like this:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Resources:
...
MyOpenSearchDomain:
DeletionPolicy: Retain
UpdateReplacePolicy: Retain
Type: AWS::OpenSearchService::Domain
Properties:
DomainName: mydomain
ClusterConfig:
InstanceCount: 3
InstanceType: t3.medium.search
DedicatedMasterEnabled: true
DedicatedMasterType: t3.medium.search
DedicatedMasterCount: 3
ZoneAwarenessEnabled: true
ZoneAwarenessConfig:
AvailabilityZoneCount: 3
EBSOptions:
EBSEnabled: true
VolumeSize: 20
VolumeType: gp2
EngineVersion: "OpenSearch_1.0"
...
</code></pre></div></div>
<h2 id="importing-the-new-stack">Importing the new stack</h2>
<p>Once you have confirmed the in-place upgrade has completed, and prepared your new stack for import, we can import the new stack. You can check the Logs tab in your domain to confirm the upgrade.</p>
<p><img src="/images/posts/os-7.png" alt="" /></p>
<p>Open the <a href="https://console.aws.amazon.com/cloudformation/">CloudFormation console</a> and use the <strong>Create stack</strong> > <strong>With existing resources (import resources)</strong> option. Upload your newly prepared template with the <code class="language-plaintext highlighter-rouge">AWS::OpenSearchService::Domain</code> resource within it.</p>
<p><img src="/images/posts/os-4.png" alt="" /></p>
<p>You’ll then be prompted for the domain name of the existing domain. Carefully copy this name from the OpenSearch console domain listing, or from the template if hardcoded. Proceed to give your new stack a name, which should be different than the one currently existing, then finalize the stack creation. As your domain is only being imported, this process should only take a moment.</p>
<p><img src="/images/posts/os-3.png" alt="" /></p>
<p>After confirming the stack creation has been successful, uncomment any related resources from the stack such as CloudWatch alarms and update your stack in-place to ensure these are put back in. I recommend also adding a tag to the stack at this point to make doubly sure that CloudFormation is aware of the resource and is targetting it correctly. You can optionally also remove the <code class="language-plaintext highlighter-rouge">DeletionPolicy</code> and <code class="language-plaintext highlighter-rouge">UpdateReplacePolicy</code> attributes at this time, however they are a good safety feature for preventing against accidental deletions so I do recommend you leave them in place.</p>
<p><img src="/images/posts/os-5.png" alt="" /></p>
<h2 id="cleaning-up">Cleaning up</h2>
<p>We now have both the old and the new stack in place, so let’s get rid of the old one. You can ignore this part entirely if you did not have an existing stack prior.</p>
<p>If you have any stacks that are dependant on a CloudFormation output export from the original, you can now update those stacks to point to the new export name you defined.</p>
<p>Once you have updated all of the export references, you may delete the older Elasticsearch stack. Before doing this, double check the template for the stack you are deleting only contains a single <code class="language-plaintext highlighter-rouge">AWS::Elasticsearch::Domain</code> resource, and that it has at least <code class="language-plaintext highlighter-rouge">DeletionPolicy: Retain</code> at the same level as <code class="language-plaintext highlighter-rouge">Type</code> and <code class="language-plaintext highlighter-rouge">Properties</code>. If you have missed any export values, the stack events will inform you of this during the stack deletion, which you can remediate and reattempt the deletion.</p>
<p>Once the stack is deleted you’re done.</p>
<p>I hope this has been a helpful resource for you to upgrade your domain. Reach out to me on Twitter at <a href="https://twitter.com/iann0036">@iann0036</a> if you liked this post. Happy searching!</p>Ian MckayRecommendations for Working with IAM - Permissions Boundaries and Conditions2021-05-06T00:00:00+00:002021-05-06T00:00:00+00:00https://onecloudplease.com/blog/iam<p><img src="/images/posts/iam-anniversary.png" alt="" /></p>
<p>To celebrate AWS Identity and Access Management (IAM)’s 10th anniversary, I talk about two powerful ways that you can limit access to Amazon Web Services (AWS); Permissions Boundaries and Conditions.</p>
<p>Using permissions boundaries and conditions is an effective way to limit access. By letting you set the maximum permissions for a user or role, permissions boundaries can be used for situations like granting someone limited permissions management abilities.</p>
<p>Conditions enable you to specify when a policy statement is enforced, providing fine-grained access through variables such as tag value, time, and IP address. Using these IAM features will help you in your pursuit of least privilege on AWS.</p>
<p><em>Read more on the AWS Partner Network Blog by <a href="https://aws.amazon.com/blogs/apn/top-recommendations-for-working-with-iam-from-our-aws-heroes-part-3-permissions-boundaries-and-conditions/">clicking here</a>.</em></p>Ian MckayCase of the doppelgänger AWS account2021-02-12T00:00:00+00:002021-02-12T00:00:00+00:00https://onecloudplease.com/blog/case-of-the-doppleganger-aws-account<p><img src="/images/posts/aws-double-headline.png" alt="" /></p>
<p>Recently, I discovered via a Twitter thread that a single address can be the root email on two AWS accounts at the same time. How is this possible? Could my account be compromised as a result?</p>
<p>Let’s take a look at the thread in question:</p>
<p><iframe id="twitter-widget-0" scrolling="no" frameborder="0" allowtransparency="true" allowfullscreen="true" class="" style="visibility: visible; width: 550px; height: 490px; display: block; flex-grow: 1;" title="Twitter Tweet" src="https://platform.twitter.com/embed/Tweet.html?dnt=false&embedId=twitter-widget-0&frame=false&hideCard=false&hideThread=false&id=1357347085150949382&lang=en&origin=https%3A%2F%2Fonecloudplease.com%2Fblog%2Fcase-of-the-doppleganger-aws-account&theme=light&widgetsVersion=889aa01%3A1612811843556&width=550px" data-tweet-id="1357347085150949382"></iframe></p>
<h2 id="how-is-this-possible">How is this possible?</h2>
<p>It turns out the thread is technically correct, however there are a strict set of circumstances that allow for this to happen. This can only occur when you have:</p>
<ol>
<li>An AWS account that is linked to your Amazon.com retail account (which is something that could only occur before some time in 2017); and</li>
<li>Another AWS account that is <em>not</em> linked to an Amazon.com account, or another Amazon.com linked account</li>
</ol>
<p>There’s actually an easy way to check which account type you have. If you log into your account as the root user, navigate to the “My Account” dashboard and click the “Edit” button on the “Account Settings” section.</p>
<p><img src="/images/posts/aws-double-screen1.png" alt="" /></p>
<p>If the next page looks like this, you have an account that is linked to an Amazon.com account:</p>
<p><img src="/images/posts/aws-double-screen2.png" alt="" /></p>
<p>If the page instead looks like this, you have an account that is unlinked:</p>
<p><img src="/images/posts/aws-double-screen3.png" alt="" /></p>
<p>The only way to have the two AWS root accounts share the same email address is to change the Amazon.com linked account’s email address to that of the unlinked account. The reason this works is because Amazon.com authentication system is unaware of the unlinked AWS accounts and, for legacy reasons, the AWS authentication system will allow sign-ins via the Amazon.com accounts. This kind of problem <a href="https://beanstalkcomputing.com/two-amazon-accounts-with-the-same-email-address-and-different-passwords/">doesn’t seem restricted</a> to just AWS either.</p>
<h2 id="changing-the-amazoncom-account-email-address">Changing the Amazon.com account email address</h2>
<p>To do this, we first log onto the Amazon.com account and navigate to the account settings.</p>
<p><img src="/images/posts/aws-double-screen4.png" alt="" /></p>
<p>From here, go to “Login & security” then hit “Edit” on the email address field. You’ll then be prompted to set a new email address, so we enter the email address of the root user of the unlinked AWS account. This then needs to be verified via an emailed OTP code (notably not your TOTP tokens). Finally, re-enter your password to confirm the change. You have now set both AWS root accounts to the same email address.</p>
<p><img src="/images/posts/aws-double-screen5.png" alt="" /></p>
<p>This becomes slightly more interesting if you happen to have 2 Amazon.com linked accounts. If you attempt to change the email address of one to the other, you’ll actually see that there is a process to override / disable an existing Amazon.com account. As the Amazon.com login is used for root account access, in this case you would effectively lose access to one of your root accounts if you did this (so don’t!).</p>
<p><img src="/images/posts/aws-double-screen9.png" alt="" /></p>
<h2 id="but-what-now">But what now?</h2>
<p>Effectively, you have now made the root account for the Amazon.com linked AWS account inaccessible. The account will still function as normal, IAM / SSO users can still log in and make changes, however if you attempt to use log in to the root account using its password it will fail. This is because when both accounts are sharing the same email address, the unlinked account is the one that will be authenticated against, regardless of whether the passwords are different or the same.</p>
<p>You can log in to the root account of the unlinked AWS account using its password, and perform all functions as normal. You can also use the reset password feature on the email address, which will reset the password on the unlinked AWS account, leaving the Amazon.com linked account untouched.</p>
<p>If you have had your fun and want to go back to a normal world, you can simply change the Amazon.com account email address back to its original form to restore access to the root account for its associated AWS account.</p>
<h2 id="is-this-a-security-concern">Is this a security concern?</h2>
<p>No, even though it certainly feels like it. You’ll note that all the email ownership security controls still apply during this process, so to perform this you will need control of the both email addresses involved, as well as any SMS / TOTP verification that is associated on the accounts.</p>
<p>If you feel you’d rather no longer be affected by <a href="https://twitter.com/QuinnyPig/status/1275137983109308419">the underpants problem</a>, you may be able to raise a support ticket with AWS Support to migrate your account from a linked to an unlinked state. There are some implications of this, but presumably the AWS Support team will be able to explain those to you. Do take care when doing this as there are stories of people having problems with their Amazon.com accounts, such as being unable to reset or remove MFA, after this process occurs.</p>
<h2 id="tldr">tl;dr</h2>
<p>Under special circumstances, two AWS accounts could share the same root email address but it is not a security problem and will probably never happen to you.</p>Ian MckayAccelerate infrastructure as code development with open source Former22020-11-12T00:00:00+00:002020-11-12T00:00:00+00:00https://onecloudplease.com/blog/building-and-maintaining-iac-tooling-for-the-aws-community<p><img src="/images/posts/former2.png" alt="" /></p>
<p>When I first started building on AWS, like most developers, I used the <a href="https://aws.amazon.com/console/">AWS Management Console</a>. After spinning up and tearing down <a href="https://aws.amazon.com/ec2/">Amazon Elastic Compute Cloud</a> (Amazon EC2) instances manually many times, I realized that I needed a better way, so I looked to implement my solutions in <a href="https://aws.amazon.com/cloudformation/">AWS CloudFormation</a>.</p>
<p>AWS CloudFormation is an infrastructure as code (IaC) service, which means I can use text files (JSON or YAML) to define a set of resources to be deployed without worrying about the semantics of how they are deployed. Using AWS CloudFormation helps with consistency and repeatability; however, it can take time to learn the syntax of the language and build the stack templates. Luckily, there is now an open source tool called Former2 to quickly and easily make the transition.</p>
<p>Former2 is an open source project that allows you to generate IaC templates (for example, AWS CloudFormation or <a href="https://www.terraform.io/">HashiCorp Terraform</a>) from the existing resources within your AWS account. In this article, I’ll explain how I created Former2, show how to use the tool, describe the challenges faced, and share my vision for the future.</p>
<p><em>Read more on the AWS Open Source Blog by <a href="https://aws.amazon.com/blogs/opensource/accelerate-infrastructure-as-code-development-with-open-source-former2/">clicking here</a>.</em></p>Ian Mckay