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.
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:
- Set the deployment status to
in_progress
on GitHub - Download the associated release asset, and extract it to a uniquely named directory next to the symlink
- Remove the downloaded asset
- Replace the symlink atomically with a link to new HTML directory
- Remove old HTML directory
- Set deployment status to
success
on GitHub - 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.
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
(tested with 2.25.1)jsonschema
(tested with 3.2.0)pyjwt
(tested with 2.3.0)zc.lockfile
(tested with 2.0)
On Ubuntu these can be installed by
$ apt install python3-requests python3-jsonschema python3-jwt python3-zc.lockfile
All configuration shown here is for Apache 2.4.
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:
deploy_user
: A special user to run thedeploy_website
script viasudo
. This can be configured with more restrictive permissions.client_id
: The client ID of the GitHub App used for authentication (see below).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).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:
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).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:
deploy_url
: The public facing URL to the deployment of this environment.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 bydeploy_website
with a new directory containing the generated static-site output.log_recipients
(optional): A list of email addresses to receive the log messages for each deployment in this specific environment.
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"
$ sudo install --owner=root --group=root bin/deploywebhookgithub bin/deploy_website /usr/local/sbin
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
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.
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 inSHOULD_DEPLOY
.
Note: These workflows are specifically tailored to a Next.js project, but should be easily adjusted to other
npm
compatible static-site generators withtest
andexport
scripts.
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
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
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.
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:
- Lookup parameters in the configuration file - no risk
- Verify the signature - no risk
- Create a deployment-specific HTML root - low risk, see below
- Download the generated assets - low risk, see below
- 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:
- Download the associated release asset - low risk, URL is validated and downloads larger than 2GiB skipped.
- Create a deployment-specific HTML root - no risk, dynamic filename components are verified to contain no special characters.
- 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
. - Replace the symlink atomically with a link to new HTML directory - no risk
- Remove old HTML directory - no risk
- 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.
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.
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.
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.
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.