Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Audit workflows codecov #398

Closed
3 tasks
minrk opened this issue Apr 15, 2021 · 30 comments
Closed
3 tasks

Audit workflows codecov #398

minrk opened this issue Apr 15, 2021 · 30 comments

Comments

@minrk
Copy link
Member

minrk commented Apr 15, 2021

codecov revealed a security issue where their bash uploader was compromised.

Anyone using the bash uploader and validating the checksum wasn't affected, but the official codecov/codecv-action didn't (and still doesn't). We don't use the bash uploader directly, but this search suggests we used the codecov action in the following repos:

GitHub Actions docs state that every action has access to every bit of information available to the workflow, meaning we should assume that every bit of information available anywhere in a workflow with the codecov-action has been compromised.

So we should audit the affected workflows of each of these repos and either:

  1. verify that the workflow never had access to useful credentials, or
  2. rotate any possibly compromised secrets, or
  3. rotate all secrets in the repos, regardless, rather than trusting our own analysis of the situation. Rotating all credentials may well be less work than figuring this out with confidence.

Having multiple workflows seems to help here: As I understand it, secrets are not available unless they are explicitly added to the workflow somewhere (they are technically accessible to all actions if they are used anywhere in the workflow). I think we mostly don't use any credentials in our test workflows. And this reveals that we should keep it that way!

It also suggests to me that we should avoid using third party actions in our release workflows or any workflows with any secrets anywhere, unless they do a lot, and be pretty careful to pin actions in release workflows with at least full tags, and possibly shas.

@minrk
Copy link
Member Author

minrk commented Apr 15, 2021

From gitter discussion, it's possible that we don't really need to worry if secrets. doesn't occur in any of the above workflows, and it doesn't. So maybe just the auditing of our release workflows to reduce the likelihood of a similar event catching us out in the future.

@minrk
Copy link
Member Author

minrk commented Apr 15, 2021

I wrote a quick script to collect all the actions and secrets in any workflow in any repo in an org on github to get an overview of what third party actions might have access to if they are compromised:

results for jupyterhub
jupyterhub/jupyterhub/.github/workflows/release.yml
  possible secrets:
    L65:           TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
    L95:           username: ${{ secrets.DOCKERHUB_USERNAME }}
    L96:           password: ${{ secrets.DOCKERHUB_TOKEN }}
    L109:           githubToken: ${{ secrets.GITHUB_TOKEN }}
    L128:           githubToken: ${{ secrets.GITHUB_TOKEN }}
    L148:           githubToken: ${{ secrets.GITHUB_TOKEN }}
  actions:
    actions/checkout@v2
    actions/setup-node@v1
    actions/setup-python@v2
    actions/upload-artifact@v2
    docker/build-push-action@v2
    docker/login-action@v1
    docker/setup-buildx-action@v1
    docker/setup-qemu-action@v1
    jupyterhub/action-major-minor-tag-calculator@main
jupyterhub/configurable-http-proxy/.github/workflows/publish.yml
  possible secrets:
    L24:           NODE_AUTH_TOKEN: ${{ secrets.npm_token }}
    L47:           username: ${{ secrets.DOCKERHUB_USERNAME }}
    L48:           password: ${{ secrets.DOCKERHUB_TOKEN }}
    L62:           githubToken: ${{ secrets.GITHUB_TOKEN }}
  actions:
    actions/checkout@v2
    actions/setup-node@v1
    docker/build-push-action@v2
    docker/login-action@v1
    docker/setup-buildx-action@v1
    docker/setup-qemu-action@v1
    jupyterhub/action-major-minor-tag-calculator@main
jupyterhub/oauthenticator/.github/workflows/publish.yml
  possible secrets:
    L35:           password: ${{ secrets.pypi_password }}
  actions:
    actions/checkout@v2
    actions/setup-python@v2
    pypa/gh-action-pypi-publish@v1.4.1
jupyterhub/dockerspawner/.github/workflows/publish.yml
  possible secrets:
    L35:           password: ${{ secrets.pypi_password }}
  actions:
    actions/checkout@v2
    actions/setup-python@v2
    pypa/gh-action-pypi-publish@v1.4.1
jupyterhub/batchspawner/.github/workflows/python-publish.yml
  possible secrets:
    L28:         TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
  actions:
    actions/checkout@v2
    actions/setup-python@v2
jupyterhub/kubespawner/.github/workflows/publish.yaml
  possible secrets:
    L57:           password: ${{ secrets.pypi_password }}
  actions:
    actions/checkout@v2
    actions/setup-python@v2
    pypa/gh-action-pypi-publish@v1.4.1
jupyterhub/ldapauthenticator/.github/workflows/publish.yml
  possible secrets:
    L35:           password: ${{ secrets.pypi_password }}
  actions:
    actions/checkout@v2
    actions/setup-python@v2
    pypa/gh-action-pypi-publish@v1.4.1
jupyterhub/wrapspawner/.github/workflows/python-publish.yml
  possible secrets:
    L28:         TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
  actions:
    actions/checkout@v2
    actions/setup-python@v2
jupyterhub/jupyter-server-proxy/.github/workflows/publish.yaml
  possible secrets:
    L60:           password: ${{ secrets.PYPI_PASSWORD }}
    L82:           NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
  actions:
    actions/checkout@v2
    actions/download-artifact@v2
    actions/setup-node@v1
    actions/setup-python@v2
    actions/upload-artifact@v2
    pypa/gh-action-pypi-publish@v1.3.0
jupyterhub/zero-to-jupyterhub-k8s/.github/workflows/publish.yml
  possible secrets:
    L86:           echo "${{ secrets.JUPYTERHUB_HELM_CHART_DEPLOY_KEY }}" > ~/.ssh/id_ed25519
    L99:           docker login -u "${{ secrets.DOCKER_USERNAME }}" -p "${{ secrets.DOCKER_PASSWORD }}"
  actions:
    actions/checkout@v2
    actions/setup-python@v2
    actions/upload-artifact@v2
    docker/setup-buildx-action@v1
    docker/setup-qemu-action@v1
jupyterhub/zero-to-jupyterhub-k8s/.github/workflows/vuln-scan.yaml
  possible secrets:
    L187:           token: "${{ secrets.GITHUB_TOKEN }}"
  actions:
    actions/checkout@v2
    actions/setup-python@v2
    aquasecurity/trivy-action@master
    jacobtomlinson/gha-find-replace@0.1.2
    peter-evans/create-pull-request@v3
jupyterhub/binderhub/.github/workflows/publish.yml
  possible secrets:
    L49:           echo "${{ secrets.JUPYTERHUB_HELM_CHART_DEPLOY_KEY }}" > ~/.ssh/id_ed25519
    L62:           docker login -u "${{ secrets.DOCKER_USERNAME }}" -p "${{ secrets.DOCKER_PASSWORD }}"
  actions:
    actions/checkout@v2
    actions/setup-python@v2
jupyterhub/repo2docker/.github/workflows/release.yml
  possible secrets:
    L34:           password: ${{ secrets.PYPI_API_TOKEN }}
  actions:
    actions/checkout@v2
    actions/setup-python@v2
    pypa/gh-action-pypi-publish@master
jupyterhub/mybinder.org-deploy/.github/workflows/cd.yml
  possible secrets:
    L118:           GIT_CRYPT_KEY: ${{ secrets.GIT_CRYPT_KEY }}
    L124:           username: ${{ secrets.DOCKER_USERNAME }}
    L125:           password: ${{ secrets.DOCKER_PASSWORD }}
    L249:           GIT_CRYPT_KEY: ${{ secrets.GIT_CRYPT_KEY }}
    L257:           username: ${{ secrets.DOCKER_USERNAME_OVH }}
    L258:           password: ${{ secrets.DOCKER_PASSWORD_OVH }}
  actions:
    actions/cache@v2
    actions/checkout@v2
    actions/setup-python@v1
    azure/docker-login@v1
    azure/setup-kubectl@v1
    google-github-actions/setup-gcloud@master
    nick-invision/retry@39da88d5f7d15a96aed861dbabbe8b7443e3182a
    sliteteam/github-action-git-crypt-unlock@a09ea5079c1b0e1887d4c8d7a4b20f00b5c2d06b
jupyterhub/mybinder.org-deploy/.github/workflows/lint-validate.yml
  possible secrets:
    L36:           GIT_CRYPT_KEY: ${{ secrets.GIT_CRYPT_KEY }}
    L46:             export YAMLLINT_CONFIG=scripts/yamllint-no-secrets.yaml
    L103:           GIT_CRYPT_KEY: ${{ secrets.GIT_CRYPT_KEY }}
    L118:             export SECRET_CONFIG="-f config/test-secrets.yaml"
  actions:
    actions/checkout@v2
    actions/setup-python@v1
    sliteteam/github-action-git-crypt-unlock@a09ea5079c1b0e1887d4c8d7a4b20f00b5c2d06b
jupyterhub/nbgitpuller/.github/workflows/publish.yml
  possible secrets:
    L35:           password: ${{ secrets.pypi_password }}
  actions:
    actions/checkout@v2
    actions/setup-python@v2
    pypa/gh-action-pypi-publish@v1.4.1
jupyterhub/ltiauthenticator/.github/workflows/publish.yaml
  possible secrets:
    L55:           password: ${{ secrets.pypi_password }}
  actions:
    actions/checkout@v2
    actions/setup-python@v2
    pypa/gh-action-pypi-publish@v1.4.1
jupyterhub/chartpress/.github/workflows/publish.yaml
  possible secrets:
    L37:           password: ${{ secrets.pypi_password }}
  actions:
    actions/checkout@v2
    actions/setup-python@v2
    pypa/gh-action-pypi-publish@v1.4.1
jupyterhub/traefik-proxy/.github/workflows/release.yml
  possible secrets:
    L42:         password: ${{ secrets.pypi_password }}
  actions:
    actions/checkout@v2
    actions/setup-python@v2
    pypa/gh-action-pypi-publish@v1.4.1
jupyterhub/nativeauthenticator/.github/workflows/publish.yaml
  possible secrets:
    L37:           password: ${{ secrets.pypi_password }}
  actions:
    actions/checkout@v2
    actions/setup-python@v2
    pypa/gh-action-pypi-publish@v1.4.1
jupyterhub/jupyter-remote-desktop-proxy/.github/workflows/binder-badge.yaml
  possible secrets:
    L12:         github-token: ${{secrets.GITHUB_TOKEN}}
  actions:
    actions/github-script@v1
jupyterhub/jupyter-remote-desktop-proxy/.github/workflows/publish.yml
  possible secrets:
    L35:           password: ${{ secrets.pypi_password }}
  actions:
    actions/checkout@v2
    actions/setup-python@v2
    pypa/gh-action-pypi-publish@v1.4.1
jupyterhub/repo2docker-action/.github/workflows/binder.yaml
  possible secrets:
    L15:         github-token: ${{secrets.GITHUB_TOKEN}}
  actions:
    actions/github-script@v1
jupyterhub/jupyterhub-idle-culler/.github/workflows/publish.yml
  possible secrets:
    L35:           password: ${{ secrets.pypi_password }}
  actions:
    actions/checkout@v2
    actions/setup-python@v2
    pypa/gh-action-pypi-publish@v1.4.1
jupyterhub/pebble-helm-chart/.github/workflows/publish.yml
  possible secrets:
    L44:           echo "${{ secrets.JUPYTERHUB_HELM_CHART_DEPLOY_KEY }}" > ~/.ssh/id_ed25519
  actions:
    actions/checkout@v2
    actions/setup-python@v2
jupyterhub/action-k8s-await-workloads/.github/workflows/package.yaml
  possible secrets:
    L51:           token: ${{ secrets.GITHUB_TOKEN }}
  actions:
    actions/checkout@v2
    actions/setup-node@v2
    peter-evans/create-pull-request@v3

We can use that to audit whether we should do some sha pinning on some dependencies and/or reimplementing some that don't offer us a whole lot.

@manics
Copy link
Member

manics commented Apr 15, 2021

Does anyone know if all secrets are independent, or if some are shared across multiple repos? If there's a significant number it might be worth switching them to an organisation secret https://docs.github.com/en/actions/reference/encrypted-secrets#creating-encrypted-secrets-for-an-organization

@minrk
Copy link
Member Author

minrk commented Apr 15, 2021

Most secrets (certainly PyPI) are scoped per repo so don't make a lot of sense to share. Should probably be the same with deploy keys for e.g. chart push, but I'm not sure if we do that right now. Are our Docker push secrets single-use tokens with repo scopes? That might make the most sense to share if they are the same

@minrk
Copy link
Member Author

minrk commented Apr 15, 2021

Maybe @hamelsmu can answer this: if no secrets appear in a workflow with a compromised action, is there any way for that action to reach secrets e.g. in other workflows on the same repo? Or is the scope of access for an action strictly what's in the workflow file? That seems to be right.

@hamelsmu
Copy link

It depends who is running the workflow and what the permissions you have on these things are. It's a really confusing subject. But by default, pull requests from forks don't have access to your secrets.

@minrk
Copy link
Member Author

minrk commented Apr 16, 2021

@hamelsmu thanks! This issue is about the codecov vulnerability where an action was compromised, which means it ran from our main branches as well. So if the workflow had secrets in it, they definitely would have been compromised. I think we are comfortable with the fact that PR workflows don't have access to things, but are wondering if actions have access to anything when run from the main branch on the origin repo even if no secrets appear in the workflow. I'm assuming not, but trying to find stronger confirmation than I have been able to so far.

@minrk
Copy link
Member Author

minrk commented Apr 16, 2021

In particular, this text is what's tripping me up:

This means that a compromise of a single action within a workflow can be very significant, as that compromised action would have access to all secrets configured on your repository, and can use the GITHUB_TOKEN to write to the repository.

Which seems to suggest that a compromised action has access to $GITHUB_TOKEN and all secrets, unconditionally.

@consideRatio
Copy link
Member

consideRatio commented Apr 16, 2021

@minrk my best guess interpretation is that there are ways for all steps executing as part of a job, including third party actions, to break free from their isolation (no proper isolation) and potentially acquire the GITHUB_TOKEN or equivalent permissions, so even though its not directly made available, it should be considered directly available from a security standpoint.

@manics
Copy link
Member

manics commented Apr 16, 2021

I think the preceding paragraph has some important context:

The individual jobs in a workflow can interact with (and compromise) other jobs. For example, a job querying the environment variables used by a later job, writing files to a shared directory that a later job processes, or even more directly by interacting with the Docker socket and inspecting other running containers and executing commands in them.

This sounds like a compromised action can access the state created by previous steps, and can manipulate the state of the system causing subsequent actions to behave differently. For example it might modify an output artifact such as a script which is passed to another job, thereby accessing the secrets in that other job. To me it implies the secret still needs to be inserted into the workflow at some point rather than all secrets being accessible, but it would be nice to get a definitive statement from GitHub.

@minrk
Copy link
Member Author

minrk commented Apr 16, 2021

@manics yeah, that's my understanding as well. I just wish they weren't self-contradictory and why I think it's weird to say that it "would have access to all secrets configured on your repository" without qualification, since that doesn't seem to be true.

I've at least confirmed that compromised actions repos have access to $GITHUB_TOKEN via ${{ github.token }} and therefore write access to the repo. They don't need anything passed through the action to get this if @actions/checkout is used (i.e. always), which sets up git with full write access by default (!).

Interestingly, when I tried a proof of concept creating a workflow to exfiltrate secrets with this, it was rejected without workflows permission:

To https://github.com/minrk/test-action
! [remote rejected] HEAD -> dontmindme (refusing to allow a GitHub App to create or update workflow > `.github/workflows/ruhroh.yml` without `workflows` permission)

Frustratingly, there doesn't appear to be a way to prevent $GITHUB_TOKEN from having write access, which makes third-party actions much less attractive across the board. I really wish you had to opt-in to write permissions in workflows, but not even having opt-out seems wild. The only thing I can think of is to run our tests only on PRs and never from our own repos, and only run deployment workflows with extremely limited action dependencies from our own branches.

Also the bar for using any third-party actions should be pretty high, I guess.

@manics
Copy link
Member

manics commented Apr 16, 2021

Alternatively we could fork third party actions from individuals in smaller orgs, do a full code review, and for Javascript actions rebuild dist. It means more work if we want to update the action, but I imagine in most cases we could get away without updating.

@sgibson91
Copy link
Member

sgibson91 commented Apr 16, 2021

Another option could be to revive the idea I had of building a container image that contains all the tools we need to, e.g., deploy mybinder.org and run our deployment in that environment rather than relying on third-party actions to install tools for us (extra benefit is speed since the installation steps are run at docker build not workflow trigger time)

@hamelsmu
Copy link

hamelsmu commented Apr 16, 2021 via email

@manics
Copy link
Member

manics commented Apr 16, 2021

If we're pinning all actions we could consider pinning all software dependencies (including transient dependencies) too.

@hamelsmu
Copy link

hamelsmu commented Apr 16, 2021

I WAS COMPLETELY WRONG.

The problem is much worse than I had anticipated

Re: GITHUB_TOKEN

Feedback from some knowledgeable folks at GitHub

It’s in memory in the runner so it can be extracted via dump - I’ve seen a one line script fu to get it

So um, I don't know what else to say. Will try to find out more information about this.

My new understanding is that compromised third party third party Actions can trivially access your secrets

You might be wondering what is the point of this YAML syntax

env:
   TOKEN: ${{ secrets.GITHUB_TOKEN }}

The answer is the compromised Action can still grab GITHUB_TOKEN even if you don't explicitly pass this line to a step by doing a memory dump, as long as you passed it to an earlier step. So this line in your YAML syntax just functions like an alias, and is not a defense to your secrets. You can grab this as long it is passed to an earlier workflow step in the same job

@chrispat
Copy link

If your workflow does not explicitly reference any of your secrets, then none of them are sent to the runner. The GITHUB_TOKEN can't read secrets from your repo. However, it can do other things like push code as outlined in the documentation.

@chrispat
Copy link

Maybe @hamelsmu can answer this: if no secrets appear in a workflow with a compromised action, is there any way for that action to reach secrets e.g. in other workflows on the same repo? Or is the scope of access for an action strictly what's in the workflow file? That seems to be right.

That is correct. If you do not reference any secrets in the workflow then none will be available.

@hamelsmu
Copy link

Thanks @chrispat so it seems that a downstream compromised workflow step could access a secret if it is passed to an earlier step (because that's what triggers the secret to be sent to the runner)? Or is each step self contained somehow?

@chrispat
Copy link

Steps are not isolated from each other, they all run in the same workspace. A job is the isolation level in Actions.

@hamelsmu
Copy link

@chrispat one more question. Since the secrets are available in memory to downstream steps within a job, what is your recommendation on the scope of what needs to be audited?

For example I can think of the following things -

(1) All third party actions you use (2) Any software dependencies you use in your workflow (3) Any dependencies that Actions itself uses or vendors into the environment.

@minrk
Copy link
Member Author

minrk commented Apr 16, 2021

Thanks @chrispat and @hamelsmu! Good to have confirmation that secrets aren't directly available. We have small workflows with credentials for publishing releases and larger workflows with none, which makes our audit surface easier. That mainly leaves the write access with $GITHUB_TOKEN that's left to deal with.

What is the best way to have a workflow that cannot have any write permissions? For instance, in our affected test workflows, we don't use any secrets, so that's fine. I thought that was enough to be safe, but didn't understand that all actions appear to unconditionally have write access to your repo.

@chrispat
Copy link

We are shipping a feature next week whtat will let you configure the permissions for the GITHUB_TOKEN secret and let you set the default permissions to contents: read for an entire org. Keep an eye on the change log.

@blink1073
Copy link

This changelog? https://github.blog/changelog/

@sgibson91
Copy link
Member

This recent blog has been on my to-read list as well https://github.blog/2021-04-13-implementing-least-privilege-for-secrets-in-github-actions/

@chrispat
Copy link

https://t.co/vcjtAlePw2

@minrk
Copy link
Member Author

minrk commented Apr 21, 2021

Awesome! I took the opportunity to apply the updated restricted default permissions to the jupyterhub org (and ipython, jupyter, jupyter-server). Any actions that need write permissions ought to be opt-in now

@minrk
Copy link
Member Author

minrk commented Apr 21, 2021

I've checked the GitHub Events archive for the affected timeframe, and the repos which used the codecov action never had a push event triggered by GHA.

So I think we can close this issue for now, but it's still probably prudent to do audits (and maybe pin shas) for all of our deploy/publish/release actions

@consideRatio
Copy link
Member

@minrk i opened #404 to followup on regarding audit / sha pinnings for our workflows referencing GitHub secrets

@minrk
Copy link
Member Author

minrk commented Apr 30, 2021

Another follow-up: Codecov updated their report indicating that they are confident the only compromise was exfiltration of env and git remote -v. Since we never pass environment variables to the codecov step, and $GITHUB_TOKEN was only available in the .git configuration (not url), I think we can reasonably conclude that:

  1. the only possible compromise was $GITHUB_TOKEN, technically available on the filesystem due to @actions/checkout. Nothing useful was in the env of the action.
  2. our token was not compromised, since nothing of value was in the env
  3. the compromised code never attempted to push fraudulent commits
  4. even if our tokens were compromised, the exfiltration destination would have only had on the order of seconds to use $GITHUB_TOKEN to push malicious commits
  5. for extra prudence, I queried the GitHub events archive with BigQuery for commit actions across our repos originating from github actions, and found no suspicious commits, only things like the image vulnerability-scan commits on z2jh like this one *

* note: that commit looks like it was made by @consideRatio because there is no record in the commit that it was made by GHA (should be fixed by jupyterhub/zero-to-jupyterhub-k8s#2153). The events archive, however, has an 'actor' field that cannot be spoofed, unlike git committer/author.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants