<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Carlo van Overbeek</title>
    <description>The latest articles on Forem by Carlo van Overbeek (@carlovo).</description>
    <link>https://forem.com/carlovo</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F738987%2F40ee0cb7-ad16-461f-8fb4-dac93413b25c.jpg</url>
      <title>Forem: Carlo van Overbeek</title>
      <link>https://forem.com/carlovo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/carlovo"/>
    <language>en</language>
    <item>
      <title>UX designers, hear my prayer...</title>
      <dc:creator>Carlo van Overbeek</dc:creator>
      <pubDate>Fri, 14 Feb 2025 21:14:17 +0000</pubDate>
      <link>https://forem.com/carlovo/ux-designers-hear-my-prayer-3mjp</link>
      <guid>https://forem.com/carlovo/ux-designers-hear-my-prayer-3mjp</guid>
      <description>&lt;p&gt;If I could get one wish granted right now on the condition I had to be 100% sure it would benefit everyone in the world, I would ask for this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6kn433tb3sv6smr0y78k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6kn433tb3sv6smr0y78k.png" alt="Screenshot of dev.to with a loading bar at the top" width="800" height="476"&gt;&lt;/a&gt;&lt;br&gt;
(Sorry for it being ugly, I'm not a UX designer after all...)&lt;/p&gt;

&lt;p&gt;You might ask "what even is that?" or "how does that benefit anyone?" Very valid questions, but let's first take a step back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why do I want this?
&lt;/h2&gt;

&lt;p&gt;Most webpages and an increasing amount of applications these days employ some kind of background component loading. Now that's all nice and good. It becomes a nuisance when I want to click something and just at that moment the UI changes. Something in the background fully loaded, shifts or overlays the screen and causes me to miss, or worse: click something else. I literally accepted meetings in MS Outlook I wanted to decline and vice versa because of this!&lt;/p&gt;

&lt;p&gt;Now this post is not simply a rant. I understand the benefits of loading sub-components in the background and would never advise against it. Moreover, I have a possible solution for UX designers aspiring to tackle this problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The solution
&lt;/h2&gt;

&lt;p&gt;Bring back a good-old loading bar!&lt;/p&gt;

&lt;p&gt;When all data for a sub-component to render has fully loaded in the background, display a loading bar at a nice non-interfering place in the UI. It signals to the user "if you wanted to click anything, please do so now." When the time runs out, only then does the UI change. (Changing the UI immediately because of a user click would also still be acceptable, of course.)&lt;/p&gt;

&lt;p&gt;You might argue that when a (web-)app starts up, so many things have to load in the background, this would never be practical. You would just have a stream of loading bar after loading bar, annoying the user. Also fine. In that case, just display a red bar for a while to signal: "we are still busy, you may look, but click at your own risk." I would be totally okay with that. Despite of you want might think of me after reading this, I'm actually a patient person. I just don't like misclicking when it's not my fault.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Too many apps start to feel like a first-person shooter these days... Speaking of which, video games usually communicate very well when user input is or isn't desired, for example with frosting on the edges of the screen. Once again, I'm not a UX person, maybe a loading bar isn't the best solution to this problem, but some kind of signalling would be fantastic.&lt;/p&gt;

</description>
      <category>ux</category>
      <category>ui</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Straight to the Money 💰 minimalistic yet all-inclusive Python project template</title>
      <dc:creator>Carlo van Overbeek</dc:creator>
      <pubDate>Mon, 02 Dec 2024 21:49:25 +0000</pubDate>
      <link>https://forem.com/carlovo/straight-to-the-money-minimalistic-yet-all-inclusive-python-project-template-4633</link>
      <guid>https://forem.com/carlovo/straight-to-the-money-minimalistic-yet-all-inclusive-python-project-template-4633</guid>
      <description>&lt;p&gt;Stop wasting time in analysis paralysis over Python tooling choices 💸&lt;br&gt;
Pick the defaults 💱&lt;br&gt;
Go straight to the money 💹&lt;/p&gt;

&lt;p&gt;tl;dr: I created a minimalistic yet all-inclusive Python project template.&lt;br&gt;
&lt;a href="https://carlovo.github.io/straight_to_the_money/" rel="noopener noreferrer"&gt;Check it out.&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Background Story
&lt;/h2&gt;

&lt;p&gt;When it comes to helping others with developing software, too many templates/guides/peoples focus on features, or even worse, tools. While my guided template extremely spotlights some tools, they are approached from a very different focus: needs. Every software development project has almost identical needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;version control&lt;/li&gt;
&lt;li&gt;dependency management&lt;/li&gt;
&lt;li&gt;linting&lt;/li&gt;
&lt;li&gt;testing&lt;/li&gt;
&lt;li&gt;compiling&lt;/li&gt;
&lt;li&gt;documentation&lt;/li&gt;
&lt;li&gt;publishing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Of course, this list is not exhaustive. Software development these days also needs CI/CD, SBOM exports, ergonomic chairs etc. However, if anything from the list above is missing in your project, something is truly lacking. With this template I'm trying to fill in those needs as simple yet effective as possible.&lt;/p&gt;

&lt;p&gt;Some fanatics will look at this template and might say something like: "Isn't pytest better than unittest?" Or: "Isn't tox also super useful?" Well, are those extra dependencies that will only really be useful to people who already know how to add them to this template? Guess what, all three yes!&lt;/p&gt;

&lt;p&gt;Similarly silly discussions can be had about Hatch, MkDocs or Sphinx. I've had it with all these well-meaning 'developer advocates' forcing some tool on you at first chance, while no-one out there seems to be able to provide you a comprehensive end-to-end developer journey. Also, by keeping the template simple, it's open to extension with your own favorite tools without carrying the bloat from my choices.&lt;/p&gt;

&lt;p&gt;Happy coding 🐍🤗&lt;/p&gt;

</description>
      <category>python</category>
      <category>uv</category>
      <category>cookcutter</category>
      <category>template</category>
    </item>
    <item>
      <title>Input Variables File for Multiple Terraform Workspaces</title>
      <dc:creator>Carlo van Overbeek</dc:creator>
      <pubDate>Wed, 04 May 2022 13:59:40 +0000</pubDate>
      <link>https://forem.com/carlovo/input-variables-file-for-multiple-terraform-workspaces-19f7</link>
      <guid>https://forem.com/carlovo/input-variables-file-for-multiple-terraform-workspaces-19f7</guid>
      <description>&lt;p&gt;We've all been there: you have written a Terraform configuration that is so awesome, you have to apply it multiple times... Or at least to &lt;code&gt;dev&lt;/code&gt;, &lt;code&gt;test&lt;/code&gt; and &lt;code&gt;prod&lt;/code&gt;. However amazing you configuration might be, it's very rare to have all details exactly the same over every environment. You usually end up with something like this, and that's perfectly fine (for readability, I've condensed the code a bit, see &lt;a href="https://gist.github.com/Carlovo/42dcf2b2f62972891e374699552c14c2" rel="noopener noreferrer"&gt;this gist&lt;/a&gt; for a more complete example):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_instance"&lt;/span&gt; &lt;span class="s2"&gt;"this"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instance_count&lt;/span&gt;

  &lt;span class="nx"&gt;ami&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ami_id&lt;/span&gt;
  &lt;span class="nx"&gt;instance_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instance_type&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"project-xyz-&lt;/span&gt;&lt;span class="k"&gt;${terraform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workspace&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where this starts to hurt is during apply time, because you will have for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform workspace &lt;span class="k"&gt;select &lt;/span&gt;prod
