📦 tools-gatekeeper
A lightweight web server that protects private S3 files using token-based authentication.
Originally tested with DigitalOcean Spaces and served via Cloudflare 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 patternexpires:The UNIX Timestamp for when our token expires
Note: You can include arbitrary fields like
user_idfor 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 function internally for pattern matching. You can read it's documentation to learn more.
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.
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.
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:
<!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>