new poast and new font
Some checks failed
/ Deploy website to server (push) Has been cancelled

This commit is contained in:
2025-05-14 00:05:11 +01:00
parent 76fbc4f439
commit a843e7b902
3 changed files with 189 additions and 1 deletions

View File

@@ -25,6 +25,10 @@ const { title, description, image = '/blog-placeholder-1.jpg' } = Astro.props;
<!-- RSS autodiscovery -->
<link rel="alternate" type="application/rss+xml" title={title} href={`${Astro.site}rss.xml`} />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&display=swap" rel="stylesheet">
<!-- Primary Meta Tags -->
<title>{title}</title>
<meta name="title" content={title} />

View File

@@ -0,0 +1,184 @@
---
title: continuous deployment with SSH and tailscale
description: A guide in implementing continuous deployment with SSH and Tailscale
pubDate: May 13 2025
---
*Skip to [high level overview](#high-level-overview) if you are not interested in the back story.*
I recently purchased a cheap Dell OptiPlex to act as my own server. I have since successfully moved all my services to the machine, including my [git server](https://code.nym.sh) and my website which was previously hosted on [Fly](https://fly.io). Without the awesome `fly deploy` command to deploy my website, I had to find another way to automate the deployment of my website.
Here comes the problem: my website is exposed to the public via [CloudFlare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/), but the host running my website isn't publicly accessible. This means that, for instance, I cannot SSH into the host in a CI/CD environment, such as GitHub Actions.
Thankfully, using [Tailscale](https://tailscale.com/), I can securely access any of my devices, even outside my home network.
## Why Tailscale?
Based on [WireGuard](https://www.wireguard.com/), Tailscale lets you set up your own VPN with ease for your own devices, which forms a "tailnet". Any device connected to the tailnet can securely access other devices within it using the corresponding assigned address.
Therefore, if I can manage to connect a machine to my tailnet, I will be able to access my host anywhere, in any environment, including CI/CD pipelines. Fortunately, Tailscale maintains a [GitHub Action](https://tailscale.com/kb/1276/tailscale-github-action) that lets you connect to your tailnet in a GitHub Action runner.
## High Level Overview
1. Trigger continuous deployment when commits are made to the main branch
2. Connect the runner of the continuous deployment pipeline to the tailnet to which your host is connected
3. SSH into the host using the assigned IP
4. Pull the new commits on the host, and re-deploy the website
I will be using GitHub Action syntax for the CI/CD config file, but the overarching concept applies to any CI/CD environment.
## The Basics
Start with the following code:
```yml
on:
push:
branches:
- main
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
name: Deploy website to server
env:
MACHINE_USER_NAME: user
MACHINE_NAME: my-host
```
This allows the pipeline to be triggered when commits are made to the `main` branch, and manually in the GitHub UI.
Two environment variables are also defined here:
- `MACHINE_USER_NAME`: the name of the user used for SSH-ing into the machine; and
- `MACHINE_NAME`: the name of the machine assigned by Tailscale. You can find this out in the admin console of Tailscale.
## Setting Up Tailscale GitHub Action
### Defining a Tag
In Tailscale, you can assign and group machines with *tags*. One of their primary purposes is to allow you to apply access control to machines based on tags.
In this case, it is useful to assign the runner running the CI/CD pipeline a tag. Then, using Tailscale's [Access Control Lists (ACLs)](https://tailscale.com/kb/1018/acls), we can limit access the tag has to the host to which we wish to deploy. We will name it `tag:ci`.
To define a tag, navigate to the "Access Controls" page in the admin console. You will then be presented with an editor for the ACL file. Within the `"tagOwners"` key, add the following:
```json
{
"tagOwners": {
"tag:ci": ["autogroup:owner"]
}
}
```
`"autogroup:owner"` specifies that the owner of the tailnet can apply this tag. Before we move on, make a note of the IP address of the machine that will be hosting the service.
Now, let's define an ACL for the tag. We want machines that are tagged with `tag:ci` to be able to SSH into the host machine and nothing else. As an example, we will use `1.2.3.4` as the IP address of the machine.
```json
{
"acls": [
// other lists
{"action": "accept", "src": ["tag:ci"], "dst": ["1.2.3.4:22"]}
]
}
```
This specifies that machines tagged with `tag:ci` can only access machine with IP `1.2.3.4` on port `22`, which is the default SSH port. If your machine has a non-standard SSH port, change `22` to the correct port.
### Creating an OAuth Client
Tailscale's GitHub Action relies on OAuth2 for authentication. To get started, go to the admin console, then navigate to the "OAuth Clients" section in settings. Give the client a descriptive name, and make sure the [`auth_keys` scope](https://tailscale.com/kb/1215/oauth-clients#scopes) is enabled. Take a note of the client ID and client secret, and store them as GitHub Action secrets.
Now, add a step in the workflow file to set up Tailscale:
```yml
on:
push:
branches:
- main
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
name: Deploy website to server
env:
MACHINE_USER_NAME: user
MACHINE_NAME: my-host
steps:
- name: Setup Tailscale
uses: tailscale/github-action@v3
with:
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
oauth-secret: ${{ secrets.TS_OAUTH_CLIENT_SECRET }}
tags: 'tag:ci'
```
This step installs Tailscale on the runner machine, and authenticates it into our tailnet using the OAuth credentials we created. The `tag:ci` is also applied to the runner, which means it can access the machine at `1.2.3.4` on port 22 and nothing else.
## Setting Up SSH Access
To let the CI/CD runner authenticate over SSH, we will need to:
1. Generate an SSH key pair
2. Install the public key to the host machine, and
3. Save the private key as a GitHub secret to be used in the workflow.
Once you have completed the steps above, add the following workflow step after the Tailscale setup step:
```yml
- name: Add SSH key
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
run: |
mkdir -p ~/.ssh
MACHINE_IP="$(tailscale ip -4 $MACHINE_NAME)"
ssh-keyscan $MACHINE_IP >> ~/.ssh/known_hosts
printf "%s" "$SSH_PRIVATE_KEY" > ~/.ssh/key
# add a new line to the end of the private key file
# otherwise it won't be loaded properly
echo >> ~/.ssh/key
chmod 600 ~/.ssh/key
```
Let's break down the script:
1. Create the default SSH directory if it does not exist
2. Using Tailscale's CLI to find the IPv4 of the host machine by its assigned name, and store it in `MACHINE_IP` variable
3. Perform a key scan on the machine, then add it to the list of known hosts
4. Save the private key in GitHub secret (made available as an environment variable via the `env` block) to `~/.ssh/key` file
5. GitHub trims any trailing newline character in a secret value. Because SSH expects a trailing newline character at the end of a key file, a new line needs to be appended to the end of the key file created in the previous step
6. Correct the permission of the key file
## Deploying the Website
With everything in place, the runner is now able to SSH into the host machine via Tailscale! How you deploy your services may vary, so I will use [my website](https://github.com/kennethnym/website/blob/main/.github/workflows/deploy.yml) as an example, which is stored as a git repo on the host machine and is run as a Docker container.
The important part here is obtaining the machine IP using its assigned name, which we did in the previous step; passing the correct key file to SSH; and finally providing the correct username and IP.
```yml
- name: Deploy website
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
run: |
MACHINE_IP="$(tailscale ip -4 $MACHINE_NAME)"
ssh -i ~/.ssh/key "$MACHINE_USER_NAME@$MACHINE_IP" /bin/bash << EOF
cd /opt/website
git pull
docker build -t website .
docker stop website-container
docker rm website-container
docker run --name website-container --restart=always --publish 5432:80 --detach website
EOF
```
## References
- [Using GitHub Actions and Tailscale to build and deploy applications securely](https://tailscale.com/blog/2021-05-github-actions-and-tailscale)
## Final Notes
We have successfully created a GitHub Action workflow that automatically deploys a service over SSH using Tailscale, without public access to our host machine. I hope you find this guide helpful, and thank you for reading.
Please don't hesitate to reach out should you spot any mistake.

View File

@@ -74,7 +74,7 @@ pre {
}
body.blog {
font-family: "Georgia", serif;
font-family: "Source Serif 4", "Georgia", serif;
}
.nf {