<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
  
  <title>Corvus Consulting</title>
  <subtitle>Notes on writing software and working with data.</subtitle>
  <link href="https://corvusconsulting.net/feed.xml" rel="self" />
  <link href="https://corvusconsulting.net/" />
  <updated>2026-10-18T00:00:00Z</updated>
  <id>https://corvusconsulting.net/</id>
  <author>
    <name>Josh Wheeler</name>
  </author>
  <entry>
    <title>Initial Foray into AWS: Tinkering with IAM Permissions</title>
    <link href="https://corvusconsulting.net/posts/iam-part1/" />
    <updated>2026-10-16T00:00:00Z</updated>
    <id>https://corvusconsulting.net/posts/iam-part1/</id>
    <content type="html">&lt;p&gt;The point of this series is to document my exploration of AWS, fumbles and all. I find that by the time someone expert in a thing gets around to writing about it, the initial confusion has long been forgotten.&lt;/p&gt;
&lt;p&gt;Querying with root credentials seemed like a bad idea. So my first step was to explore IAM (Identity and Access Management) and create a broad-ish set of permissions through which I could do my initial querying.&lt;/p&gt;
&lt;p&gt;I began using the local directory to redirect terminal outputs to files to see the general structure of a user and analyze various other data shapes.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;aws iam get-user --profile pokesandbox &amp;gt; iam-profile.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The output for my sandbox user profile looked like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;   {
    &amp;quot;User&amp;quot;: {
        &amp;quot;Path&amp;quot;: &amp;quot;/&amp;quot;,
        &amp;quot;UserName&amp;quot;: &amp;quot;PokeSandbox-Dev&amp;quot;,
        &amp;quot;UserId&amp;quot;: &amp;quot;AIDXXXXXXXXXXXXX&amp;quot;,
        &amp;quot;Arn&amp;quot;: &amp;quot;arn:aws:iam::123456789012:user/PokeSandbox-Dev&amp;quot;,
        &amp;quot;CreateDate&amp;quot;: &amp;quot;2026-10-16T10:06:32+00:00&amp;quot;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, I wanted to map out exactly what read-only commands were available to me under this profile, starting with a simple search for the &lt;code&gt;get-&lt;/code&gt; verb:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;λ aws iam help | grep get-
    get-access-key-last-used
    get-account-authorization-details
    get-account-password-policy
    get-account-summary
    get-context-keys-for-custom-policy
    get-context-keys-for-principal-policy
    get-credential-report
    get-delegation-request
    get-group
    get-group-policy
    get-human-readable-summary
    get-instance-profile
    get-login-profile
    get-mfa-device
    get-open-id-connect-provider
    get-organizations-access-report
    get-outbound-web-identity-federation-info
    get-policy
    get-policy-version
    get-role
    get-role-policy
    get-saml-provider
    get-server-certificate
    get-service-last-accessed-details
    get-service-last-accessed-details-with-entities
    get-service-linked-role-deletion-status
    get-ssh-public-key
    get-user
    get-user-policy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;While looking over this list, I realized filtering for just &lt;code&gt;get-&lt;/code&gt; was artificially limiting my visibility. In the AWS API design, &lt;code&gt;get-&lt;/code&gt; is typically reserved for fetching deep, specific details on a single object. If I wanted a complete view of my read-only constraints, I needed to expand my filter to include &lt;code&gt;list-&lt;/code&gt; (for arrays/collections of objects) and &lt;code&gt;generate-&lt;/code&gt; (for back-end auditing engine tasks).&lt;/p&gt;
&lt;p&gt;Using an extended regex filter, I ran a much wider net:&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;Full output of the extended grep (70+ lines)&lt;/summary&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;λ aws iam help | grep -E &amp;quot;get-|list-|generate-&amp;quot;
     generate-credential-report
     generate-organizations-access-report
     generate-service-last-accessed-details
     get-access-key-last-used
     get-account-authorization-details
     get-account-password-policy
     get-account-summary
     get-context-keys-for-custom-policy
     get-context-keys-for-principal-policy
     get-credential-report
     get-delegation-request
     get-group
     get-group-policy
     get-human-readable-summary
     get-instance-profile
     get-login-profile
     get-mfa-device
     get-open-id-connect-provider
     get-organizations-access-report
     get-outbound-web-identity-federation-info
     get-policy
     get-policy-version
     get-role
     get-role-policy
     get-saml-provider
     get-server-certificate
     get-service-last-accessed-details
     get-service-last-accessed-details-with-entities
     get-service-linked-role-deletion-status
     get-ssh-public-key
     get-user
     get-user-policy
     list-access-keys
     list-account-aliases
     list-attached-group-policies
     list-attached-role-policies
     list-attached-user-policies
     list-delegation-requests
     list-entities-for-policy
     list-group-policies
     list-groups
     list-groups-for-user
     list-instance-profile-tags
     list-instance-profiles
     list-instance-profiles-for-role
     list-mfa-device-tags
     list-mfa-devices
     list-open-id-connect-provider-tags
     list-open-id-connect-providers
     list-organizations-features
     list-policies
     list-policies-granting-service-access
     list-policy-tags
     list-policy-versions
     list-role-policies
     list-role-tags
     list-roles
     list-saml-provider-tags
     list-saml-providers
     list-server-certificate-tags
     list-server-certificates
     list-service-specific-credentials
     list-signing-certificates
     list-ssh-public-keys
     list-user-policies
     list-user-tags
     list-users
     list-virtual-mfa-devices
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;
&lt;h2&gt;Retrieving Policies for a user&lt;/h2&gt;
&lt;p&gt;I may want to see what policies are attached to a given user. I don&#39;t know how to use the command so I just bash it out and get some help text&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;λ aws iam list-user-policies

aws: [ERROR]: An error occurred (ParamValidation): the following arguments are required: --user-name
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After adding the username...&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;λ aws iam list-user-policies --user-name=PokeSandbox-Dev
{
    &amp;quot;PolicyNames&amp;quot;: []
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After some searching I finally came upon &lt;code&gt;aws iam list-attached-user-policies --user-name PokeSandbox-Dev&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;λ aws iam list-attached-user-policies --user-name PokeSandbox-Dev
{
    &amp;quot;AttachedPolicies&amp;quot;: [
        {
            &amp;quot;PolicyName&amp;quot;: &amp;quot;AWSXrayReadOnlyAccess&amp;quot;,
            &amp;quot;PolicyArn&amp;quot;: &amp;quot;arn:aws:iam::aws:policy/AWSXrayReadOnlyAccess&amp;quot;
        },
        {
            &amp;quot;PolicyName&amp;quot;: &amp;quot;AmazonKinesisReadOnlyAccess&amp;quot;,
            &amp;quot;PolicyArn&amp;quot;: &amp;quot;arn:aws:iam::aws:policy/AmazonKinesisReadOnlyAccess&amp;quot;
        },
        {
            &amp;quot;PolicyName&amp;quot;: &amp;quot;CloudWatchReadOnlyAccess&amp;quot;,
            &amp;quot;PolicyArn&amp;quot;: &amp;quot;arn:aws:iam::aws:policy/CloudWatchReadOnlyAccess&amp;quot;
        },
        {
            &amp;quot;PolicyName&amp;quot;: &amp;quot;IAMReadOnlyAccess&amp;quot;,
            &amp;quot;PolicyArn&amp;quot;: &amp;quot;arn:aws:iam::aws:policy/IAMReadOnlyAccess&amp;quot;
        },
        {
            &amp;quot;PolicyName&amp;quot;: &amp;quot;ViewOnlyAccess&amp;quot;,
            &amp;quot;PolicyArn&amp;quot;: &amp;quot;arn:aws:iam::aws:policy/job-function/ViewOnlyAccess&amp;quot;
        },
        {
            &amp;quot;PolicyName&amp;quot;: &amp;quot;AmazonS3FullAccess&amp;quot;,
            &amp;quot;PolicyArn&amp;quot;: &amp;quot;arn:aws:iam::aws:policy/AmazonS3FullAccess&amp;quot;
        },
        {
            &amp;quot;PolicyName&amp;quot;: &amp;quot;AWSLambda_FullAccess&amp;quot;,
            &amp;quot;PolicyArn&amp;quot;: &amp;quot;arn:aws:iam::aws:policy/AWSLambda_FullAccess&amp;quot;
        },
        {
            &amp;quot;PolicyName&amp;quot;: &amp;quot;AmazonEventBridgeReadOnlyAccess&amp;quot;,
            &amp;quot;PolicyArn&amp;quot;: &amp;quot;arn:aws:iam::aws:policy/AmazonEventBridgeReadOnlyAccess&amp;quot;
        }
    ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Attached user policies are a way to assign standalone permission sets by reference to one or more users, whereas inline policies are policies that are defined in the identity itself.&lt;/p&gt;
&lt;h2&gt;A discovery process&lt;/h2&gt;
&lt;p&gt;I eventually figured out that the AWS CLI is self documenting. So if I&#39;m curious about IAM, for example, I could just run &lt;code&gt;aws iam help&lt;/code&gt; and read the list of commands and then &lt;code&gt;aws iam list-attached-user-policies help&lt;/code&gt; and so on.&lt;/p&gt;
&lt;p&gt;I ran into an issue where my auth had expired and needed a way to have two auth &amp;quot;contexts&amp;quot; which is detailed in &lt;a href=&quot;https://corvusconsulting.net/posts/iam-part2/&quot;&gt;Part 2&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Separate AWS credentials per project on Windows (cmder + Git Bash)</title>
    <link href="https://corvusconsulting.net/posts/iam-part2/" />
    <updated>2026-10-18T00:00:00Z</updated>
    <id>https://corvusconsulting.net/posts/iam-part2/</id>
    <content type="html">&lt;p&gt;I do consulting work for a client and I&#39;m also exploring AWS on my own, and the two need different credentials. &lt;a href=&quot;https://corvusconsulting.net/posts/iam-part1/&quot;&gt;Part 1&lt;/a&gt; used &lt;code&gt;--profile pokesandbox&lt;/code&gt;, but I wanted something where I couldn&#39;t accidentally run a command against the wrong account. My solution is one config file per project, selected by the terminal tab I open.&lt;/p&gt;
&lt;h2&gt;One config file per project&lt;/h2&gt;
&lt;p&gt;Each project directory gets its own &lt;code&gt;.aws/config&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[default]
region = us-east-1
output = json
aws_access_key_id = AKIXXXXXXXXXXXXXXXXX
aws_secret_access_key = &amp;lt;secret_key&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The CLI also accepts keys in the config file, so I use a single file per project instead of splitting config and credentials. Add &lt;code&gt;.aws/&lt;/code&gt; to &lt;code&gt;.gitignore&lt;/code&gt;, because this file now contains secrets.&lt;/p&gt;
&lt;h2&gt;Pointing the CLI at it&lt;/h2&gt;
&lt;p&gt;The CLI reads &lt;code&gt;AWS_CONFIG_FILE&lt;/code&gt; before falling back to &lt;code&gt;~/.aws/config&lt;/code&gt;. I set it in a cmder task (Settings &amp;gt; Startup &amp;gt; Tasks):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;-new_console:t:&amp;quot;PokeData&amp;quot;:d:&amp;quot;C:&#92;PokeData&#92;&amp;quot; set &amp;quot;PATH=%ConEmuDir%&#92;..&#92;git-for-windows&#92;usr&#92;bin;%PATH%&amp;quot; &amp;amp; %ConEmuDir%&#92;..&#92;git-for-windows&#92;git-cmd.exe --no-cd --command=%ConEmuBaseDirShort%&#92;conemu-msys2-64.exe &amp;quot;%ConEmuDir%&#92;..&#92;git-for-windows&#92;usr&#92;bin&#92;bash.exe&amp;quot; --login -i -c &#39;export AWS_CONFIG_FILE=&amp;quot;/c/PokeData/.aws/config&amp;quot;; exec bash --login -i&#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The part that matters is &lt;code&gt;export AWS_CONFIG_FILE=&amp;quot;/c/PokeData/.aws/config&amp;quot;&lt;/code&gt;. The rest opens a Git Bash tab, titled &amp;quot;PokeData&amp;quot;, starting in &lt;code&gt;C:&#92;PokeData&#92;&lt;/code&gt;. The context comes from the tab, not the directory: the CLI doesn&#39;t detect where you are, the tab just has the variable set.&lt;/p&gt;
&lt;h2&gt;Checking which identity you&#39;re using&lt;/h2&gt;
&lt;p&gt;In the PokeData tab:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;λ aws sts get-caller-identity
{
    &amp;quot;UserId&amp;quot;: &amp;quot;AIDAXXXXXXXXXXXXXXXXX&amp;quot;,
    &amp;quot;Account&amp;quot;: &amp;quot;111111111111&amp;quot;,
    &amp;quot;Arn&amp;quot;: &amp;quot;arn:aws:iam::111111111111:user/PokeSandbox-Dev&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In a client tab:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;λ aws sts get-caller-identity
{
    &amp;quot;UserId&amp;quot;: &amp;quot;AIDAXXXXXXXXXXXXXXXXX&amp;quot;,
    &amp;quot;Account&amp;quot;: &amp;quot;222222222222&amp;quot;,
    &amp;quot;Arn&amp;quot;: &amp;quot;arn:aws:iam::222222222222:user/client-user&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now I can operate in two different contexts without having to think about it.&lt;/p&gt;
</content>
  </entry>
</feed>