Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Authenticating your e-reader

inkwell-pair is a small companion service that signs a device into the auth gateway by cookie, so a new Kindle doesn't have to type a password on the device.

The flow: from an authenticated browser, generate a 6-digit code; on the new device, open /token/<code> once; the sidecar sets the session cookie and redirects to the reader. The device then behaves as authenticated for the cookie's lifetime.

The sidecar does not authenticate anyone itself. It sets the cookie that an external gateway (authelia, nginx forward-auth, Authentik, Custom) already treats as a valid session.

Routes

MethodPathEffect
GET/generate-tokenMints a 6-digit code, stores it with the configured TTL, renders it as a page.
GET/token/<code>Validates the code; on success sets the cookie and 303-redirects to PAIR_REDIRECT_URL; on failure returns 404.

/generate-token is the route the reverse proxy gates behind the auth gateway. /token/<code> is the route it leaves reachable to unauthenticated devices — that's the whole point.

The token store is in-memory. A restart drops any unredeemed codes; they're short-lived enough that regenerating is trivial.

Configuration

Every knob is an environment variable. Only PAIR_REDIRECT_URL is required.

VariableDefaultEffect
PAIR_REDIRECT_URLrequiredWhere /token/<code> redirects on success.
PAIR_PORT3000HTTP listen port.
PAIR_BIND0.0.0.0Bind interface. Use 127.0.0.1 to restrict to the local proxy.
PAIR_TOKEN_TTL_SECS300Lifetime of a freshly minted code (5 minutes).
PAIR_COOKIE_NAMEauthelia_sessionCookie name.
PAIR_COOKIE_VALUEvalidCookie value. Set to whatever the auth gateway treats as a valid session token.
PAIR_COOKIE_DOMAIN(unset)Cookie Domain. Use .example.com to cover subdomains.
PAIR_COOKIE_PATH/Cookie Path.
PAIR_COOKIE_MAX_AGE_SECS2592000Cookie Max-Age (30 days).
PAIR_COOKIE_SECUREtrueSecure flag. Disable only when running over plain HTTP inside a trusted LAN.
PAIR_COOKIE_HTTP_ONLYtrueHttpOnly flag.
PAIR_COOKIE_SAME_SITELaxSameSite value (Lax, Strict, or None).
RUST_LOGinfoTracing filter.

Docker

docker build -t inkwell-pair:latest -f pair/Dockerfile .
docker run --rm -p 3000:3000 \
  -e PAIR_REDIRECT_URL=https://inkwell.example.com/ \
  -e PAIR_COOKIE_DOMAIN=.example.com \
  inkwell-pair:latest

The -f pair/Dockerfile selects the sidecar's Dockerfile while keeping the build context at the repo root, so the builder can see the workspace Cargo.toml and Cargo.lock.

docker-compose

Run the sidecar alongside the reader:

services:
  inkwell:
    image: inkwell:latest
    restart: unless-stopped
    ports:
      - "8080:8080"
    volumes:
      - ./config.yaml:/app/config.yaml:ro
      - inkwell-data:/data

  inkwell-pair:
    image: inkwell-pair:latest
    build:
      context: .
      dockerfile: pair/Dockerfile
    restart: unless-stopped
    ports:
      - "3000:3000"
    environment:
      PAIR_REDIRECT_URL: https://inkwell.example.com/
      PAIR_COOKIE_DOMAIN: .example.com
      PAIR_COOKIE_NAME: authelia_session
      PAIR_COOKIE_VALUE: ${PAIR_COOKIE_VALUE}

volumes:
  inkwell-data:

Keep PAIR_COOKIE_VALUE out of the compose file; pass it via .env or a secret manager.

Reverse proxy

The proxy in front of both services splits behaviour by host and by path:

  • inkwell.example.com/* — gated. Every request goes through the auth gateway.
  • pair.example.com/generate-token — gated. Only an operator with a browser session should mint codes.
  • pair.example.com/token/<code> — bypassed. This is the entry point for a device without a session; gating it breaks the flow.

The examples below sketch the shape; TLS setup, header forwarding, and other proxy boilerplate are omitted. Each assumes authelia at 127.0.0.1:9091, the reader at 127.0.0.1:8080, and the sidecar at 127.0.0.1:3000.

Caddy

inkwell.example.com {
    forward_auth 127.0.0.1:9091 {
        uri /api/authz/forward-auth
    }
    reverse_proxy 127.0.0.1:8080
}

pair.example.com {
    @redeem path_regexp ^/token/[0-9]{6}$
    handle @redeem {
        reverse_proxy 127.0.0.1:3000
    }
    handle {
        forward_auth 127.0.0.1:9091 {
            uri /api/authz/forward-auth
        }
        reverse_proxy 127.0.0.1:3000
    }
}

nginx

server {
    server_name inkwell.example.com;
    auth_request /_auth;
    location / { proxy_pass http://127.0.0.1:8080; }
    location = /_auth { internal; proxy_pass http://127.0.0.1:9091/api/verify; }
}

server {
    server_name pair.example.com;
    location ~ ^/token/[0-9]{6}$ {
        proxy_pass http://127.0.0.1:3000;
    }
    location / {
        auth_request /_auth;
        proxy_pass http://127.0.0.1:3000;
    }
    location = /_auth { internal; proxy_pass http://127.0.0.1:9091/api/verify; }
}

Traefik

services:
  inkwell:
    labels:
      - "traefik.http.routers.inkwell.rule=Host(`inkwell.example.com`)"
      - "traefik.http.routers.inkwell.middlewares=authelia@docker"

  inkwell-pair:
    labels:
      # Redemption bypass — higher priority so it matches first.
      - "traefik.http.routers.pair-redeem.rule=Host(`pair.example.com`) && PathRegexp(`^/token/[0-9]{6}$`)"
      - "traefik.http.routers.pair-redeem.priority=100"
      # Everything else on pair.example.com is gated.
      - "traefik.http.routers.pair-mint.rule=Host(`pair.example.com`)"
      - "traefik.http.routers.pair-mint.middlewares=authelia@docker"

Authelia access control

# authelia.yml
access_control:
  default_policy: deny
  rules:
    - domain: inkwell.example.com
      policy: one_factor
    - domain: pair.example.com
      resources: ['^/generate-token$']
      policy: one_factor
    - domain: pair.example.com
      resources: ['^/token/[0-9]{6}$']
      policy: bypass

Set PAIR_COOKIE_NAME and PAIR_COOKIE_VALUE to the values the auth setup treats as "this device is trusted". For stock authelia, integration is more involved than a fixed cookie value can support; the sidecar is most useful with custom forward-auth shims that accept "session exists" as proof.