OIDC Authentication for Kubernetes using PowerShell

An integral part of running a Kubernetes cluster in production is security, including how human users authenticate with the API server (the public interface of the cluster). A properly configured cluster should allow only those with proper credentials access. And those with access should only have the ability to interact with the objects necessary to complete their jobs. This is the principle of least privilege. I may go into RBAC in Kubernetes later, if I feel like I have anything interesting to say on the topic. For now, let's see how authentication can work with OIDC.

The Kubernetes documentation has a very good article on Authenticating which I suggest you read for more in-depth knowledge on this topic. I am focusing on the OpenID Connect Tokens section. I am going to assume some basic understanding of OAuth2 and OIDC flows. If you are not familiar, here is a good illustrated guide I saw recently come up on Hacker News.

Configuring the Kubernetes API server for OIDC

The first step in this process is configuring the API server for OpenID Connect. There are several arguments that need to be passed into the server on startup to get this all wired in. You can see the full list here, but I am going to mention one:

—oidc-username-claim

This argument defaults to the sub value, which may work for you. I changed this to the email field for it to be a little more user friendly when assigning UserRoleBindings later on (it makes it clear who is gaining access to what, especially if you are using infrastructure as code practices).

Once you have configured the client id, URL, and CA for your OIDC provider (idp), you can get a token and attempt to connect.

Getting the token from your identity provider

To retrieve the token from your identity provider, you will need a few things:

Let's do this with some PowerShell

$creds = Get-Credential -Message "Provide your $identityServerUrl credentials."

This will generate a prompt for the user to enter in their username and password, and stores the password in a System.Security.SecureString object. This ensures that the data is removed from memory properly when no longer needed (of course a clever user who has access to the system would probably be able to get the data if they really wanted, but it indicates that this is sensitive information).

Now let's create the body for the request

$body = @{
  "scope" = "kubernetes email_only offline_access";
  "grant_type" = "password";
  "client_id" = "$clientId";
  "client_secret" = $secret;
  "username" = $creds.UserName;
  "password" = $($creds.GetNetworkCredential().Password);
}

You will have to fill in the details with your own idp. You can also use different scopes if you wish. The kubernetes scope was one I added to handle specific information for Kubernetes based authentication.

$identityServerTokenEndpoint = "$identityServerUrl/identity/connect/token"
$tokenString = Invoke-RestMethod -Uri $identityServerTokenEndpoint -Method Post -Body $body

Now we can call the token endpoint to get our token from the identity provider. The response should have in it an access token and a refresh token (it may have an id_token instead, depending on identity provider). We can use the access token as the identity token in our Kubernetes configuration file. We can set our .kube config using kubectl, and then use the new context to authenticate with our cluster:

$accessToken = $tokenString.access_token
$refreshToken = $tokenString.refresh_token

$clusterName = "kubernetes-idp"
$contextName = "$clusterName"

& kubectl config set-cluster $clusterName --server=$k8sServerUrl --certificate-authority=$k8sCaCrt

& kubectl config set-context $contextName --cluster=$clusterName --user="$($creds.UserName)"

& kubectl config set-credentials "$($creds.UserName)" --auth-provider=oidc --auth-provider-arg=idp-issuer-url="$identityServerUrl/identity" --auth-provider-arg=client-id=$clientId --auth-provider-arg=client-secret=$secret --auth-provider-arg=refresh-token=$refreshToken --auth-provider-arg=idp-certificate-authority=$identityCaCrt --auth-provider-arg=id-token=$accessToken

& kubectl config use-context $contextName

Where $identityCaCrt is the identity provider's CA for its public domain certificate. The other variables should be self-explanatory. Just fill in the details of your cluster.

The token returned by the identity provider is a JWT. One downside with JWTs is that there is no mechanism to invalidate them directly (because the client controls the certificate, it is an asynchronous process by design). We can alleviate concerns by setting a short TTL on these tokens, but this leads to another issue: it can be annoying to keep getting these tokens. This is what the refresh token is for.

Setting the refresh token should help with generating valid credentials while this token is being used. However, if you go an extended period without using your token, it will be invalid the next time you try to use it. One way to help with this issue is a scheduled task or cronjob to continually generate these credentials and update your Kubernetes config file.

Refresh the token

To refresh the token, we make a refresh token request to the identity provider, using the refresh token we got initially.

$body = @{
  "grant_type" = "refresh_token";
  "client_id" = "$clientId";
  "client_secret" = $secret;
  "refresh_token" = $refreshToken;
}

$identityServerTokenEndpoint = "$identityServerUrl/identity/connect/token"
$tokenString = Invoke-RestMethod -Uri $identityServerTokenEndpoint -Method Post -Body $body

As an example to register a scheduled task on Windows that runs every 50 minutes (assuming the TTL on the token is 60 minutes):

$trigger = New-JobTrigger -Once -At ((Get-Date).AddMinutes(50)) -RepetitionInterval (New-TimeSpan -Minute 50) -RepeatIndefinitely
Register-ScheduledJob -Name "K8s-OIDC-Refresh" -Trigger $trigger -ScriptBlock {
    param(
        [string]$clientId, 
        [string]$secret,
        [string]$refreshToken,
        [string]$identityServerUrl,
        [string]$identityCaCrt,
        [pscredential]$creds
    )

    $body = @{
        "grant_type" = "refresh_token";
        "client_id" = "$clientId";
        "client_secret" = $secret;
        "refresh_token" = $refreshToken;
    }

    Write-Host "Refreshing token from $identityServerUrl..."
    $identityServerTokenEndpoint = "$identityServerUrl/identity/connect/token"
    try {
        $tokenString = Invoke-RestMethod -Uri $identityServerTokenEndpoint -Method Post -Body $body
    } catch {
        Write-Host -ForegroundColor Red "Error getting tokens"
        Write-Host -ForegroundColor Red "$($PSItem.Exception)"
        exit 1
    }

    $accessToken = $tokenString.access_token
    $refreshToken = $tokenString.refresh_token
    try {
        & kubectl config set-credentials "$($creds.UserName)" --auth-provider=oidc --auth-provider-arg=idp-issuer-url="$identityServerUrl/identity" --auth-provider-arg=client-id=$clientId --auth-provider-arg=client-secret=$secret --auth-provider-arg=refresh-token=$refreshToken --auth-provider-arg=idp-certificate-authority=$identityCaCrt --auth-provider-arg=id-token=$accessToken
    } catch {
        Write-Host -ForegroundColor Red "Error writing config"
        Write-Host -ForegroundColor Red "$($PSItem.Exception)"
        exit 1
    }

    Write-Host "Update job to include new parameters"
    $oidcJob = Get-ScheduledJob | Where-Object {$_.Name -like "K8s-OIDC-Refresh"}
    Set-ScheduledJob -InputObject $oidcJob -ArgumentList (
        $clientId,
        $secret,
        $refreshToken,
        $identityServerUrl,
        $identityCaCrt,
        $creds
    )
} -ArgumentList (
    $clientId,
    $secret,
    $refreshToken,
    $identityServerUrl,
    $identityCaCrt,
    $creds
)

Now, as long as the computer is on, the scheduled task should keep your Kubernetes configuration file up to date with a fresh token (assuming your user continues to have access). There are obvious downsides with this approach, such as over the weekend if your computer is off, you will need to generate a new token first thing Monday morning. But that is a minor annoyance for good authentication and security.