Home > database >  Write multi-line secret to windows runner in GitHub workflow
Write multi-line secret to windows runner in GitHub workflow

Time:09-10

What specific syntax must be changed in the code below in order for the multi-line contents of the $MY_SECRETS environment variable to be successfully written into the C:\\Users\\runneradmin\\somedir\\mykeys.yaml file on a Windows runner in the GitHub workflow whose code is given below?

PROBLEM DEFINITION:

The echo "$MY_SECRETS" > C:\\Users\\runneradmin\\somedir\\mykeys.yaml command is only printing the string literal MY_SECRETS into the C:\\Users\\runneradmin\\somedir\\mykeys.yaml file instead of printing the multi-line contents of the MY_SECRETS variable.

We confirmed that this same echo command does successfully print the same multi-line secret in an ubuntu-latest runner, and we manually validated the correct contents of the secrets.LIST_OF_SECRETS environment variable. ... This problem seems entirely isolated to either the windows command syntax, or perhaps to the windows configuration of the GitHub windows-latest runner, either of which should be fixable by changing the workflow code below.

EXPECTED RESULT:

The multi-line secret should be printed into the C:\\Users\\runneradmin\\somedir\\mykeys.yaml file.

The resulting printout of the contents of the C:\\Users\\runneradmin\\somedir\\mykeys.yaml file should look like:

***  
***  
***  
***  

LOGS THAT DEMONSTRATE THE FAILURE:

The result of running myapp.py in the GitHub Actions log is:

ccc item is:  $MY_SECRETS

As you can see, the string literal $MY_SECRETS is being wrongly printed out instead of the 4 *** secret lines.

WORKFLOW CODE:

The minimal code for the workflow to reproduce this problem is as follows:

name: write-secrets-to-file
on:
  push:
    branches:
      - dev
