forked from telegramdesktop/tproxy-server
-
Notifications
You must be signed in to change notification settings - Fork 0
Add CI: test on every PR, deploy on merge to master #4
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
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| name: deploy | ||
|
|
||
| # Deliberately triggers on `push` to `master` only - never on `pull_request`, | ||
| # even from this repository itself. A push to master can only happen via a | ||
| # merge, which the branch protection on master restricts to repository | ||
| # collaborators (currently just the owner). This is what keeps the | ||
| # self-hosted runner below safe to use on a public repository: an outside | ||
| # PR can update this very workflow file in its own branch, but that changed | ||
| # file only ever runs (via pull_request, on a GitHub-hosted runner, see | ||
| # test.yml) against the fork it lives in - it can never cause a `push` event | ||
| # on *this* repository's `master`. See deploy/CI.md for the full writeup. | ||
| on: | ||
| push: | ||
| branches: [master] | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| concurrency: | ||
| group: deploy-production | ||
| cancel-in-progress: false | ||
|
|
||
| jobs: | ||
| deploy: | ||
| runs-on: [self-hosted, tproxy-deploy] | ||
| steps: | ||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| - name: deploy | ||
| run: sudo /usr/local/sbin/ci-deploy.sh |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| name: test | ||
|
|
||
| # Runs on GitHub-hosted runners only, for any pull request including ones | ||
| # from forks - this job never touches production and needs no elevated | ||
| # trust. See deploy/CI.md for why the deploy workflow is kept strictly | ||
| # separate from this one. | ||
| on: | ||
| pull_request: | ||
| push: | ||
| branches: [master] | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| concurrency: | ||
| group: test-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| jobs: | ||
| relay: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 | ||
| with: | ||
| go-version: '1.26.5' | ||
| - name: gofmt | ||
| run: | | ||
| unformatted="$(gofmt -l .)" | ||
| if [[ -n "$unformatted" ]]; then | ||
| echo "not gofmt-clean:" | ||
| echo "$unformatted" | ||
| exit 1 | ||
| fi | ||
| - run: go vet ./... | ||
| - run: go test ./... | ||
| - run: go build -trimpath ./... | ||
|
|
||
| keys-panel: | ||
| runs-on: ubuntu-latest | ||
| defaults: | ||
| run: | ||
| working-directory: keys-panel | ||
| steps: | ||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 | ||
| with: | ||
| go-version: '1.26.5' | ||
| - name: gofmt | ||
| run: | | ||
| unformatted="$(gofmt -l .)" | ||
| if [[ -n "$unformatted" ]]; then | ||
| echo "not gofmt-clean:" | ||
| echo "$unformatted" | ||
| exit 1 | ||
| fi | ||
| - run: go vet ./... | ||
| - run: go test ./... | ||
| - run: go build -trimpath ./... | ||
|
|
||
| shellcheck-syntax: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| - name: bash -n on every deploy script | ||
| run: | | ||
| set -euo pipefail | ||
| status=0 | ||
| while IFS= read -r -d '' script; do | ||
| echo "checking $script" | ||
| bash -n "$script" || status=1 | ||
| done < <(find deploy keys-panel/deploy -name '*.sh' -print0) | ||
| exit "$status" | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,146 @@ | ||
| # Continuous deployment | ||
|
|
||
| `master` is built and deployed by two GitHub Actions workflows, | ||
| [`.github/workflows/test.yml`](../.github/workflows/test.yml) and | ||
| [`.github/workflows/deploy.yml`](../.github/workflows/deploy.yml). This | ||
| document is the trust model behind them: why a self-hosted runner sitting on | ||
| the production host is safe to use on a public repository, what it can and | ||
| cannot do, and how to operate it. | ||
|
|
||
| ## The problem a self-hosted runner normally creates | ||
|
|
||
| GitHub's own documentation warns against using self-hosted runners with | ||
| public repositories: anyone can fork a public repo and open a pull request, | ||
| and if a workflow that pull request can influence executes on a self-hosted | ||
| runner, that PR gets arbitrary code execution on whatever host the runner | ||
| lives on. On this repository, that host is the production relay serving real | ||
| client keys - that would be a real compromise, not a hypothetical one. | ||
|
|
||
| ## Why this deployment doesn't have that problem | ||
|
|
||
| The two workflows are split by trust level, not just by task: | ||
|
|
||
| - **`test.yml`** runs on GitHub-hosted runners (`ubuntu-latest`) for both | ||
| `pull_request` (any branch, forks included) and `push` to `master`. Hosted | ||
| runners are ephemeral, sandboxed, and have no access to this host or its | ||
| secrets, so it's safe to run for anyone's PR. | ||
| - **`deploy.yml`** runs on the self-hosted runner (label `tproxy-deploy`) and | ||
| triggers **only** on `push` to `master` - never on `pull_request`, not even | ||
| for this repository's own PRs. | ||
|
|
||
| A `push` event on `master` can only exist as the result of a merge, and | ||
| `master` is a GitHub branch-protection-protected branch: direct pushes are | ||
| refused (`enforce_admins` is on, so this applies to the repository owner | ||
| too), and merging requires being a repository collaborator. A fork can | ||
| change `deploy.yml` or add a malicious step in its own copy of the file, but | ||
| nothing a fork does can make GitHub emit a `push` event against *this* | ||
| repository's `master` - only an actual merge, performed by someone with | ||
| write access, does that. That is the entire safety property the self-hosted | ||
| runner rests on; it does not depend on GitHub's separate "require approval | ||
| for outside collaborators" setting for workflow runs, though that setting is | ||
| also enabled here as defense in depth for `test.yml`. | ||
|
|
||
| Practical consequence: reviewing what's allowed to reach the runner reduces | ||
| to reviewing who can merge to `master` (currently: the repository owner | ||
| alone) and what `deploy.yml` and `ci-deploy.sh` actually execute (below) - | ||
| not to auditing every past or future community PR. | ||
|
|
||
| ## What actually runs on the runner, and as whom | ||
|
|
||
| The self-hosted runner's systemd service (`actions-runner-tproxy-server`) | ||
| runs as an unprivileged, dedicated Linux user (`ghrunner`), not root. Its | ||
| only path to privilege is one exact, argument-free sudoers rule: | ||
|
|
||
| ``` | ||
| ghrunner ALL=(root) NOPASSWD: /usr/local/sbin/ci-deploy.sh | ||
| ``` | ||
|
|
||
| `ci-deploy.sh` ([`deploy/ci-deploy.sh`](ci-deploy.sh)) takes no arguments and | ||
| reads nothing attacker-influenced beyond the already-merged repository | ||
| checkout at a fixed path; it calls | ||
| [`deploy/update-relay.sh`](update-relay.sh) and | ||
| [`keys-panel/deploy/update-keys-panel.sh`](../keys-panel/deploy/update-keys-panel.sh), | ||
| the same test-build-validate-install-with-rollback scripts described in the | ||
| main [`README.md`](../README.md#operations-and-updates) and | ||
| [`keys-panel/README.md`](../keys-panel/README.md). It does not touch | ||
| `profiles.json`, systemd units, Caddy, MTProxy's config, or the public site, | ||
| matching those scripts' own documented scope. | ||
|
|
||
| **`ci-deploy.sh` and its sudoers grant are not redeployed by the pipeline | ||
| they gate.** Installing `/usr/local/sbin/ci-deploy.sh` and the sudoers file | ||
| is a manual, one-time (or manually repeated) step on the runner host, | ||
| described below - never something `deploy.yml` writes to. If the pipeline | ||
| could rewrite the privileges it runs under, a single bad or malicious merge | ||
| could escalate itself on its very next run; keeping that file and its grant | ||
| outside the automated path is what prevents that. | ||
|
|
||
| ## Runner installation (reference / disaster recovery) | ||
|
|
||
| Already done once on the production host; this is what to repeat if the | ||
| runner needs reinstalling. | ||
|
|
||
| ```bash | ||
| sudo useradd --system --create-home --home /home/ghrunner --shell /usr/sbin/nologin ghrunner | ||
| sudo install -d -o ghrunner -g ghrunner -m 0755 /home/ghrunner/actions-runner | ||
| cd /home/ghrunner/actions-runner | ||
| sudo -u ghrunner curl --fail --silent --show-error --location \ | ||
| --proto '=https' --proto-redir '=https' --tlsv1.2 \ | ||
| -o actions-runner-linux-x64.tar.gz \ | ||
| https://github.com/actions/runner/releases/download/v2.337.0/actions-runner-linux-x64-2.337.0.tar.gz | ||
| test "$(sha256sum actions-runner-linux-x64.tar.gz | awk '{print $1}')" = "70920811a4f8ad4328818682bca5c6469c1c942fab52448868071d0063816613" | ||
| sudo -u ghrunner tar xzf actions-runner-linux-x64.tar.gz | ||
|
|
||
| # Registration tokens are single-use and expire in about an hour; generate | ||
| # one right before configuring the runner, from a machine with gh authorized | ||
| # against this repository (not necessarily this host): | ||
| # gh api -X POST repos/sandamond/tproxy-server/actions/runners/registration-token --jq .token | ||
|
|
||
| sudo -u ghrunner ./config.sh --url https://github.com/sandamond/tproxy-server \ | ||
| --token "$REGISTRATION_TOKEN" \ | ||
| --name dev-landing-prod --labels tproxy-deploy --work _work --unattended --replace | ||
| sudo ./svc.sh install ghrunner | ||
| sudo ./svc.sh start | ||
| ``` | ||
|
|
||
| Install the deploy wrapper and its narrow sudo grant (also manual, see above | ||
| for why): | ||
|
|
||
| ```bash | ||
| sudo install -o root -g root -m 0700 deploy/ci-deploy.sh /usr/local/sbin/ci-deploy.sh | ||
| echo 'ghrunner ALL=(root) NOPASSWD: /usr/local/sbin/ci-deploy.sh' | sudo tee /etc/sudoers.d/tproxy-ci-runner >/dev/null | ||
| sudo chmod 0440 /etc/sudoers.d/tproxy-ci-runner | ||
| sudo visudo -c | ||
| ``` | ||
|
|
||
| `ci-deploy.sh` changes: edit the file in the repository, get it merged like | ||
| anything else, then manually re-run the `install` line above on the runner | ||
| host. `deploy.yml` will keep calling the old version until that's done - by | ||
| design, per the section above. | ||
|
|
||
| ## Required repository settings | ||
|
|
||
| - Branch protection on `master`: PR required, `enforce_admins` on, force-push | ||
| and deletion disallowed (already configured). Once `test.yml` has run at | ||
| least once, its check should be added to `required_status_checks` so a red | ||
| test run blocks merging, not just informs it. | ||
| - "Fork pull request workflows from outside collaborators" set to require | ||
| approval for all external contributors (already applied): | ||
|
|
||
| ```bash | ||
| gh api --method PUT repos/sandamond/tproxy-server/actions/permissions/fork-pr-contributor-approval \ | ||
| -f approval_policy=all_external_contributors | ||
| ``` | ||
|
|
||
| Defense in depth for `test.yml` on the hosted runner; not load-bearing for | ||
| the self-hosted runner's safety, which rests on the trigger topology above. | ||
| This setting has three possible values | ||
| (`first_time_contributors_new_to_github`, `first_time_contributors`, | ||
| `all_external_contributors`) - GitHub's own naming, not something to guess | ||
| from the web UI label alone. | ||
|
|
||
| ## If the pipeline is down | ||
|
|
||
| `deploy/update-relay.sh` and `keys-panel/deploy/update-keys-panel.sh` are | ||
| ordinary scripts; run them by hand over SSH exactly as before this pipeline | ||
| existed if the runner or GitHub Actions itself is unavailable. Nothing about | ||
| normal operation depends on the pipeline being up. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| #!/usr/bin/env bash | ||
| set -euo pipefail | ||
|
|
||
| # Invoked as root via a single exact-match, no-argument sudoers rule from the | ||
| # unprivileged CI runner account (see deploy/CI.md). Nothing about this | ||
| # invocation is attacker-controllable through its arguments, because it takes | ||
| # none. The only externally-influenced input is the already fully tested | ||
| # repository checkout at the fixed path below, which only reaches this | ||
| # script by way of a `git push` to this repository's branch-protected | ||
| # `master` - i.e. only after a merge, which only a repository collaborator | ||
| # can perform. See deploy/CI.md for the full trust boundary this rests on. | ||
| # | ||
| # This script itself is NOT redeployed by the pipeline it drives: installing | ||
| # it and granting sudo access to it is a deliberate one-time (or manually | ||
| # repeated) step on the runner host, kept separate from the automatic update | ||
| # path below on purpose - see deploy/CI.md. | ||
|
|
||
| repository=/home/ghrunner/actions-runner/_work/tproxy-server/tproxy-server | ||
|
|
||
| if [[ "${EUID}" -ne 0 ]]; then | ||
| echo "run as root (this script expects to be invoked through sudo)" >&2 | ||
| exit 1 | ||
| fi | ||
| if [[ ! -d "$repository/.git" ]]; then | ||
| echo "expected repository checkout not found at $repository" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| echo "== relay ==" | ||
| "$repository/deploy/update-relay.sh" | ||
|
|
||
| echo "== tproxy-keys ==" | ||
| "$repository/keys-panel/deploy/update-keys-panel.sh" | ||
|
|
||
| echo "deploy complete" |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.