Skip to content

Commit a843e7b

Browse files
committed
new poast and new font
1 parent 76fbc4f commit a843e7b

3 files changed

Lines changed: 189 additions & 1 deletion

File tree

src/components/BaseHead.astro

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ const { title, description, image = '/blog-placeholder-1.jpg' } = Astro.props;
2525
<!-- RSS autodiscovery -->
2626
<link rel="alternate" type="application/rss+xml" title={title} href={`${Astro.site}rss.xml`} />
2727

28+
<link rel="preconnect" href="https://fonts.googleapis.com">
29+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
30+
<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">
31+
2832
<!-- Primary Meta Tags -->
2933
<title>{title}</title>
3034
<meta name="title" content={title} />
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
---
2+
title: continuous deployment with SSH and tailscale
3+
description: A guide in implementing continuous deployment with SSH and Tailscale
4+
pubDate: May 13 2025
5+
---
6+
7+
*Skip to [high level overview](#high-level-overview) if you are not interested in the back story.*
8+
9+
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.
10+
11+
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.
12+
13+
Thankfully, using [Tailscale](https://tailscale.com/), I can securely access any of my devices, even outside my home network.
14+
15+
## Why Tailscale?
16+
17+
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.
18+
19+
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.
20+
21+
## High Level Overview
22+
23+
1. Trigger continuous deployment when commits are made to the main branch
24+
2. Connect the runner of the continuous deployment pipeline to the tailnet to which your host is connected
25+
3. SSH into the host using the assigned IP
26+
4. Pull the new commits on the host, and re-deploy the website
27+
28+
I will be using GitHub Action syntax for the CI/CD config file, but the overarching concept applies to any CI/CD environment.
29+
30+
## The Basics
31+
32+
Start with the following code:
33+
34+
```yml
35+
on:
36+
push:
37+
branches:
38+
- main
39+
workflow_dispatch:
40+
41+
jobs:
42+
deploy:
43+
runs-on: ubuntu-latest
44+
name: Deploy website to server
45+
env:
46+
MACHINE_USER_NAME: user
47+
MACHINE_NAME: my-host
48+
```
49+
50+
This allows the pipeline to be triggered when commits are made to the `main` branch, and manually in the GitHub UI.
51+
52+
Two environment variables are also defined here:
53+
- `MACHINE_USER_NAME`: the name of the user used for SSH-ing into the machine; and
54+
- `MACHINE_NAME`: the name of the machine assigned by Tailscale. You can find this out in the admin console of Tailscale.
55+
56+
## Setting Up Tailscale GitHub Action
57+
58+
### Defining a Tag
59+
60+
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.
61+
62+
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`.
63+
64+
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:
65+
66+
```json
67+
{
68+
"tagOwners": {
69+
"tag:ci": ["autogroup:owner"]
70+
}
71+
}
72+
```
73+
74+
`"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.
75+
76+
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.
77+
78+
```json
79+
{
80+
"acls": [
81+
// other lists
82+
{"action": "accept", "src": ["tag:ci"], "dst": ["1.2.3.4:22"]}
83+
]
84+
}
85+
```
86+
87+
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.
88+
89+
### Creating an OAuth Client
90+
91+
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.
92+
93+
Now, add a step in the workflow file to set up Tailscale:
94+
95+
```yml
96+
on:
97+
push:
98+
branches:
99+
- main
100+
workflow_dispatch:
101+
102+
jobs:
103+
deploy:
104+
runs-on: ubuntu-latest
105+
name: Deploy website to server
106+
env:
107+
MACHINE_USER_NAME: user
108+
MACHINE_NAME: my-host
109+
steps:
110+
- name: Setup Tailscale
111+
uses: tailscale/github-action@v3
112+
with:
113+
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
114+
oauth-secret: ${{ secrets.TS_OAUTH_CLIENT_SECRET }}
115+
tags: 'tag:ci'
116+
```
117+
118+
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.
119+
120+
## Setting Up SSH Access
121+
122+
To let the CI/CD runner authenticate over SSH, we will need to:
123+
124+
1. Generate an SSH key pair
125+
2. Install the public key to the host machine, and
126+
3. Save the private key as a GitHub secret to be used in the workflow.
127+
128+
Once you have completed the steps above, add the following workflow step after the Tailscale setup step:
129+
130+
```yml
131+
- name: Add SSH key
132+
env:
133+
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
134+
run: |
135+
mkdir -p ~/.ssh
136+
MACHINE_IP="$(tailscale ip -4 $MACHINE_NAME)"
137+
ssh-keyscan $MACHINE_IP >> ~/.ssh/known_hosts
138+
printf "%s" "$SSH_PRIVATE_KEY" > ~/.ssh/key
139+
# add a new line to the end of the private key file
140+
# otherwise it won't be loaded properly
141+
echo >> ~/.ssh/key
142+
chmod 600 ~/.ssh/key
143+
```
144+
145+
Let's break down the script:
146+
147+
1. Create the default SSH directory if it does not exist
148+
2. Using Tailscale's CLI to find the IPv4 of the host machine by its assigned name, and store it in `MACHINE_IP` variable
149+
3. Perform a key scan on the machine, then add it to the list of known hosts
150+
4. Save the private key in GitHub secret (made available as an environment variable via the `env` block) to `~/.ssh/key` file
151+
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
152+
6. Correct the permission of the key file
153+
154+
## Deploying the Website
155+
156+
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.
157+
158+
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.
159+
160+
```yml
161+
- name: Deploy website
162+
env:
163+
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
164+
run: |
165+
MACHINE_IP="$(tailscale ip -4 $MACHINE_NAME)"
166+
ssh -i ~/.ssh/key "$MACHINE_USER_NAME@$MACHINE_IP" /bin/bash << EOF
167+
cd /opt/website
168+
git pull
169+
docker build -t website .
170+
docker stop website-container
171+
docker rm website-container
172+
docker run --name website-container --restart=always --publish 5432:80 --detach website
173+
EOF
174+
```
175+
176+
## References
177+
178+
- [Using GitHub Actions and Tailscale to build and deploy applications securely](https://tailscale.com/blog/2021-05-github-actions-and-tailscale)
179+
180+
## Final Notes
181+
182+
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.
183+
184+
Please don't hesitate to reach out should you spot any mistake.

src/styles/global.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ pre {
7474
}
7575

7676
body.blog {
77-
font-family: "Georgia", serif;
77+
font-family: "Source Serif 4", "Georgia", serif;
7878
}
7979

8080
.nf {

0 commit comments

Comments
 (0)