Coding Stephan

Github Actions: Use secret file

Github Actions are great for automating tests and builds, among other things. If you need a secret (key/token/password), you can add those in the configuration and use them in your workflow.

Sometimes you need a file that is meant to be secret inside your workflow. This post shows you how to securely save this file as a secret and recreate the file during build. We use base64 encoding for a way to convert any file to a string that can be saved in the secrets.

This is all done in powershell core, which is available in all (Windows/Mac/Linux) runners on Github. The code below should work on any platform, but is only tested on a windows-latest runner.

File to base64 string

$file = "secret.txt";
$bytesFromFile = Get-Content $file -Raw -AsByteStream;
$encodedBytes = [System.Convert]::ToBase64String($bytesFromFile);
# Display base 64 string
Write-Output "File: $file converted to base64:";
Write-Output " ";
Write-Output $encodedBytes;
Write-Output " ";
# Compute and show hash of original file
$fileHashInfo = Get-FileHash $file;
Write-Output "Hash: $($fileHashInfo.Hash)";

This will display the Base64 representation of secret.txt in the current directory. Change filename accordingly. It will also show the Sha256 hash of the file. That can be used to verify the correct file is loaded.

The long string should be added to the secrets of the repository (or other level), the next code expects the secret name B64_SECRET1.

Write base64 secret to file.

In your Github Action you can Write the secret stored in B64_SECRET1 to any location, for the sample we will save it to secret-file.txt in the defined temp folder $env:RUNNER_TEMP.

{% raw %}

name: Some Github Action

jobs:
  build:
    name: Build with secret
    runs-on: windows-latest # Code should run on any platform, change accordingly
    steps:
      - name: Create secret-file.txt from B64_SECRET1
        id: secret-file1
        run: |
          $secretFile = Join-Path -Path $env:RUNNER_TEMP -ChildPath "secret-file.txt"; 
          $encodedBytes = [System.Convert]::FromBase64String($env:SECRET_DATA1); 
          Set-Content $secretFile -Value $encodedBytes -AsByteStream;
          $secretFileHash = Get-FileHash $secretFile;
          Write-Output "::set-output name=SECRET_FILE::$secretFile";
          Write-Output "::set-output name=SECRET_FILE_HASH::$($secretFileHash.Hash)";
          Write-Output "Secret file $secretFile has hash $($secretFileHash.Hash)";          
        shell: pwsh
        env:
          SECRET_DATA1: ${{ secrets.B64_SECRET1 }}

{%endraw%}

This step has an id set to secret-file1 and will set two outputs namely SECRET_FILE and SECRET_FILE_HASH. Which can be access in steps after this step. ${{ steps.secret-file1.outputs.SECRET_FILE }}. I recommend to always set this value to an environment variable and using the environment variable in the script. This is really reliable and can be tested on your local machine.

Delete secret file

After each run the runner should be destroyed, so this step is optional. Personally I’m rather save then sorry. So I always add a step to make sure the file is actually removed. This uses the output set by the first step.

{% raw %}

      - name: Delete secret file
        run: |
          Remove-Item -Path $env:SECRET_FILE;          
        shell: pwsh
        if: always()
        env:
          SECRET_FILE: ${{ steps.secret-file1.outputs.SECRET_FILE }}

{% endraw %}

The output SECRET_FILE from the step with id secret-file1 is set as an environment variable SECRET_FILE which is then used in the Remove-Item command. It is also configured to run always, that means even if some step is unsuccessful the secret file will still be removed.

Validate hash

You can also add this part to the script that creates the secret file, if you want it to stop the run if the hash doesn’t match.

$expectedHash = "E2F9.....B6D667247"; # Set to output when generating the Base64 string (and hash)
if ($secretFileHash.Hash -ne $expectedHash) { Write-Output "::error file=$($secretFile)::Hash doesn't match"; Write-Output "Hash doesn't match"; exit 10; }

Conditionally run step

Instead of exiting with an error, like with the code above, you can also conditionally run some step based on the fact if the hash has a specific value.

Just add the correct if condition if: ${{ steps.secret-file1.outputs.SECRET_FILE_HASH == 'E2F9.....B6D667247' }} to always run if the hash matches. Or if: ${{ success() and steps.secret-file1.outputs.SECRET_FILE_HASH == 'E2F9.....B6D667247' }} to only run if the hash matches and all previous steps are successful.