Set up Google Cloud Workload Identity Federation for GitHub Actions

I have been using Service Account Key JSON to authenticate GitHub Actions with Google Cloud. This is basically a password stored in GitHub secrets that is then used by GitHub Actions to authenticate. I just saw that the google-github-actions/auth action now supports and recommends the use of Workload Identity Federation. This post is a documentation of the steps I took to enable this.

The doc suggests the Direct setup, where you’d grant permissions to the Workload Identity Pool to access different Google Cloud services. Since I have been using the Service Account, I set up Workload Identity Federation through a Service Account instead. I performed these steps in the Google Cloud console, instead of using the gcloud CLI as in the doc.

  1. Create a Workload Identity Pool
    • From the UI, the “Name” field is actually the display-name attribute of the CLI. The ID is automatically derived, so I edited it to use the github value instead.
  2. Create a Workload Identity Provider
    • Select OpenID Connect (OIDC)
    • Same deal as above re: display name and ID
    • Issuer URL is https://token.actions.githubusercontent.com
    • I selected “Default Audience”
    • Attribute mapping – manually set them
      • google.subjectassertion.sub
      • attribute.actorassertion.actor
      • attribute.repositoryassertion.repository
      • attribute.repository_ownerassertion.repository_owner
    • Attribute conditions – set to my GitHub personal user
      • assertion.repository_owner == 'tnguyen14'
  3. Allow authentications from the Pool to the Service Account
    • This step is where I deviated slightly from the doc. I selected the option “Grant access using service account impersonation” from the UI, and select the Service Account.
    • For the “Select principals (identities that can access the service account)” field, I selected repository_owner instead of repository, as suggested by the doc. My intention is to use this Provider for all repos in my personal account, so I hope this is the right way to do it. The Actions workflow succeeded with this setting, FWIW.
  4. Retrieve the Provider name
    • This info is not available from the Console UI for it, so I had to activate Cloud Shell to retrieve it from the CLI
  5. Changes to workflow config file
    • In the job, update permissions – this step is important. Without it, I got the error Error: google-github-actions/auth failed with: gitHub Actions did not inject $ACTIONS_ID_TOKEN_REQUEST_TOKEN or $ACTIONS_ID_TOKEN_REQUEST_URL into this job. This most likely means the GitHub Actions workflow permissions are incorrect, or this job is being run from a fork.
      permissions:
        contents: 'read'
        id-token: 'write'
    • Replace the auth step’s credentials_json with workload_identity_provider and service_account
      - id: auth
        uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: projects/xxx/locations/global/workloadIdentityPools/github/providers/github-tnguyen14
          service_account: [email protected]

A simple bug when using Redux Toolkit that took me a while to figure out

I have been using React and redux for a few years, and got quite comfortable with building moderately complex component logic with them. I recently migrated an app over to using Redux Toolkit, motivated by wanting to stay up to date with the latest recommendation, as well as reducing boilerplate, which can be cumbersome with redux action creators and reducers.

When using Redux Toolkit, often times you’d need to declare extraReducers to act upon actions defined in a different slice. That would look like so

const asyncAction = createAsyncThunk(
  'async/action',
  async (foo) => {
    await doAsync();
    return 42;
  }
)

const form = createSlice({
  name: 'form'
  initialState,
  reducers: {
    ...
  },
  extraReducers: (builder) => {
    builder.addCase(actionOne, (state, action) => {
    })
    .addMatcher((action) => [
      actionTwo.type,
      actionThree.type,
      asyncAction.fulfilled.type
    ].includes(action.type), (state) => {})
  }
});

You would use addMatcher to match on multiple action types. This does become a bit verbose, so redux-toolkit does provide a helper function called isAnyOf to make this a bit easier

builder.addMatcher(isAnyOf(
  actionTwo,
  actionThree,
  asyncAction.fulfilled
), (state) => {});

This is pretty straightforward. However, in the process of doing the refactoring to use the helper, I made a mistake of forgetting to remove the .type attribute on the action

builder.addMatcher(isAnyOf(
  actionTwo.type,
  actionThree.type,
  asyncAction.fulfilled
), (state) => {});

This might have been simple bug to fix had I known where to look. However, it manifested in a pretty strange behavior, where the matcher would match even when a different action is fired. This caused a pretty confusing behavior in my case, due to an inadvertent state logic:

