{"id":50902091,"url":"https://github.com/michidk/hodor","last_synced_at":"2026-06-16T03:32:25.760Z","repository":{"id":349510130,"uuid":"1202634230","full_name":"michidk/hodor","owner":"michidk","description":"A tiny reverse proxy that gates any web app behind a single shared password. No users, no database, no OAuth — just one binary, one password, one login page.","archived":false,"fork":false,"pushed_at":"2026-06-08T14:56:35.000Z","size":135,"stargazers_count":5,"open_issues_count":2,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-08T16:28:16.384Z","etag":null,"topics":["auth","authentication","docker","gateway","guard","kubernetes","login","password-protection","reverse-proxy","rust","sidecar"],"latest_commit_sha":null,"homepage":"https://github.com/michidk/hodor","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/michidk.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-04-06T08:30:19.000Z","updated_at":"2026-05-16T21:17:49.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/michidk/hodor","commit_stats":null,"previous_names":["michidk/hodor"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/michidk/hodor","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/michidk%2Fhodor","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/michidk%2Fhodor/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/michidk%2Fhodor/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/michidk%2Fhodor/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/michidk","download_url":"https://codeload.github.com/michidk/hodor/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/michidk%2Fhodor/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34390052,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-16T02:00:06.860Z","response_time":126,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["auth","authentication","docker","gateway","guard","kubernetes","login","password-protection","reverse-proxy","rust","sidecar"],"created_at":"2026-06-16T03:32:24.063Z","updated_at":"2026-06-16T03:32:25.753Z","avatar_url":"https://github.com/michidk.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# hodor\n\nA tiny reverse proxy that holds the door — put it in front of any app to gate access behind a single shared password. No users, no database, no OAuth. Just one password and a login page.\n\n## Features\n\n- Single shared password — no user accounts, no database\n- Clean dark-themed login page (or bring your own with Jinja2 templates)\n- Runs as a Docker sidecar in front of any web app\n- HMAC-SHA256 signed session cookies\n- Streaming reverse proxy (handles large uploads/downloads without buffering)\n- Constant-time password comparison\n- Per-IP rate limiting on login (5 attempts / 60s)\n- Structured tracing output (compact or JSON)\n- Health check endpoint for container orchestrators\n- Graceful shutdown on SIGTERM\n- Layered config: defaults → `hodor.toml` → environment variables\n- Built with Rust, runs from a `scratch` image (~5MB)\n\n## Quick Start\n\n```yaml\n# docker-compose.yml\nservices:\n  gate:\n    image: ghcr.io/michidk/hodor:latest\n    ports:\n      - \"8080:8080\"\n    environment:\n      PASSWORD: \"changeme\"                          # the login password\n      UPSTREAM: \"http://app:80\"\n      SECRET: \"changeme\"                              # signs session cookies (generate with: openssl rand -hex 32)\n    depends_on:\n      - app\n\n  app:\n    image: traefik/whoami\n```\n\n```sh\ndocker compose up\n```\n\nOpen `http://localhost:8080` — you'll see the login page. Enter the password, and you're proxied through to the app.\n\n![Screenshot of the hodor login page](.github/images/screenshot.png)\n\n## Configuration\n\nHodor uses layered configuration. Each layer overrides the previous:\n\n1. **Defaults** — sensible built-in values\n2. **`hodor.toml`** — optional config file in the working directory\n3. **Environment variables** — override everything (uppercase, e.g. `PASSWORD`)\n\n### Options\n\n| Key | Env var | Required | Default | Description |\n| --- | --- | --- | --- | --- |\n| `password` | `PASSWORD` | yes | | The shared password |\n| `upstream` | `UPSTREAM` | yes | | Backend URL to proxy to (e.g. `http://app:3000`) |\n| `secret` | `SECRET` | no | random | Cookie signing key. Set this to persist sessions across restarts |\n| `listen` | `LISTEN` | no | `:8080` | Listen address |\n| `title` | `TITLE` | no | `Password Required` | Login page heading |\n| `template` | `TEMPLATE` | no | built-in | Path to a custom HTML login page template |\n| `error_template` | `ERROR_TEMPLATE` | no | built-in | Path to a custom HTML error page template |\n| `session_ttl` | `SESSION_TTL` | no | `86400` | Session duration in seconds (default: 24h) |\n| `secure_cookie` | `SECURE_COOKIE` | no | `false` | Set `true` to add the `Secure` flag to cookies (requires HTTPS) |\n| `log_format` | `LOG_FORMAT` | no | `compact` | Tracing output format: `compact` or `json` |\n| — | `RUST_LOG` | no | `info` | Log level filter (e.g. `debug`, `hodor=trace`) |\n\n### Config File Example\n\n```toml\n# hodor.toml\npassword = \"changeme\"\nupstream = \"http://app:3000\"\nsecret = \"changeme\" # generate with: openssl rand -hex 32\ntitle = \"Restricted Area\"\nsession_ttl = 3600\nsecure_cookie = true\n```\n\nEnvironment variables always win. Set `PASSWORD=override` and it takes precedence over `password` in the TOML file.\n\n## How It Works\n\n```\nRequest → hodor\n  ├─ /_gate/health → 200 ok (bypass auth)\n  ├─ Has valid session cookie? → Reverse proxy to UPSTREAM\n  └─ No cookie? → Show login page\n       └─ POST /_gate/login\n            ├─ Rate limited? → 429\n            ├─ Password correct? → Set cookie, redirect back\n            └─ Wrong? → Show login page with error\n```\n\n### Reserved Paths\n\n- `/_gate/login` — login form submission (POST) / redirect to gate (GET)\n- `/_gate/logout` — clears session cookie\n- `/_gate/health` — returns `ok` (for liveness/readiness probes)\n\nAll other paths are proxied to the upstream.\n\n### Proxy Behavior\n\n- Streams request and response bodies without buffering (safe for large files)\n- Sets `X-Forwarded-For` and `X-Forwarded-Proto` headers on proxied requests\n- Strips hop-by-hop headers (Connection, Transfer-Encoding, etc.)\n- Forwards the upstream's `Host` header\n- WebSocket proxying is not yet supported (returns 501)\n\n## Custom Login Page\n\nHodor ships with a built-in dark-themed login page. To use your own login page, set `template` to the path of an HTML file:\n\n```yaml\nenvironment:\n  TEMPLATE: /etc/hodor/login.html\nvolumes:\n  - ./my-login.html:/etc/hodor/login.html:ro\n```\n\nTemplates use [Jinja2 syntax](https://jinja.palletsprojects.com/) (via [minijinja](https://github.com/mitsuhiko/minijinja)). The following variables are available:\n\n| Variable | Type | Description |\n| --- | --- | --- |\n| `title` | string | The configured title (auto-escaped) |\n| `show_error` | bool | `true` when the user entered a wrong password |\n\n### Template Example\n\nThe built-in template ([`src/template.html`](src/template.html)) is a good starting point for custom designs. Here's a minimal example showing the required structure:\n\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"en\"\u003e\n\u003chead\u003e\n  \u003cmeta charset=\"utf-8\"\u003e\n  \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1\"\u003e\n  \u003ctitle\u003e{{ title }}\u003c/title\u003e\n  \u003cstyle\u003e\n    * { box-sizing: border-box; margin: 0; }\n    body {\n      min-height: 100vh;\n      display: grid;\n      place-items: center;\n      padding: 24px;\n      font-family: system-ui, sans-serif;\n      background: #f5f5f5;\n    }\n    .card {\n      width: 100%;\n      max-width: 380px;\n      background: #fff;\n      border-radius: 12px;\n      padding: 32px;\n      box-shadow: 0 4px 24px rgba(0, 0, 0, 0.1);\n    }\n    h1 { margin-bottom: 20px; font-size: 1.4rem; }\n    input, button {\n      width: 100%;\n      padding: 10px 14px;\n      border: 1px solid #ddd;\n      border-radius: 8px;\n      font: inherit;\n    }\n    input { margin-bottom: 12px; }\n    button { background: #111; color: #fff; border: none; cursor: pointer; }\n    .error {\n      display: {% if show_error %}block{% else %}none{% endif %};\n      margin-bottom: 12px;\n      padding: 10px;\n      border-radius: 8px;\n      background: #fef2f2;\n      color: #dc2626;\n    }\n  \u003c/style\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n  \u003cmain class=\"card\"\u003e\n    \u003ch1\u003e{{ title }}\u003c/h1\u003e\n    \u003cdiv class=\"error\"\u003eWrong password.\u003c/div\u003e\n    \u003cform method=\"post\" action=\"/_gate/login\"\u003e\n      \u003cinput type=\"hidden\" name=\"redirect\" value=\"/\"\u003e\n      \u003cinput name=\"password\" type=\"password\" placeholder=\"Password\" autocomplete=\"current-password\" autofocus required\u003e\n      \u003cbutton type=\"submit\"\u003eContinue\u003c/button\u003e\n    \u003c/form\u003e\n  \u003c/main\u003e\n  \u003cscript\u003e\n    const redirect = document.querySelector('input[name=\"redirect\"]');\n    if (redirect) redirect.value = window.location.pathname + window.location.search + window.location.hash || '/';\n  \u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n```\n\n### Template Requirements\n\n1. The form **must** POST to `/_gate/login` with a `password` field\n2. Include a `redirect` hidden field (populated via JS) so users return to the page they were trying to access\n3. Use `{% if show_error %}` to conditionally show error messages\n\n## Custom Error Page\n\nHodor also ships with a built-in styled error page for upstream failures and unsupported WebSocket upgrades. To customize it, set `error_template` to the path of an HTML file:\n\n```yaml\nenvironment:\n  ERROR_TEMPLATE: /etc/hodor/error.html\nvolumes:\n  - ./my-error.html:/etc/hodor/error.html:ro\n```\n\nThe built-in error template ([`src/error_template.html`](src/error_template.html)) receives these variables:\n\n| Variable | Type | Description |\n| --- | --- | --- |\n| `title` | string | The configured title (auto-escaped) |\n| `status_code` | number | HTTP status code such as `502` or `501` |\n| `heading` | string | Short error heading |\n| `message` | string | Human-readable error message |\n\n## Building from Source\n\n```sh\ncargo build --release\n```\n\n```sh\nPASSWORD=secret UPSTREAM=http://localhost:3000 ./target/release/hodor\n```\n\n## Docker\n\nBuild locally:\n\n```sh\ndocker build -t hodor .\ndocker run -e PASSWORD=secret -e UPSTREAM=http://host.docker.internal:3000 -p 8080:8080 hodor\n```\n\n### Health Checks\n\nHodor exposes `/_gate/health` which returns `200 ok` — use it for liveness and readiness probes.\n\nSince hodor runs from a `scratch` image, there's no shell or utilities inside the container. Use an external probe or your orchestrator's native HTTP health check:\n\n```yaml\n# Kubernetes\nlivenessProbe:\n  httpGet:\n    path: /_gate/health\n    port: 8080\n  initialDelaySeconds: 2\n  periodSeconds: 10\n```\n\n## License\n\n[MIT](LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmichidk%2Fhodor","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmichidk%2Fhodor","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmichidk%2Fhodor/lists"}