terraform apply &lt;span class="nt"&gt;-var&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'instance_count=5'&lt;/span&gt; &lt;span class="nt"&gt;-var&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'ami_id=ami-1a2b3c'&lt;/span&gt; &lt;span class="nt"&gt;-var&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'instance_type=t2.large'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This line of variables becomes unclear very fast. Also, remembering all those variables or making your command line remember the statement (per environment) is rather cumbersome. Moreover, making this work in automation (again, per environment...) will most likely require some &lt;code&gt;env&lt;/code&gt; variables magic. This separates the definition of your infrastructure from the standard ways of calling it, which is bad. (By the way, if you have never heard of connascence, stop reading this article and read &lt;a href="https://en.wikipedia.org/wiki/Connascence" rel="noopener noreferrer"&gt;this one&lt;/a&gt; instead.)&lt;/p&gt;

&lt;p&gt;Terraform Cloud offers some remediation to this, but also here you create a separation between template and &lt;code&gt;env&lt;/code&gt; variable sets. It would be much better if we could keep the default sets of variables in version control next to our configuration. After some tinkering, I found the following solution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"workspace_variables"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;instance_count&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;
    &lt;span class="nx"&gt;ami_id&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
    &lt;span class="nx"&gt;instance_type&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="p"&gt;}))&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;default_vars&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lookup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workspace_variables&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;terraform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workspace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;instance_count&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="nx"&gt;ami_id&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ami-abc123"&lt;/span&gt;
    &lt;span class="nx"&gt;instance_type&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"t2.micro"&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nx"&gt;instance_count&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instance_count&lt;/span&gt; &lt;span class="err"&gt;!&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instance_count&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;default_vars&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"instance_count"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;ami_id&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ami_id&lt;/span&gt; &lt;span class="err"&gt;!&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ami_id&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;default_vars&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ami_id"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;instance_type&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instance_type&lt;/span&gt; &lt;span class="err"&gt;!&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instance_type&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;default_vars&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"instance_type"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code snippet reads one an optional variable to rule them all: &lt;code&gt;workspace_variables&lt;/code&gt;. If none is provided, it will use the default sub-variable set. Also, you can still override any set of variables with the Terraform cli. Next to this snippet, you will need to make the default for all variables &lt;code&gt;null&lt;/code&gt; and you will have to replace &lt;code&gt;var.&lt;/code&gt; with &lt;code&gt;local.&lt;/code&gt; when using them. Then you can create a Terraform vars file with something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="nx"&gt;workspace_variables&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;dev&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;instance_count&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="nx"&gt;ami_id&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ami-abc123"&lt;/span&gt;
    &lt;span class="nx"&gt;instance_type&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"t2.micro"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;test&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;instance_count&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
    &lt;span class="nx"&gt;ami_id&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ami-a1b2c3"&lt;/span&gt;
    &lt;span class="nx"&gt;instance_type&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"t2.medium"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;prod&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;instance_count&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
    &lt;span class="nx"&gt;ami_id&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ami-1a2b3c"&lt;/span&gt;
    &lt;span class="nx"&gt;instance_type&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"t2.large"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Much clearer and all kept in one findable place that can be put in version control! And you will get the configuration you want with a simple Terraform flow of selecting your workspace and hitting apply. Also, you can still override the variables with the command line for hot fixes, tests etc.:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform workspace &lt;span class="k"&gt;select &lt;/span&gt;prod
