Skip to content

tryone144/github_action_webhook_deployment

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

12 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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. 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 https://www.cryptool.org/.

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
  2. Download the associated release asset, and extract it to a uniquely named directory next to the symlink
  3. Remove the downloaded asset
  4. Replace the symlink atomically with a link to new HTML directory
  5. Remove old HTML directory
  6. Set deployment status to success on GitHub
  7. 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:

On Ubuntu these can be installed by

$ 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. We assume the virtual host example.com here.

ScriptAlias /deploy /usr/local/sbin/deploywebhookgithub
<Location /deploy>
    Require all granted
</Location>

Create the configuration file for the webhook in /etc/deploywebhookgithub/config.json:

{
  "deploy_user": "deploy_website",
  "client_id": "<client ID of GitHub App>",
  "client_key": "<path to private key for GitHub App>",
  "log_recipients": ["[email protected]"],
  "repositories": {
    "githubuser/repository": {
      "signature_key": "<random key created with: openssl rand -base64 32>",
      "log_recipients": ["[email protected]"],
      "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.

$ 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.
  2. client_id: The client ID of the GitHub App used for authentication (see below).
  3. 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).
  4. 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).
  2. 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.
  2. 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.
  3. 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.

DocumentRoot "/var/www/example.com/root"

Unix Configuration

Install Scripts

$ 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.

$ 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 with the following content:

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 and release.yml workflow definitions into your GitHub repository at .github/workflows.

In 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 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). 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:

$ 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
  2. Verify the signature - no risk
  3. Create a deployment-specific HTML root - low risk, see below
  4. Download the generated assets - low risk, see below
  5. 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.
  2. Create a deployment-specific HTML root - no risk, dynamic filename components are verified to contain no special characters.
  3. 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.
  4. Replace the symlink atomically with a link to new HTML directory - no risk
  5. Remove old HTML directory - no risk
  6. 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:

$ git config --global user.email
$ git config --global user.email [email protected]

Note: Using a GitHub issued no-reply address silently swallows the logs.

About

Automatic Website Depolyment via GitHub Actions + Webhook

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages