2026-05-24 19:40:12 -07:00
2026-05-24 19:40:12 -07:00
2026-05-24 19:40:12 -07:00
2026-05-24 19:40:12 -07:00
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 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 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 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>
S
Description
🔒 Expose private S3 folders using short-lived tokens
Readme 34 KiB
Languages
Go 96.4%
Dockerfile 3.6%