terraform apply &lt;span class="nt"&gt;-var&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'ami_id=ami-x1337x'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It may look like too much boilerplate for simple parameter input, but I've found that it weighs up to setting variables the 'normal' way if there are a lot of them. See if it works for you!&lt;/p&gt;

&lt;p&gt;Final remark: it would require some more tinkering and boilerplate to implement proper Terraform variable validation and forward compatibility of the vars file, but that might be fixed when &lt;a href="https://www.terraform.io/language/expressions/type-constraints#experimental-optional-object-type-attributes" rel="noopener noreferrer"&gt;optional variable attributes&lt;/a&gt; comes out of public beta.&lt;/p&gt;

&lt;p&gt;(The code above is condensed for readability, for a more complete example see &lt;a href="https://gist.github.com/Carlovo/42dcf2b2f62972891e374699552c14c2" rel="noopener noreferrer"&gt;this gist&lt;/a&gt;.)&lt;/p&gt;

</description>
      <category>aws</category>
      <category>terraform</category>
      <category>devops</category>
    </item>
    <item>
      <title>'Code injection' in AWS CodePipeline</title>
      <dc:creator>Carlo van Overbeek</dc:creator>
      <pubDate>Wed, 16 Feb 2022 12:56:06 +0000</pubDate>
      <link>https://forem.com/carlovo/code-injection-in-aws-codepipeline-2adj</link>
      <guid>https://forem.com/carlovo/code-injection-in-aws-codepipeline-2adj</guid>
      <description>&lt;p&gt;Now before you think I am casually going to write up here how I hacked AWS, I think CodePipeline works as intended. Still, I would like to bring a quirk to your attention in the way it's set up. This quirk can make it run different code, than the code that was put into the pipeline.&lt;/p&gt;

&lt;p&gt;In short: overwrite the source artifact in S3 in the time between CodePipeline putting it there and follow-up actions downloading it for their business. This is not mentioned in their docs. (I did mention it as feedback to them some months ago, but nothing changed and I haven't heard from them.)&lt;/p&gt;

&lt;p&gt;I could leave you with this, but it's of course more fun and insightful to give some more details.&lt;/p&gt;

&lt;h2&gt;
  
  
  Steps to reproduce
&lt;/h2&gt;

&lt;p&gt;First of, you will need to have a CodePipeline pipeline deployed. If you don't know how to set it up, see &lt;a href="https://docs.aws.amazon.com/codepipeline/latest/userguide/tutorials-simple-codecommit.html" rel="noopener noreferrer"&gt;AWS' documentation&lt;/a&gt;. I will use CodeCommit as repository source and CodeDeploy as execution environment in my example below, like in the provided AWS example. Any other combination of services that uses S3 to temporarily store the source artifact should work fine as well. (I verified that it also works with GitHub source / Lambda execution.)&lt;/p&gt;

&lt;h3&gt;
  
  
  Normal flow
&lt;/h3&gt;

&lt;p&gt;For the demo, I created a minimal CodeCommit repository with only the following &lt;code&gt;buildspec.yaml&lt;/code&gt; file in a repository called &lt;code&gt;inject&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff3nv68bxfsyxog0t9wo8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff3nv68bxfsyxog0t9wo8.png" alt="buildspec.yaml with echo hello world in CodeCommit repository" width="779" height="711"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This should make a CodeBuild server echo &lt;code&gt;hello world&lt;/code&gt;. (Normally, you would do something more interesting here, such as compiling code or running a Terraform configuration.)&lt;/p&gt;

&lt;p&gt;Then I start a pipeline that uses this repository as its source and pass it to CodeBuild:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhzw632g5i18wolc5a8d8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhzw632g5i18wolc5a8d8.png" alt="CodePipeline manual trigger #1" width="800" height="355"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As expected, this causes the following to happen in CodeBuild:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsgssvfmwye9ef8bdhh4j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsgssvfmwye9ef8bdhh4j.png" alt="CodeBuild echo hello world" width="800" height="673"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The string &lt;code&gt;hello world&lt;/code&gt; was correctly outputted.&lt;/p&gt;

&lt;p&gt;Now to the more interesting part.&lt;/p&gt;

&lt;h3&gt;
  
  
  With injected code
&lt;/h3&gt;

&lt;p&gt;Some general advice before you start: speed is key, so sharpen your AWS GUI navigation skills ;) (You could of course put &lt;a href="https://docs.aws.amazon.com/codepipeline/latest/userguide/approvals.html" rel="noopener noreferrer"&gt;a manual approval action&lt;/a&gt; in the pipeline to give yourself all the time you need, but I wanted to keep this demo as simple as possible.)&lt;/p&gt;

&lt;p&gt;Again start the pipeline:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flq6p1sdnoddd7wi0yxl2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flq6p1sdnoddd7wi0yxl2.png" alt="CodePipeline manual trigger #2" width="800" height="340"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now quickly go to the S3 bucket where the source artifacts are stored and check the name of the latest source zip file:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3wnzf76a0vsf6nc5nlbx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3wnzf76a0vsf6nc5nlbx.png" alt="S3 source bucket list of objects by creation date" width="800" height="298"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now quickly rename your code package accordingly:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgh4yl13rrfqu2objev2n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgh4yl13rrfqu2objev2n.png" alt="buildspec.yaml with echo hello friend on local machine" width="723" height="587"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note the different string to be echoed.&lt;/p&gt;

&lt;p&gt;Upload that file to the bucket used as source, overwriting the original source file (to save time you could already setup this window, don't forget to use the correct settings for KMS etc.):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5ox8k2sr5k8bi7ftunj1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5ox8k2sr5k8bi7ftunj1.png" alt="S3 upload file" width="800" height="718"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now watch how CodeBuild executes the code just uploaded instead of what's in the repository:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz7kbhim2bzc47lvhk0p8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz7kbhim2bzc47lvhk0p8.png" alt="CodeBuild echo hello friend" width="800" height="677"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing remarks
&lt;/h2&gt;

&lt;p&gt;Being able to make a pipeline execute different code than that what was put into it is of course already undesirable by itself. What I think that makes it a more serious problem in this case is that it can be done via a completely different service (S3) which is usually used for all sorts of stuff. Editing the code to be executed in S3 can easily go unnoticed in an account hosting a score of different microservices or one with a lot of users. Also, in such a case the chance of a single service or user being hacked that is privileged enough to edit S3 is way higher than the chance that someone hacks their way up to a privilege to edit a pipeline directly.&lt;/p&gt;

&lt;p&gt;Again, I would like to emphasize that this is not really a hack into CodePipeline. Still, I think CodePipeline is trying its best to hide all its shifting around with roles and artifacts from you. That is of course great for simplicity, but it might also lead to a security hole in your architecture when you are not careful or simply overlook it. It would be nice if CodePipeline would support pointing to a specific object version in the future, those have a stable unique id. For now, all you can do is protect the source artifact bucket at least as good as your pipeline. If you would like some inspiration on protecting your S3 buckets to the most paranoid level, I wrote a blog about that as well: &lt;a href="https://dev.to/carlovo/read-only-buckets-in-shared-aws-accounts-3922"&gt;Read-only buckets in shared AWS accounts&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I wrote this blog and performed the work described within it at &lt;a href="https://www.simacan.com/" rel="noopener noreferrer"&gt;Simacan&lt;/a&gt;, my employer at the time of writing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>security</category>
      <category>devops</category>
    </item>
    <item>
      <title>Read-only buckets in shared AWS accounts</title>
      <dc:creator>Carlo van Overbeek</dc:creator>
      <pubDate>Wed, 16 Feb 2022 10:49:29 +0000</pubDate>
      <link>https://forem.com/carlovo/read-only-buckets-in-shared-aws-accounts-3922</link>
      <guid>https://forem.com/carlovo/read-only-buckets-in-shared-aws-accounts-3922</guid>
      <description>&lt;p&gt;You probably know that these days AWS advises you to create a separate account for every team/env/component/etc. I am certain you know that best practices and reality often differ. Maybe two teams need access to a shared component in one AWS account. Maybe the account still stems from the time AWS advised against too many accounts and refactoring to separate accounts is too cumbersome. Even if your account contains a single component managed by a single team, the component might encompass multiple services and you want to improve on defense in depth.&lt;/p&gt;

&lt;p&gt;Whatever the reason, if you are in a situation where an AWS account is shared, it's very likely that everyone uses S3. Users may have data there that others are allowed to read, but not manipulate or even not touch at all. There are multiple ways to enforce rules like these and IAM seems like the go-to option in these cases. By the end of this post I hope you'll see things can be restricted more tightly than with IAM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best option: triple denial bucket policies
&lt;/h2&gt;

&lt;p&gt;I am not going to drone about all the possible options before finally arriving at what I think works best. I have listed other viable options with their drawbacks below.&lt;/p&gt;

&lt;p&gt;Bucket policies are your best option. Before you think "trivial answer", the exact statements are tricky and partly undocumented. Otherwise, I probably wouldn't have bothered to write this blog in the first place. The best way to start explaining the solution is by first showing the actual policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Triple-Denial-Policy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AllowOnlySpecificPrincipals"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Deny"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"NotAction"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3:List*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3:Get*"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::a-team-specific-bucket/*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::a-team-specific-bucket"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"StringNotLike"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"aws:userid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                        &lt;/span&gt;&lt;span class="s2"&gt;"AIDAXXXXXXXXXX"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                        &lt;/span&gt;&lt;span class="s2"&gt;"AROAXXXXXXXXXX:*"&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The meaning of a triple denial might be hard to grasp at first, so let me try to put into words what is happening. I think it works easiest if you read the policy from the bottom up: if you are &lt;strong&gt;not&lt;/strong&gt; one of the users with a specified &lt;code&gt;userid&lt;/code&gt; and you want to do something &lt;strong&gt;else&lt;/strong&gt; with the specified resources than &lt;code&gt;get&lt;/code&gt;/&lt;code&gt;list&lt;/code&gt;, then you are &lt;strong&gt;not&lt;/strong&gt; allowed.&lt;/p&gt;

&lt;p&gt;Interestingly, the &lt;code&gt;NotAction&lt;/code&gt; is specified in the &lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html" rel="noopener noreferrer"&gt;IAM documentation&lt;/a&gt;, but not in the &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-policy-language-overview.html" rel="noopener noreferrer"&gt;S3 documentation&lt;/a&gt;. But it works, go try it out :) Note that you still need to actually grant people read-only or more rights to this bucket and its objects, but there are already more than enough examples on how to do that, so I'll leave that out here. This statement only prevents others from ever manipulating that bucket and its data. Save the AWS root account of course, but I think that's fair.&lt;/p&gt;