jobs:
  write-the-secrets-windows:
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v3
      - shell: python
        name: Configure agent
        env:
          MY_SECRETS: ${{ secrets.LIST_OF_SECRETS }}
        run: |
          import subprocess
          import pathlib
          pathlib.Path("C:\\Users\\runneradmin\\somedir\\").mkdir(parents=True, exist_ok=True)
          print('About to: echo "$MY_SECRETS" > C:\\Users\\runneradmin\\somedir\\mykeys.yaml')
          output = subprocess.getoutput('echo "$MY_SECRETS" > C:\\Users\\runneradmin\\somedir\\mykeys.yaml')
          print(output)
          os.chdir('D:\\a\\myRepoName\\')
          mycmd = "python myRepoName\\myapp.py"
          p = subprocess.Popen(mycmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
          while(True):
            # returns None while subprocess is running
            retcode = p.poll() 
            line = p.stdout.readline()
            print(line)
            if retcode is not None:
              break 

MINIMAL APP CODE:

Then the minimal myapp.py program that demonstrates what was actually written into the C:\\Users\\runneradmin\\somedir\\mykeys.yaml file is:

with open('C:\\Users\\runneradmin\\somedir\\mykeys.yaml') as file:
  for item in file:
    print('ccc item is: ', str(item))

STRUCTURE OF MULTI-LINE SECRET:

The structure of the multi-line secret contained in the secrets.LIST_OF_SECRETS environment variable is:

var1:value1
var2:value2
var3:value3
var4:value4

These 4 lines should be what gets printed out when myapp.py is run by the workflow, though the print for each line should look like *** because each line is a secret.

@JAAAY'S SUGGESTION:

Here is how we have applied @JAAAY's suggestion to the requirements of this OP:

Repo file structure contains only 2 files as shown:

.github/
    workflows/
        test.yml
main.py  

test.yml contains:

name: write-secrets-to-file
on:
  push:
    branches:
      - main
jobs:
  write-the-secrets-windows:
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v3
      - shell: python
        name: Configure agent
        env:
          MY_SECRETS: ${{ secrets.LIST_OF_SECRETS }}
        run: |
          import base64, subprocess, sys, os, pathlib
          pathlib.Path("C:\\Users\\runneradmin\\somedir\\").mkdir(parents=True, exist_ok=True)
      
          def powershell(cmd, input=None):
            cmd64 = base64.encodebytes(cmd.encode('utf-16-le')).decode('ascii').strip()
            stdin = None if input is None else subprocess.PIPE
            process = subprocess.Popen(["powershell.exe", "-NonInteractive", "-EncodedCommand", cmd64], stdin=stdin, stdout=subprocess.PIPE)
            if input is not None:
                input = input.encode(sys.stdout.encoding)
            output, stderr = process.communicate(input)
            output = output.decode(sys.stdout.encoding).replace('\r\n', '\n')
            return output

          secrets = os.environ["MY_SECRETS"]
      
          command = r"""$secrets = @'
          {}
          '@
          $secrets | Out-File -FilePath C:\\Users\\runneradmin\\somedir\\mykeys.yaml""".format(secrets)
    
          print('About to echo secrets to file.')
      
          print(powershell(command))
          output = subprocess.run(["powershell.exe", "-Command", ], capture_output=True, shell=True)
          print(output)
          output = subprocess.getoutput(["powershell.exe", "-Command", "Get-Content -Path C:\\Users\\runneradmin\\somedir\\mykeys.yaml"])
          print(output)
      
          mycmd = "python main.py"
          p = subprocess.Popen(mycmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
          while(True):
            # returns None while subprocess is running
            retcode = p.poll() 
            line = p.stdout.readline()
            print(line)
            if retcode is not None:
              break 

main.py contents are:

with open('C:\\Users\\runneradmin\\somedir\\mykeys.yaml') as file:
  for item in file:
    print('ccc item is: ', str(item))
    if "key1" in item:
      print("Found key1")

Repository-level multi-line secret named LIST_OF_SECRETS contains:

key1:val1
key2:val2

And the seemingly relevant portion of the logs look like:

b'ccc item is:  \xff\xfek\x00e\x00y\x001\x00:\x00v\x00a\x00l\x001\x00\r\n'
b'\r\n'
b'ccc item is:  \x00k\x00e\x00y\x002\x00:\x00v\x00a\x00l\x002\x00\r\n'
b'\r\n'
b'ccc item is:  \x00\r\n'
b'\r\n'
b'ccc item is:  \x00\r\n'
b''
b''
b''
...many more lines

Do you see that it failed to find key1 and also that the secret lines are encoded in addition to being wrapped in a byte array?

These results are very different behavior than we are getting in other environments, including a Windows 10 laptop on prem, RHEL8 agents in a cloud provider using a different pipeline tool, and GitHub ubuntu-latest runners, all of which work perfectly without these problems.

CodePudding user response:

You need to use yaml library:

import yaml

data = {'MY_SECRETS':'''
var1:value1
var2:value2
var3:value3
var4:value4
'''}#add your secret 

with open('file.yaml', 'w') as outfile: # Your file
    yaml.dump(data, outfile, default_flow_style=False)

I used this.

CodePudding user response:

I tried the following code and it worked fine :

LIST_OF_SECRETS

key1:val1
key2:val2

Github action (test.yml)

name: write-secrets-to-file
on:
  push:
    branches:
      - main
jobs:
  write-the-secrets-windows:
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v3
      - shell: python
        name: Configure agent
        env:
          MY_SECRETS: ${{ secrets.LIST_OF_SECRETS }}
        run: |
          import base64, subprocess, sys, os
          
          def powershell(cmd, input=None):
            cmd64 = base64.encodebytes(cmd.encode('utf-16-le')).decode('ascii').strip()
            stdin = None if input is None else subprocess.PIPE
            process = subprocess.Popen(["powershell.exe", "-NonInteractive", "-EncodedCommand", cmd64], stdin=stdin, stdout=subprocess.PIPE)
            if input is not None:
                input = input.encode(sys.stdout.encoding)
            output, stderr = process.communicate(input)
            output = output.decode(sys.stdout.encoding).replace('\r\n', '\n')
            return output

          secrets = os.environ["MY_SECRETS"]
          
          command = r"""$secrets = @'
          {}
          '@
          $secrets | Out-File -FilePath .\mykeys.yaml""".format(secrets)
        
          print('About to echo secrets to file.')
          
          print(powershell(command))
          output = subprocess.run(["powershell.exe", "-Command", ], capture_output=True, shell=True)
          print(output)
          output = subprocess.getoutput(["powershell.exe", "-Command", "Get-Content -Path .\mykeys.yaml"])
          print(output)

Output

***
***

As you also mention in the question, Github will obfuscate any printed value containing the secrets with ***

EDIT : Updated the code to work with multiple line secrets. This answer was highly influenced by this one

  • Related