# 📦 `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 anime4all - Avatars
Homepage Avatars Wallpapers
Avatar of 'Hatsune Miku' Avatar of 'Kasane Teto' Avatar of 'Akita Neru'
```