Files

130 lines
5.2 KiB
Markdown
Raw Permalink Normal View History

2026-05-24 19:40:12 -07:00
# 📦 `tools-gatekeeper`
A lightweight web server that protects **private S3 files** using **token-based authentication**.
> Originally tested with [DigitalOcean Spaces](https://www.digitalocean.com/products/spaces/) and served via [Cloudflare CDN](https://www.cloudflare.com/cdn/).
> Compatible with **any S3-compatible provider**.
## ⚙️ Configuration
You can configure the Gatekeeper through **environment variables**.
> **Required variables** are marked with an asterisk `*`
| Environment Variable | Description |
| -------------------- | -------------------------------------------------------------------- |
| `*S3_ACCESS_KEY` | S3-compatible API access key |
| `*S3_SECRET_KEY` | S3-compatible API secret key |
| `*S3_ENDPOINT` | S3 Endpoint (e.g. `https://nyc3.digitaloceanspaces.com`) |
| `*S3_REGION` | S3 Region (e.g. `us-east-1`) |
| `S3_BUCKET` | S3 Bucket Name |
| `HTTP_ADDRESS` | Address and port to accept requests on, defaults to `localhost:8080` |
| `HTTP_CORS_ORIGIN` | Enables CORS for the given origin. Leave blank to disable. |
| `HTTP_KEY` | Shared secret used for token verification. |
## 🔰 Generating Tokens
Gatekeeper protects all files under the `/private` path in your bucket.
Any request to `/private/...` must include a valid token in the query parameter:
In this example we'll be hosting a anime website and our directory structure looks like this:
```
. cdn.anime4all.net
|__ /site
| |__ /images
| | |__ index-hero.png
| | |__ index-carousel-1.png
| | |__ index-carousel-2.png
| | |__ index-carousel-3.png
| |__ /assets
| |__ /favicon.png
| |__ /index.css
| |__ /index.js
|__ /private
|__ /wallpapers
| |__ codegeass_16x9_1080.png
| |__ cowboybebop_4x3_480.png
| |__ evangelion_4x3_480.png
|__ /avatars
|__ miku.png
|__ teto.png
|__ neru.png
```
To access our super cool avatars and wallpapers, users must be logged in — those files live inside the /private directory and require a valid token.
---
### Step 1. Create Payload
Our gatekeeper expects the token payload to at minimum include two fields:
* `pattern:` Our filename matching pattern
* `expires:` The UNIX Timestamp for when our token expires
> **Note:** You can include arbitrary fields like `user_id` for your own logging purposes or whatnot.
> The gatekeeper will ignore them.
Our website displays all avatars on a single page so we'll give our user full access to the private avatars directory, which matches the pattern `/private/avatars/*`.
If we wanted to give them access to a single file we could alternatively do `/private/avatars/teto.png`.
> **Note:** The gatekeeper uses the [path.Match](https://pkg.go.dev/path#Match) function internally for pattern matching. You can read it's documentation to learn more.
```go
payload, err := json.Marshal(map[string]any{
"pattern": "/private/avatars/*", // Allow all files in avatars folder
"expires": time.Now().Unix() + 3600 // Token expires in 1 hour
})
if err != nil {
panic(err)
}
```
Best practice is to ensure your server clock is correct and that the token lifetime isn't longer than it should be!
---
### Step 2. Generating Signature
After we stringified our JSON payload we will sign its bytes with HMAC-SHA256 and our secret key which we share between the gatekeeper and our webserver. In GO we can do this using the hmac standard library.
```go
signer := hmac.New(sha256.New, []byte("your-shared-secret-key")) // envvar: HTTP_SIGNATURE
signer.Write(payload)
signature := signer.Sum(nil)
```
---
### Step 3. Compiling Token
Finally we encode both the payload and signature into base64 (URL encoding is fine, but raw encoding is shorter),
and concatenate them together using a period as the delimiter.
```go
encodedPayload := base64.RawURLEncoding.EncodeToString(payload)
encodedSignature := base64.RawURLEncoding.EncodeToString(signature)
token := fmt.Sprintf("%s.%s", encodedPayload, encodedSignature)
```
---
### Step 4. Rendering
Our anime site also uses Go Templates (not php lol) so we generate a URL with our hostname, path, and our freshly baked token in the query parameter:
```html
<!DOCTYPE html>
<html>
<head>
<link rel="icon" type="image/png" href="https://cdn.anime4all/site/assets/favicon.png">
<title>anime4all - Avatars</title>
</head>
<body>
<div class="container-navigation">
<a href="/">Homepage</a>
<a href="/avatars">Avatars</a>
<a href="/wallpapers">Wallpapers</a>
</div>
<div class="container-avatars">
<img src="https://cdn.anime4all.net/private/avatars/miku.png?token={{.token}}" alt="Avatar of 'Hatsune Miku'">
<img src="https://cdn.anime4all.net/private/avatars/teto.png?token={{.token}}" alt="Avatar of 'Kasane Teto'">
<img src="https://cdn.anime4all.net/private/avatars/neru.png?token={{.token}}" alt="Avatar of 'Akita Neru'">
</div>
</body>
</html>
```