https://github.com/tryone144/github_action_webhook_deployment
Automatic Website Depolyment via GitHub Actions + Webhook
https://github.com/tryone144/github_action_webhook_deployment
Last synced: 10 months ago
JSON representation
Automatic Website Depolyment via GitHub Actions + Webhook
- Host: GitHub
- URL: https://github.com/tryone144/github_action_webhook_deployment
- Owner: tryone144
- License: apache-2.0
- Created: 2024-06-19T19:40:36.000Z (almost 2 years ago)
- Default Branch: master
- Last Pushed: 2024-07-19T13:29:51.000Z (almost 2 years ago)
- Last Synced: 2025-03-27T05:27:32.357Z (about 1 year ago)
- Language: Python
- Size: 32.2 KB
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Automatic Website Deployment via GitHub Actions + Webhook
## Introduction
The scenario of this deployment automation is a server hosting a website
generated by the static-site generator [Next.js](https://nextjs.org). The
sources are hosted on GitHub. Whenever the sources are updated on GitHub,
the website is built via a GitHub Actions workflow and the static files are
deployed on the webserver.
The setup described here is used for .
> **Note:** While the workflows are specifically tailored to a Next.js project,
> the webhook is framework agnostic and can deploy the generated artifact of
> any static-site generator.
## Process Overview
The sources of the website are hosted in a GitHub repository. The repository
contains a GitHub Actions workflow to build the static site and upload the
generated site as a release asset. This workflow is triggered by a push. After
uploading, the workflow triggers a GitHub Deployment event. The repository is
configured to call a webhook on `deployment_status` events.
The webhook is implemented on the webserver as a Python script which reads a
configuration file that contains the local repository and deployment target
location for one or more repositories.
If the deployment request is valid, then a deployment script is called with the
configured parameters and the committer's email address. The script is also
implemented as a Python script. It performs the following steps:
1. Set the deployment status to `in_progress` on GitHub
1. Download the associated release asset, and extract it to a uniquely named
directory next to the symlink
1. Remove the downloaded asset
1. Replace the symlink atomically with a link to new HTML directory
1. Remove old HTML directory
1. Set deployment status to `success` on GitHub
1. Send the logs to configured maintainers and the committer
Access to the GitHub API for downloading the release asset and updating the
deployment status is handled via a GitHub App. This app manages retrieval of
short-lived and restricted access tokens. To provide these permissions, the app
has to be installed by the owner of the repository.
## Python Dependencies
The scripts have been successfully tested with Python 3.10.12, but newer
versions should work.
In addition to the standard library, the following libraries are required:
- [`requests`](https://github.com/psf/requests) (tested with 2.25.1)
- [`jsonschema`](https://github.com/python-jsonschema/jsonschema) (tested with 3.2.0)
- [`pyjwt`](https://github.com/jpadilla/pyjwt) (tested with 2.3.0)
- [`zc.lockfile`](https://github.com/zopefoundation/zc.lockfile) (tested with 2.0)
On Ubuntu these can be installed by
```console
$ apt install python3-requests python3-jsonschema python3-jwt python3-zc.lockfile
```
## Webserver Configuration
All configuration shown here is for Apache 2.4.
### Webhook
Configure the Python script implementing the webhook in a suitable virtual host
or globally by using this sample
[config](etc/apache2/conf-available/deploywebhookgithub.conf). We assume the
virtual host `example.com` here.
```apache
ScriptAlias /deploy /usr/local/sbin/deploywebhookgithub
Require all granted
```
Create the configuration file for the webhook in
[`/etc/deploywebhookgithub/config.json`](etc/deploywebhookgithub/config.json):
```json
{
"deploy_user": "deploy_website",
"client_id": "",
"client_key": "",
"log_recipients": ["admin@example.com"],
"repositories": {
"githubuser/repository": {
"signature_key": "",
"log_recipients": ["maintainer@example.com"],
"environments": {
"production": {
"deploy_url": "https://www.example.com/",
"html_symlink": "/var/www/example.com/root"
}
}
}
}
}
```
Set permissions to allow the webserver to read the file.
```console
$ sudo chmod 640 /etc/deploywebhookgithub/config.json
$ sudo chown root:www-data /etc/deploywebhookgithub/config.json
```
The configuration contains five top-level keys:
1. `deploy_user`: A special user to run the `deploy_website` script via
`sudo`. This can be configured with more restrictive permissions.
1. `client_id`: The client ID of the GitHub App used for authentication (see
[below](#github-configuration)).
1. `client_key`: The path to an RSA private key of the GitHub App in PEM format.
Used for requesting an installation access token to authenticate the
deployment script against the GitHub API (see [below](#github-configuration)).
1. `log_recipients` (optional): A list of email addresses to receive the log
messages for all deployments.
You can configure multiple GitHub repositories in `repositories`. The example
above includes a single repository: `githubuser/repository`.
For each repository the configuration contains three keys:
1. `signature_key`: A random key used to authenticate GitHub to the webhook
script. The key needs to be configured in the GitHub webhook configuration,
as well (see [below](#github-configuration)).
1. `log_recipients` (optional): A list of email addresses to receive the log
messages for each deployment of this repository.
Each repository has a separate entry for the deployment environment. The
example contains only one environment: `production`.
For each repository and environment the configuration contains three keys:
1. `deploy_url`: The public facing URL to the deployment of this environment.
1. `html_symlink`: The symlink pointing to HTML root directory of the website.
The symlink needs to be configured in the web server as the document root
and will be replaced by `deploy_website` with a new directory containing the
generated static-site output.
1. `log_recipients` (optional): A list of email addresses to receive the log
messages for each deployment in this specific environment.
### Website
The virtual host of the website example.com needs to be configured with
`html_symlink` (see above) as document root.
```apache
DocumentRoot "/var/www/example.com/root"
```
## Unix Configuration
### Install Scripts
```console
$ sudo install --owner=root --group=root bin/deploywebhookgithub bin/deploy_website /usr/local/sbin
```
### Deployment User
The deployment script `deploy_website` is run as user `deploy_website` via
`sudo` from `deploywebhookgithub`. The user can be configured in
`/etc/deploywebhookgithub/config.json` with `deploy_user`.
Using a different user than the webserver user `www-data` makes the static
website read-only for the webserver.
```console
$ sudo adduser --system --ingroup www-data --disabled-password --gecos 'User for deploying websites via github webhook' deploy_website
```
### Sudo Configuration
To allow the webserver to run the `deploy_website` script as the user with the
same name, create the file [`/etc/sudoers.d/deploy_website`](etc/sudoers.d/deploy_website)
with the following content:
```sudo
Cmnd_Alias DEPLOYCMD = \
/usr/local/sbin/deploy_website /var/www/example.com/root githubuser/repository production *
Defaults!DEPLOYCMD env_keep+="GITHUB_TOKEN SIGNATURE_KEY"
%www-data ALL=(deploy_website)NOPASSWD: DEPLOYCMD
```
The paths of the HTML root, repository name and environment must match the
webhook configuration in `/etc/deploywebhookgithub/config.json`. Multiple
paths/repositories/environments can be configured as required. The wildcard at
the end of the command is required for passing the deployment-id, asset-url
and -checksum and email addresses.
## GitHub Configuration
### Workflows
Copy the [`development.yml`](./workflows/development.yml) and
[`release.yml`](./workflows/release.yml) workflow definitions into your GitHub
repository at `.github/workflows`.
In [`release.yml`](./workflows/release.yml), adjust the branches that trigger a
deployment and configure the environment variables `SHOULD_DEPLOY` to your
repository name, and `DEPLOY_(ENVIRONMENT|URL)` and your environment names and
respective urls.
> **Note:** The `release` workflow is only run for the repository mentioned in
> `SHOULD_DEPLOY`.
> **Note:** These workflows are specifically tailored to a Next.js project, but
> should be easily adjusted to other `npm` compatible static-site generators
> with `test` and `export` scripts.
### Webhook
In the GitHub repository `Settings` select `Webhooks` and `Add webhook` and
enter the following parameters:
`Payload URL:` https://example.com/deploy
> **Note:** The URL needs to match the ScriptAlias in the
> [Webserver Configuration](#webserver-configuration) above.
`Content type: application/json`
`Secret:` random-value
> **Note:** The secret needs to match the value configured in
> `/etc/deploywebhookgithub/config.json`
`SSL verification:` Select `Enable SSL verifcation`
`Which events would you like to trigger this webhook?` Select `Let me select
individual events.` and enable `Deployment statuses`
### Secrets
In the GitHub repository `Settings` select `Secrets and Variables > Actions`
and `New repository secret` and enter the following parameters:
`Name:` DEPLOYMENT_KEY
`Secret:` random-value
> **Note:** The secret needs to match the value configured for the Webhook
> and in `/etc/deploywebhookgithub/config.json`
### GitHub App
Authenticating the deployment script against the GitHub API is done via a
GitHub App.
To register a new GitHub App for the organization, go to `Settings` in the
organization scope and select `Developer Settings > GitHub Apps` and
`New GitHub App` and enter the following parameters:
`GitHub App name:` PROJECT Webhook Deployment
`Homepage URL:` https://github.com/USER
`Webhook`: De-select `Active`
`Permissions > Repository permissions` Select `Read-only` for `Contents` and
`Read and write` for `Deployments`
> **Note:** The intended use for this app is download the generated artifact
> and update the deployment status of select repositories. No further
> permissions are required
`Where can this GitHub App be installed?` Select `Only on this account` to
keep this app private
After creating the app, go to `General` and select `Generate a private key`.
Securely store this key on the server at the configured path (see
[above](#webserver-configuration)). For example, save it next to the
configuration file at `/etc/deploywebhookgithub/private_key.pem`.
> **Note:** Set permissions to allow the webserver to read the file:
> ```console
> $ sudo chmod 640 /etc/deploywebhookgithub/private_key.pem
> $ sudo chown root:www-data /etc/deploywebhookgithub/private_key.pem
> ```
Finally, install the application for the organization by going to `Install App`
and selecting `Install` on the target account. On the installation page, select
`Only select repositories` and select all repositories you have configured in
the configuration file above.
> **Note:** You can re-use the same app when setting up a second instance of
> this deployment webhook on a different endpoint. For additional security,
> generate a second private key for that instance.
## Security
### Webserver
This section analyzes the risk for the webserver.
The webhook implementation increases the attack surface with it's REST endpoint
to a limited degree.
Calls to the REST endpoint are protected by a HMAC signature of the webhook
payload. This signature does not protection against replay attacks. This could
allow an attacker to trigger deployment of older instances.
The replay risk can be further mitigated by protecting the endpoint with TLS,
which is advisable in any case to protect the transmitted information. A
different signature key can and should be used for each configured repository.
The potentially untrusted information transmitted by the webhook is used to:
1. Lookup parameters in the configuration file - **no risk**
1. Verify the signature - **no risk**
1. Create a deployment-specific HTML root - **low risk**, see below
1. Download the generated assets - **low risk**, see below
1. Determine the committer's email address - **low risk**, see below
The deployment script is called with parameters looked up from the
configuration file and the webhook payload. While the former are trusted, the
latter are individually validated.
- To generate a deployment-specific HTML root, the deployment-id and commit-sha
are extracted from the webhook payload. These are validated to be a number and
sha1 string. Under these circumstances, they can't escape the configured root-
directory for that deployment.
- The download URL to the generated asset is verified to point to a release
asset in the configured repository. This URL is hardcoded in the deployment
metadata an cannot be changed. Furthermore, downloads are limited to 2GiB.
- The asset is protected by an HMAC (with the same secret used for the webhook).
This is hardcoded into the deployment metadata and cannot be changed.
- The residual risk of using the externally provided email address is sending
an email with the deployment logs to a potentially manipulated email address.
Special shell-escaping of the parameters is not necessary as the deployment
script is called directly via `execvpe`.
The deployment script should be called with `sudo` using a non-privileged user,
as described above. This allows the website to be deployed read-only for the
webserver.
The deployment script performs the following actions:
1. Download the associated release asset - **low risk**, URL is validated and
downloads larger than 2GiB skipped.
1. Create a deployment-specific HTML root - **no risk**, dynamic filename
components are verified to contain no special characters.
1. Extract the downloaded asset into the new HTML root - **low risk**, the asset
is protected by an HMAC. To protect against directory traversals outside the
target directory, we rely on `tar`.
1. Replace the symlink atomically with a link to new HTML directory - **no risk**
1. Remove old HTML directory - **no risk**
1. Email the Jekyll logs - **low risk**, see above
In summary the download and extraction of a tar archive poses a limited risk if
vulnerabilities are found in `tar` and the GitHub repository contains attacker
controlled input to generate a malicious file.
### Website
The integrity of the website depends on the protection of the GitHub repository.
Anybody who can push to the repository or subvert GitHub security controls can
change the website. Special care has to be taken when changing the GitHub Action
workflows and export scripts.
In addition the integrity of the website depends on the integrity of the
webserver.
## Troubleshooting
### GitHub Webhook
In the webhook configuration on GitHub all executed webhook calls are listed
and show the details including the server response.
A 200 or 202 response code with empty body indicates that the call was accepted
and the `deploy_website` script called. A message in the body indicates that the
call was ignored.
Response codes 40x indicate an error, e.g. a repository or environment not
found in the webhook configuration, missing information in the webhook payload
or an invalid `signature_key`.
A response code of 500 indicates a more fundamental error that needs to be
investigated in the webserver error logs.
### Websserver Logs
The webserver error logs show for each webhook call the JSON body, information
on errors, the `deploy_website` call with its arguments and its output.
### `deploy_website` Output
The log output as well as errors detected by the `deploy_website` script are
emailed to the last git committer leading to the webhook call as well as the
recipients configured in `/etc/deploywebhookgithub/config.json`. For this to
work a valid email address needs to be configured by the developer on his/her
local machine. It can be checked and updated with the following commands:
```console
$ git config --global user.email
$ git config --global user.email name@example.com
```
> **Note:** Using a GitHub issued no-reply address silently swallows the logs.