&lt;p&gt;A bit about the &lt;code&gt;userid&lt;/code&gt;'s. AWS actually has a &lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html#identifiers-unique-ids" rel="noopener noreferrer"&gt;nicely concise write-up&lt;/a&gt; about those. In short, they can prevent that reusing a particular name for an IAM entity will grant unwanted access. &lt;strong&gt;So keep in mind you have to update this policy each time the team's composition changes!&lt;/strong&gt; If a team locks itself out, only &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketPolicy.html" rel="noopener noreferrer"&gt;the root account can save them&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Although it's not documented, I found that IAM roles (the &lt;code&gt;userid&lt;/code&gt;'s starting with &lt;code&gt;AROA&lt;/code&gt;) need the &lt;code&gt;:*&lt;/code&gt; appendix to work. (Maybe they are appended by a session key each time used or something?) Anyway, I haven't seen this behavior documented anywhere and it's there for both CodeBuild and CodePipeline. Maybe it's different for other services… At any rate, be warned that referencing to a &lt;code&gt;userid&lt;/code&gt; alone in an S3 bucket policy might not be enough to actually grant access to roles. By the way, be careful with granting access to the bucket to too many users and roles, you may end up with the same issues I will be addressing below when discussing IAM.&lt;/p&gt;

&lt;p&gt;Note, that the policy is easily convertible to fully deny access to all others than specified by changing the &lt;code&gt;"NotAction":[...]&lt;/code&gt; to &lt;code&gt;"Action": "s3:*"&lt;/code&gt;. Finally, I haven't tried it, but a policy like this probably also works on other services with resource policies, such as KMS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: Terraform example
&lt;/h2&gt;