const initialState = {
  loading: false,
};
const form = createSlice({
  name: 'form',
  initialState,
  reducers: {
    submit: (state) => {
      state.loading = true;
    }
  },
  extraReducers: (builder) => {
    builder.addMatcher(
      isAnyOf(
        submitFailure.type,
        validationFailure.type,
        refresh.fulfilled
    ), (state) => {
      state.loading = false;
    });
  };
});

So I was in a situation where, upon calling the submit action, I can see that the action was fired, but the loading state did not become true. From just reading the code, it would never occur to me that the matcher in the extraReducers was invoked. This was especially tricky to realize, because the actual code contains many more lines of complex logic and a high number of reducers to parse through. It is still unclear to me why the isAnyOf matcher would behave that way. I suspect it has to do with the fact that the helper can accommodate many different types of arguments. I wonder if TypeScript check would have caught this issue here.

TIL – git aliases are case-insensitive

Today I learned that git aliases are case-insensitive – this behavior is documented in http://git-scm.com/docs/git-config#_configuration_file.

The configuration variables are used by both the Git plumbing and the porcelains. The variables are divided into sections, wherein the fully qualified variable name of the variable itself is the last dot-separated segment and the section name is everything before the last dot. The variable names are case-insensitive, allow only alphanumeric characters and -, and must start with an alphabetic character.

For quite a long time, I noticed a strange behavior in my local git environment (which uses a dotfile config https://github.com/tnguyen14/dotfiles/blob/master/home/.gitconfig). When a branch has not been merged, git would usually shows me a warning like this:

:; git branch -d my-feature-branch
error: The branch 'my-feature-branch' is not fully merged.
If you are sure you want to delete it, run 'git branch -D my-feature-branch'.

I find this behavior helpful, cautioning me against accidentally deleting branches and losing work. Because I do branch deletion often, I add then following config to ~/.gitconfig:

[alias]
  bd = branch -d

However, I started noticing that when I do git bd my-branch, it would just delete the branch regardless of whether I have the branch changes merged or not. This went on for a while, and eventually it bothered me enough to look into while. I finally figured it out today. This is happening because I have another alias in gitconfig:

[alias]
  bd = branch -d
  ...
  bD = branch -D

I suppose that when I was setting up aliases, I was trying to be clever and added a convenient alias for when I do want to force-delete the branch. As aliases are case-sensitive, the latter one (branch -D) wins and is always used whenever I type git bd.

Enable SSH into Windows Server with cloud-init

When I had to bring up a Windows Server on AWS using the default Windows Server AMI recently, I needed a way to automatically configure the machine so it can be SSH-ed into without manual configuration. This was needed in order to run ansible on the machine after it was brought up by Terraform.

This could be done with cloud-init and a PowerShell script. The script would install OpenSSH server and configure the permissions for the authorized_keys file to allow incoming SSH session for the Administrator user.

<powershell>
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
Start-Service sshd
Set-Service -Name sshd -StartupType 'Automatic'

$AuthorizedKeyFile = 'C:\ProgramData\ssh\administrators_authorized_keys'
New-Item $AuthorizedKeyFile
Set-Content $AuthorizedKeyFile '${authorized_keys}'

# Reset authorized_keys file ACL to enable SSH
# By default, it inherits parent folder permission, which is too permissive

$Acl = Get-Acl -Path $AuthorizedKeyFile

# disable inheritance
$isProtected = $true
$preserveInheritance = $false
$Acl.SetAccessRuleProtection($isProtected, $preserveInheritance)

$Administrators = 'BUILTIN\Administrators'
$System = 'SYSTEM'
$FullControl = 'FullControl'

$AdministratorAccessRule = New-Object Security.AccessControl.FileSystemAccessRule $Administrators, $FullControl, 'Allow'
$SystemAccessRule = New-Object Security.AccessControl.FileSystemAccessRule $System, $FullControl, 'Allow'

$Acl.SetAccessRule($AdministratorAccessRule)
$Acl.SetAccessRule($SystemAccessRule)

Set-Acl -Path $AuthorizedKeyFile -AclObject $Acl
</powershell>

The local machine’s public SSH key is added to authorized_keys file as a template parameter.