&lt;p&gt;So how to get this into your infrastructure-as-code. Suppose team-a wants a bucket with objects which only they and their pipeline can possibly manipulate and other teams are present in the account. Then this could be the way to go:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_group"&lt;/span&gt; &lt;span class="s2"&gt;"team_a"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;group_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"team-a"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;team_a_and_pipeline&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_iam_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;team_a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;[*].&lt;/span&gt;&lt;span class="nx"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;team_a_pipeline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;unique_id&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket"&lt;/span&gt; &lt;span class="s2"&gt;"team_a_stuff"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"team-a-stuff"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy_document"&lt;/span&gt; &lt;span class="s2"&gt;"team_a_stuff_bucket"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;policy_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Triple-Denial-Policy"&lt;/span&gt;

  &lt;span class="c1"&gt;# No less than 3 denials in 1 statement so a little explanation:&lt;/span&gt;
  &lt;span class="c1"&gt;# This statement should prevent write operations by all users&lt;/span&gt;
  &lt;span class="c1"&gt;# except the ones specifically allowed&lt;/span&gt;
  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;sid&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AllowOnlySpecificPrincipals"&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Deny"&lt;/span&gt;
    &lt;span class="nx"&gt;principals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
      &lt;span class="nx"&gt;identifiers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;not_actions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;"s3:Get*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"s3:List*"&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="kd"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bucket_arn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bucket_arn&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/*"&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;condition&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;test&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"StringNotLike"&lt;/span&gt;
      &lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"aws:userid"&lt;/span&gt;
      &lt;span class="nx"&gt;values&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;team_a_and_pipeline&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket_policy"&lt;/span&gt; &lt;span class="s2"&gt;"team_a_stuff"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;team_a_stuff&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_iam_policy_document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;team_a_stuff_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I would suggest leaving some comments near the triple-denial statement to keep the code more maintainable. Also, as already implied above when discussing the &lt;code&gt;userid&lt;/code&gt;'s, someone from team-a will have to run this code snippet when the team composition changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other options (and their drawbacks)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  IAM
&lt;/h3&gt;

&lt;p&gt;You may rightly ask "Why all the complicated denials when a simple IAM deny on the specific resource would also suffice?" This is indeed possible, but in a shared account I would advise against it, because you are not really solving but rather shifting the problem. Who governs the IAM policies in a shared account? If you have a dedicated IAM team in that account you just created an extra burden for them. If IAM is a shared responsibility, then the IAM deny statement can be overwritten by anyone from the other team with sufficient IAM permissions, while the resource policy can only be overwritten by the accounts listed when it was created.&lt;/p&gt;

&lt;p&gt;Furthermore, IAM usually suffers from privilege creep. Also, in shared accounts privilege escalation is always lurking because of all the roles spawned by multiple teams, which when used in the correct order can probably get you to any IAM rights you want. (Even when using triple denial bucket policies, you could still override someone else's password this way to gain access, but I think getting and using those rights unnoticed is the least likely form of privilege escalation in a well-designed AWS account.)&lt;/p&gt;

&lt;p&gt;Lastly, you will have to enforce in some way that all users and roles spawned from unauthorized users also have this deny statement. Good luck. (It is possible to do that by the way, but the implementation is beyond the scope of this article and such a solution is not easier let alone clearer than a single resource policy.)&lt;/p&gt;

&lt;h3&gt;
  
  
  KMS
&lt;/h3&gt;

&lt;p&gt;You could put one single simple statement in the bucket policy enforcing that all objects put to the bucket need to be encrypted with a specific KMS key, which only certain people have access to. This works, but again you are only shifting the problem here like with IAM. If you go for this strategy, you will have to protect the bucket policy and restrict usage of the key to some specific users… And so you are back at IAM problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;If you want a bucket and its objects to be kept absolutely private to only some users/roles/root in an AWS account or at least keep others from write actions, then a tightly written bucket policy as shown above is your best option. IAM and KMS can perform the same task seemingly simpler, but keeping others from coming through the cracks will be a lot more complicated in the long run.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I wrote this blog and performed the work described within it at &lt;a href="https://www.simacan.com/" rel="noopener noreferrer"&gt;Simacan&lt;/a&gt;, my employer at the time of writing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>security</category>
      <category>terraform</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
