{"id":35444912,"url":"https://github.com/permesi/permesi","last_synced_at":"2026-03-03T10:03:15.512Z","repository":{"id":57654283,"uuid":"386687565","full_name":"permesi/permesi","owner":"permesi","description":"Identity and Access Management","archived":false,"fork":false,"pushed_at":"2026-01-22T23:13:14.000Z","size":2642,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-01-23T12:36:03.597Z","etag":null,"topics":["access-control","authentication","iam","identity-provider","oauth","passkeys","zero-trust"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/permesi.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","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},"funding":{"github":"nbari"}},"created_at":"2021-07-16T15:43:32.000Z","updated_at":"2026-01-22T23:09:12.000Z","dependencies_parsed_at":"2024-01-19T14:28:58.142Z","dependency_job_id":"2091c3c9-c6d1-48f1-97d3-ff8821b56c8a","html_url":"https://github.com/permesi/permesi","commit_stats":{"total_commits":6,"total_committers":1,"mean_commits":6.0,"dds":0.0,"last_synced_commit":"86aba027c140054e9b028bd3985f9835a76e25f9"},"previous_names":[],"tags_count":45,"template":false,"template_full_name":null,"purl":"pkg:github/permesi/permesi","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/permesi%2Fpermesi","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/permesi%2Fpermesi/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/permesi%2Fpermesi/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/permesi%2Fpermesi/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/permesi","download_url":"https://codeload.github.com/permesi/permesi/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/permesi%2Fpermesi/sbom","scorecard":{"id":728002,"data":{"date":"2025-08-11","repo":{"name":"github.com/permesi/permesi","commit":"8e400292fd5f19f01580b8d6c4389d3ad3b9c7e5"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":2.9,"checks":[{"name":"SAST","score":0,"reason":"no SAST tool detected","details":["Warn: no pull requests merged into dev branch"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Code-Review","score":0,"reason":"Found 0/30 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Warn: topLevel 'contents' permission set to 'write': .github/workflows/build.yml:11","Warn: no topLevel permission defined: .github/workflows/coverage.yml:1","Warn: topLevel 'contents' permission set to 'write': .github/workflows/deploy.yml:11","Warn: no topLevel permission defined: .github/workflows/test.yml:1","Info: no jobLevel write permissions found"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: BSD 3-Clause \"New\" or \"Revised\" License: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":0,"reason":"Project has not signed or included provenance with any releases.","details":["Warn: release artifact 0.1.6 not signed: https://api.github.com/repos/permesi/permesi/releases/187322764","Warn: release artifact 0.1.5 not signed: https://api.github.com/repos/permesi/permesi/releases/146982880","Warn: release artifact 0.1.4 not signed: https://api.github.com/repos/permesi/permesi/releases/143010613","Warn: release artifact 0.1.3 not signed: https://api.github.com/repos/permesi/permesi/releases/139760987","Warn: release artifact 0.1.2 not signed: https://api.github.com/repos/permesi/permesi/releases/137854254","Warn: release artifact 0.1.6 does not have provenance: https://api.github.com/repos/permesi/permesi/releases/187322764","Warn: release artifact 0.1.5 does not have provenance: https://api.github.com/repos/permesi/permesi/releases/146982880","Warn: release artifact 0.1.4 does not have provenance: https://api.github.com/repos/permesi/permesi/releases/143010613","Warn: release artifact 0.1.3 does not have provenance: https://api.github.com/repos/permesi/permesi/releases/139760987","Warn: release artifact 0.1.2 does not have provenance: https://api.github.com/repos/permesi/permesi/releases/137854254"],"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Packaging","score":10,"reason":"packaging workflow detected","details":["Info: Project packages its releases by way of GitHub Actions.: .github/workflows/build.yml:60"],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/build.yml:34: update your workflow using https://app.stepsecurity.io/secureworkflow/permesi/permesi/build.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/build.yml:43: update your workflow using https://app.stepsecurity.io/secureworkflow/permesi/permesi/build.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/build.yml:66: update your workflow using https://app.stepsecurity.io/secureworkflow/permesi/permesi/build.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/build.yml:69: update your workflow using https://app.stepsecurity.io/secureworkflow/permesi/permesi/build.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/build.yml:77: update your workflow using https://app.stepsecurity.io/secureworkflow/permesi/permesi/build.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/build.yml:82: update your workflow using https://app.stepsecurity.io/secureworkflow/permesi/permesi/build.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/build.yml:85: update your workflow using https://app.stepsecurity.io/secureworkflow/permesi/permesi/build.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/coverage.yml:15: update your workflow using https://app.stepsecurity.io/secureworkflow/permesi/permesi/coverage.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/coverage.yml:16: update your workflow using https://app.stepsecurity.io/secureworkflow/permesi/permesi/coverage.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/coverage.yml:36: update your workflow using https://app.stepsecurity.io/secureworkflow/permesi/permesi/coverage.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/coverage.yml:43: update your workflow using https://app.stepsecurity.io/secureworkflow/permesi/permesi/coverage.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/deploy.yml:31: update your workflow using https://app.stepsecurity.io/secureworkflow/permesi/permesi/deploy.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/deploy.yml:37: update your workflow using https://app.stepsecurity.io/secureworkflow/permesi/permesi/deploy.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/deploy.yml:67: update your workflow using https://app.stepsecurity.io/secureworkflow/permesi/permesi/deploy.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/deploy.yml:79: update your workflow using https://app.stepsecurity.io/secureworkflow/permesi/permesi/deploy.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/deploy.yml:82: update your workflow using https://app.stepsecurity.io/secureworkflow/permesi/permesi/deploy.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/deploy.yml:94: update your workflow using https://app.stepsecurity.io/secureworkflow/permesi/permesi/deploy.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/deploy.yml:97: update your workflow using https://app.stepsecurity.io/secureworkflow/permesi/permesi/deploy.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/deploy.yml:105: update your workflow using https://app.stepsecurity.io/secureworkflow/permesi/permesi/deploy.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/deploy.yml:110: update your workflow using https://app.stepsecurity.io/secureworkflow/permesi/permesi/deploy.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/deploy.yml:113: update your workflow using https://app.stepsecurity.io/secureworkflow/permesi/permesi/deploy.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/test.yml:15: update your workflow using https://app.stepsecurity.io/secureworkflow/permesi/permesi/test.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/test.yml:16: update your workflow using https://app.stepsecurity.io/secureworkflow/permesi/permesi/test.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/test.yml:25: update your workflow using https://app.stepsecurity.io/secureworkflow/permesi/permesi/test.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/test.yml:26: update your workflow using https://app.stepsecurity.io/secureworkflow/permesi/permesi/test.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/test.yml:35: update your workflow using https://app.stepsecurity.io/secureworkflow/permesi/permesi/test.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/test.yml:36: update your workflow using https://app.stepsecurity.io/secureworkflow/permesi/permesi/test.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/test.yml:55: update your workflow using https://app.stepsecurity.io/secureworkflow/permesi/permesi/test.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/test.yml:56: update your workflow using https://app.stepsecurity.io/secureworkflow/permesi/permesi/test.yml/main?enable=pin","Warn: containerImage not pinned by hash: Dockerfile:1","Warn: containerImage not pinned by hash: Dockerfile:15: pin your Docker image by updating alpine:latest to alpine:latest@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1","Info:   0 out of  10 GitHub-owned GitHubAction dependencies pinned","Info:   0 out of  19 third-party GitHubAction dependencies pinned","Info:   0 out of   2 containerImage dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"Branch-Protection","score":-1,"reason":"internal error: error during branchesHandler.setup: internal error: githubv4.Query: Resource not accessible by integration","details":null,"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"Vulnerabilities","score":0,"reason":"11 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: RUSTSEC-2025-0004 / GHSA-rpmj-rpgj-qmpm","Warn: Project is vulnerable to: GHSA-4fcv-w3qc-ppgg","Warn: Project is vulnerable to: RUSTSEC-2025-0022","Warn: Project is vulnerable to: RUSTSEC-2024-0436","Warn: Project is vulnerable to: GHSA-4p46-pwfr-66x6","Warn: Project is vulnerable to: RUSTSEC-2025-0009","Warn: Project is vulnerable to: GHSA-c86p-w88r-qvqr","Warn: Project is vulnerable to: RUSTSEC-2023-0071 / GHSA-4grx-2x9w-596c / GHSA-c38w-74pg-36hr","Warn: Project is vulnerable to: GHSA-rr8g-9fpq-6wmg","Warn: Project is vulnerable to: RUSTSEC-2025-0023","Warn: Project is vulnerable to: GHSA-94vh-gphv-8pm8"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-22T13:27:42.003Z","repository_id":57654283,"created_at":"2025-08-22T13:27:42.003Z","updated_at":"2025-08-22T13:27:42.003Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28853196,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-28T15:15:36.453Z","status":"ssl_error","status_checked_at":"2026-01-28T15:15:13.020Z","response_time":57,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"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":["access-control","authentication","iam","identity-provider","oauth","passkeys","zero-trust"],"created_at":"2026-01-03T01:19:33.668Z","updated_at":"2026-02-08T19:16:07.272Z","avatar_url":"https://github.com/permesi.png","language":"Rust","funding_links":["https://github.com/sponsors/nbari"],"categories":[],"sub_categories":[],"readme":"# permesi\n\n**permesi** Identity and Access Management\n\n[![Test \u0026 Build](https://github.com/permesi/permesi/actions/workflows/build.yml/badge.svg)](https://github.com/permesi/permesi/actions/workflows/build.yml)\n[![codecov](https://codecov.io/gh/permesi/permesi/graph/badge.svg?token=ODC4S2YHPF)](https://codecov.io/gh/permesi/permesi)\n[![API Docs](https://img.shields.io/badge/API-Docs-blue)](https://permesi.github.io/permesi/)\n\n\u003cimg src=\"permesi.svg\" height=\"400\"\u003e\n\n## ⚡ Quick Start (Full Local Stack)\n\n**Prerequisites (intentional):**\n- **System**: `zsh`, `podman`, `tmux`, `jq`, `curl`, `xh`, `ripgrep` (rg)\n- **Languages**: `rust` (stable), `node` (LTS)\n- **Infrastructure**: `just`, `terraform`, `vault`, `mkcert`, `direnv`\n\u003e These tools are required to run a real IAM stack locally: isolated services, TLS everywhere, Vault-backed cryptography, and reproducible infrastructure.\n\n```bash\n# 1. Clone the repo\ngit clone https://github.com/permesi/permesi.git\ncd permesi\n\n# 2. Allow listening on privileged ports (Linux only, for HAProxy on :443)\njust haproxy-sysctl\n\n# 3. Ignite the engine, This will open a tmux session with all services running in panes.\njust start\n\n# 4. (Optional) run firefox in deveper mode:\njust firefox\n```\n\n[![asciicast](https://asciinema.org/a/782038.svg)](https://asciinema.org/a/782038)\n\n*`just start` launches the full infrastructure (Postgres, Vault, Jaeger,\nHAProxy) and starts the services (`genesis`, `permesi`, and the `web` console)\nin a `tmux` session using Unix domain sockets for backend communication. If\n`tmux` is not installed, it will start the infra and you can run the services\nmanually.*\n\n**Verify the stack is healthy:**\n- **Web Console:** [https://permesi.localhost](https://permesi.localhost)\n- **API (Permesi):** [https://api.permesi.localhost/health](https://api.permesi.localhost/health)\n- **API (Genesis):** [https://genesis.permesi.localhost/health](https://genesis.permesi.localhost/health)\n- **Tracing (Jaeger):** [http://localhost:16686](http://localhost:16686)\n\n## Workspace Layout\n\nThis repository is a Rust workspace (monorepo) containing:\n\n- `services/permesi`: core IAM / OIDC authority\n- `services/genesis`: edge admission token mint\n- `crates/admission_token`: shared admission token contract + sign/verify helpers\n- `apps/web`: CSR-only Leptos admin console (Trunk + Tailwind, static `dist/`)\n\nNote: service HTTP modules live under `src/api/` (previously `src/permesi/` and `src/genesis/`).\n\n## Architecture\n\npermesi employs a **Split-Trust Architecture** to separate network noise from core identity logic.\n\n### The Components\n\n#### 1. `genesis` (The Edge / \"The Bouncer\")\n* **Role:** Public-facing edge service.\n* **Responsibility:** Handles raw TCP/HTTP connections, enforces strict rate limits, performs PoW (Proof of Work) challenges for abuse prevention, and sanitizes inputs.\n* **Output:** Issues a short-lived, cryptographically signed **Admission Token**.\n* **State:** Stateless / Ephemeral.\n* **Key Publication:** Publishes a PASERK keyset at `GET /paserk.json`.\n\n#### 2. `permesi` (The Core / \"The Authority\")\n* **Role:** The OIDC Authority.\n* **Responsibility:** OPAQUE signup/login, email verification, and OIDC flows.\n* **Trust Model:** Verifies **Admission Tokens** from `genesis` *offline* (signature + `exp` + `aud` + `iss`) without calling `genesis` during normal request handling. Validates short-lived **Zero Tokens** offline using the PASERK keyset for auth POSTs.\n* **Output:** Issues standard OIDC Access/ID Tokens (JWTs).\n\n#### 3. Database\n* **Role:** System of Record.\n* **Usage:** Stores user records (OPAQUE registration records), email verification tokens/outbox, plus **Audit Logs** and **Revocation Lists**. It is **not** required for the hot-path verification of Admission Tokens, ensuring high availability even during DB latency spikes.\n\n---\n\n## Production bootstrap (no containers)\n\nTo bootstrap Postgres without the local container flow, run the SQL directly. `db/sql/` is the\nsingle source of truth for dev containers and bare-metal setups:\n\n```sh\n# 1) Create Vault root users, runtime roles, grants, and load schemas (edit passwords first).\npsql \"postgres://\u003cadmin\u003e@\u003chost\u003e:5432/postgres\" -v ON_ERROR_STOP=1 -f db/sql/00_init.sql\n```\n\n`db/sql/00_init.sql` uses dev defaults (`vault_genesis` / `vault_permesi` with the same password).\nFor production, update those passwords and remove the `seed_test_client.sql` include before\nrunning it, or use it as a template for your own bootstrap script. If you choose not to run\n`db/sql/00_init.sql`, load the service schemas directly with `db/sql/01_genesis.sql` and\n`db/sql/02_permesi.sql`.\n\nFor scheduled maintenance, `db/sql/cron_jobs.sql` is the only place where pg_cron jobs are\nregistered (run it against the `postgres` database). Application schemas never install or\nschedule pg_cron jobs directly.\n\n## Cryptography\n\n- **Admission tokens:** PASETO v4.public (Ed25519). `genesis` signs via Vault Transit; private keys never leave Vault. Public keys are published via a PASERK keyset for offline verification.\n- **permesi encryption:** Vault Transit key type `chacha20-poly1305` (default `transit/permesi` / key `users`) for encrypt/decrypt operations.\n- **OPAQUE (user auth):** Client-side OPAQUE; server stores only the registration record. The server setup seed is stored in Vault KV v2 (`opaque_server_seed`).\n\n## Admission Token Verification (Offline)\n\nAdmission token verification never calls `genesis` on the hot path. The flow is:\n\n1. `genesis` signs a PASETO v4.public token with Vault Transit and puts the PASERK ID (`k4.pid...`) in the token footer as `kid`.\n2. `permesi` parses the footer `kid`, looks up the matching `k4.public...` key in the PASERK keyset, and verifies the signature.\n3. `permesi` validates claims (`iss`, `aud`, `action`, `iat/exp`, TTL). If any check fails, the request is rejected.\n\nKeyset behavior:\n\n- `active_kid` is only used by `genesis` to choose the signing key. Verification always uses the token's footer `kid`.\n- When configured with a PASERK URL, `permesi` caches `/paserk.json` (default TTL 5 minutes) and refreshes it on unknown `kid` with a cooldown. No per-request calls are made.\n- When configured with a local file or JSON string, verification is fully offline (no network fetches).\n\nMissing / planned:\n- Optional revocation mode (DB lookup or cached revocation list). There is no public token introspection endpoint.\n\n## Tenant model (prototype)\n\nOrganizations are the tenant boundary in permesi. Each organization owns projects, projects own\nenvironments, and environments own applications. Org-scoped membership and roles are the source\nof authorization for tenant resources, and environment tiers enforce a single production\nenvironment per project with non-production blocked until production exists.\n\nMore details and the creation flow live in `services/permesi/README.md` under “Organization\nendpoints and authorization”.\n\n## Trust Boundaries\n\n```mermaid\nflowchart LR\n  subgraph Internet[\"Untrusted: Internet\"]\n    U[User / Client]\n  end\n\n  subgraph Edge[\"Trust Boundary: Edge\"]\n    G[\"genesis\u003cbr/\u003eedge admission token mint\"]\n    PASERK[(\"PASERK\u003cbr/\u003eGET /paserk.json\")]\n  end\n\n  subgraph Core[\"Trust Boundary: Core IAM\"]\n    P[\"permesi\u003cbr/\u003ecore IAM / OIDC authority\"]\n  end\n\n  subgraph Data[\"Optional: Data Plane\"]\n    DB[(\"Audit / Revocation DB\")]\n  end\n\n  U --\u003e|1. Request admission| G\n  G --\u003e|\"2. Signed Admission Token (PASETO)\"| U\n\n  G --\u003e|Publishes public keys| PASERK\n  P --\u003e|Loads PASERK keyset at deploy/startup| PASERK\n\n  U --\u003e|3. Credentials + Admission Token| P\n  P --\u003e|\"4. Offline verify: sig + exp + aud + iss\"| P\n\n  G -.-\u003e|\"Optional audit write (jti)\"| DB\n  P -.-\u003e|\"Optional revocation check (jti)\"| DB\n```\n\n## User Authentication (OPAQUE + Zero Token)\n\nAll auth POSTs require a Genesis zero token (validated offline using the PASERK keyset).\n\n```mermaid\nsequenceDiagram\n    participant U as User / Client\n    participant G as Genesis (Edge)\n    participant P as Permesi (Core)\n    participant DB as Postgres\n\n    Note over U, G: Zero token mint\n    U-\u003e\u003eG: Request zero token\n    G--\u003e\u003eU: Zero token\n\n    Note over U, P: OPAQUE login\n    U-\u003e\u003eP: /v1/auth/opaque/login/start + zero token\n    P-\u003e\u003eP: Verify token (PASERK keyset)\n    P--\u003e\u003eU: credential_response + login_id\n\n    U-\u003e\u003eP: /v1/auth/opaque/login/finish + zero token\n    P-\u003e\u003eP: Verify token (PASERK keyset)\n    P-\u003e\u003eP: OPAQUE finish (no password sent)\n    P-\u003e\u003eDB: Persist session\n    P--\u003e\u003eU: 204 + Set-Cookie (session)\n\n    Note over U, P: Session hydration\n    U-\u003e\u003eP: /v1/auth/session (cookie)\n    P-\u003e\u003eDB: Load session\n    P--\u003e\u003eU: 200 session or 204\n```\n\n Signup uses `/v1/auth/opaque/signup/start` + `/finish` and email verification uses `/v1/auth/verify-email` + `/v1/auth/resend-verification` (all require zero tokens).\n\n## Passkey + MFA Login Flow\n\nUsers sign up with email and password, then can register passkeys from `/console/me/security` once they are logged in. The login page prioritizes passwordless flows; users enter their email and can sign in with a passkey, or expand the password form if they want to use OPAQUE.\n\nMFA enforcement is consistent across login paths. If TOTP is enabled for the account, the login flow always proceeds to the MFA challenge after either password or passkey authentication succeeds.\n\n```mermaid\nflowchart TD\n  Signup[Signup: email + password] --\u003e Verify[Email verification]\n  Verify --\u003e Console[\"/console/me/security\"]\n  Console --\u003e AddPasskey[Register passkey]\n\n  Login[Login: enter email] --\u003e Passkey{Use passkey?}\n  Passkey --\u003e|Yes| PasskeyAuth[Passkey auth]\n  Passkey --\u003e|No| ShowPassword[Show password fields]\n  ShowPassword --\u003e Opaque[OPAQUE password login]\n\n  PasskeyAuth --\u003e Session[Session issued]\n  Opaque --\u003e Session\n  Session --\u003e MFA{TOTP enabled?}\n  MFA --\u003e|Yes| Challenge[MFA challenge]\n  MFA --\u003e|No| Success[Signed in]\n  Challenge --\u003e Success\n```\n\n### Admin Rate Limiting\nAdministrative endpoints (bootstrap and elevation) are strictly rate-limited to 3 attempts per 10 minutes per user to protect against Vault token brute-forcing. Consecutive failures trigger a 15-minute cooldown.\n\n### Auth endpoints (quick scan)\n| Method | Path | Notes |\n|---|---|---|\n| `POST` | `/v1/auth/opaque/signup/start` | OPAQUE registration start; requires zero token |\n| `POST` | `/v1/auth/opaque/signup/finish` | OPAQUE registration finish; requires zero token |\n| `POST` | `/v1/auth/opaque/login/start` | OPAQUE login start; requires zero token |\n| `POST` | `/v1/auth/opaque/login/finish` | OPAQUE login finish; requires zero token |\n| `POST` | `/v1/auth/verify-email` | Consume email verification token; requires zero token |\n| `POST` | `/v1/auth/resend-verification` | Resend verification link; requires zero token |\n\n## Vault Dependency\n\nVault is required for both services in production (AppRole auth or Agent proxy, dynamic DB creds, transit encryption, and the OPAQUE seed in KV v2). Running without Vault is not supported.\n\n### Connectivity Modes\n\nThe `vault-url` (and its env equivalents) supports two operational modes:\n\n1.  **TCP Mode (`http://` or `https://`)**:\n    -   Requires `vault-role-id` and `vault-secret-id` (or `vault-wrapped-token`).\n    -   The application performs the AppRole login and manages background token/lease renewals.\n2.  **Agent Mode (`/path/to/socket` or `unix:///path/to/socket`)**:\n    -   Connects to a Vault Agent `api_proxy` via a Unix domain socket.\n    -   No role/secret IDs are required.\n    -   The application delegates authentication and renewals to the Agent.\n    -   **Requirement**: Vault Agent must be configured with `use_auto_auth_token = true`.\n\nProduction readiness checklist:\n- HA cluster with tested failover.\n- Automated unseal or a documented unseal runbook.\n- Backups plus restore drills (e.g., raft snapshots or storage backups).\n- Monitoring and alerts for health, sealed state, and token/lease renew failures.\n\n## Build\n\n- `cargo build -p permesi`\n- `cargo build -p genesis`\n- **Terraform** (v1.5+): Required for provisioning local Vault infrastructure.\n\n## Web Console\n\n- `just web`: Tailwind build/watch + Trunk dev server.\n- `just web-build`: production build (`apps/web/dist`).\n- Node.js is only required for CSS tooling; the output is fully static.\n- Frontend env is compile-time (via `option_env!`). Set `PERMESI_API_BASE_URL`, `PERMESI_TOKEN_BASE_URL`, and `PERMESI_CLIENT_ID` before build.\n- `PERMESI_CLIENT_ID` is public (embedded in WASM); store it in GitHub Actions Variables, not Secrets.\n\n## Local Development (Full Flow)\n\nDefault ports: genesis `8000`, permesi `8001`, web `8080`.\n\nLocal HTTPS is the default for development. HAProxy terminates TLS for `permesi.localhost`, `api.permesi.localhost`, and `genesis.permesi.localhost` using a mkcert-issued certificate, then forwards to the services over TLS using Vault-issued certificates. `just start` launches HAProxy with TLS termination on port `443`. The Trunk dev server runs on `8081` behind HAProxy and binds to `0.0.0.0` for container access.\nIf HAProxy can't reach host services on macOS, it falls back to `host.docker.internal` automatically.\n\nIf you want to run services manually instead of using the all-in-one `just start` (socket mode):\n1) Run services: `just genesis-socket` and `just permesi-socket` (or `just start-http` for the TCP flow). They auto-source `.envrc`, so direnv is optional.\n\n`just start` uses tmux when available to start a `permesi` session with genesis + permesi + web panes, plus a fourth pane for ad hoc commands.\nIf you're already inside tmux, it creates the `permesi` session in the background and prints attach instructions.\nRe-running attaches to the existing session when not inside tmux; stop with `tmux kill-session -t permesi`.\n\nBecause AppRole SecretIDs are single-use (`secret_id_num_uses=1`), `just genesis` and `just permesi` fetch a fresh\nSecretID before each `cargo watch` run using the Vault CLI. Make sure `vault` is installed and authenticated (via\n`VAULT_ADDR`/`VAULT_TOKEN` or your Vault token helper).\n\nIf you want infra only: `just dev-start-infra` then `just dev-envrc` (this also runs `direnv allow` if available).\nIf Postgres init scripts didn't run (for example, an existing `db/data`), run `just db-bootstrap`\nto (re)apply schemas and runtime roles, then `just db-verify` to confirm constraints.\n\nCleanup: `just stop` to stop containers, and `just reset` to remove the infra containers, wipe Vault data, and delete local Postgres data/logs (`db/data`, `db/logs`).\n\n`just dev-envrc` emits Vault credentials plus local endpoints. In local dev, both services use TLS certificates issued by a single Vault PKI CA. `PERMESI_ADMISSION_PASERK_CA_PATH` should point at the Genesis Vault CA bundle when fetching `paserk.json` directly from the Genesis service.\n- `PERMESI_TLS_PEM_BUNDLE=.../certs/permesi/tls.bundle.pem`\n- `PERMESI_ADMISSION_PASERK_CA_PATH=.../certs/genesis/ca.pem`\n- `GENESIS_TLS_PEM_BUNDLE=.../certs/genesis/tls.bundle.pem`\n- `PERMESI_ADMISSION_PASERK_URL=https://genesis.permesi.localhost:8000/paserk.json`\n- `PERMESI_FRONTEND_BASE_URL=https://permesi.localhost`\n- `PERMESI_API_BASE_URL=https://api.permesi.localhost`\n- `PERMESI_TOKEN_BASE_URL=https://genesis.permesi.localhost`\n- `PERMESI_PASSKEYS_ALLOWED_ORIGINS=https://permesi.localhost`\n- `PERMESI_OPERATOR_TOKEN` (used for `/admin/claim`)\n\n### Useful Development Recipes\n\n- `just signup-verify-url`: Extract the latest email verification link from the database.\n- `just operator-token`: Generate a fresh platform operator token for admin claim/elevation.\n- `just db-verify`: Confirm database constraints and schema state.\n- `just openapi`: Regenerate OpenAPI specs from code.\n\nPasskey registration is available in preview mode by default and does not persist credentials without additional storage.\n Configure the relying party and origin validation via `PERMESI_PASSKEYS_RP_ID`, `PERMESI_PASSKEYS_RP_NAME`, and `PERMESI_PASSKEYS_ALLOWED_ORIGINS`, adjust challenge TTL with `PERMESI_PASSKEYS_CHALLENGE_TTL_SECONDS`, and toggle preview behavior with `PERMESI_PASSKEYS_PREVIEW_MODE`. Persisting passkeys would require a dedicated table to store `credential_id` (bytes), `user_id`, `public_key` (serialized passkey), `sign_count`, `transports`, `created_at`, and `last_used_at` (nullable).\n\n### Local HTTPS for Passkeys (mkcert + HAProxy)\n\nPasskeys require HTTPS. The repo includes a local HAProxy config at `config/haproxy/haproxy.cfg` that terminates TLS and routes `permesi.localhost`, `api.permesi.localhost`, and `genesis.permesi.localhost` to the usual local ports. Generate a local certificate with mkcert, combine it for HAProxy, then run the container:\n\n```\njust mkcert-local\n\njust haproxy-start\n\n```\n\nManual steps (if you prefer to run them directly):\n\n```\nmkcert -install\nmkcert -key-file config/haproxy/certs/permesi.localhost-key.pem \\\n  -cert-file config/haproxy/certs/permesi.localhost-cert.pem \\\n  \"localhost\" \"127.0.0.1\" \"::1\" \"*.localhost\" \\\n  \"permesi.localhost\" \"api.permesi.localhost\" \"genesis.permesi.localhost\" \"*.permesi.localhost\"\ncat config/haproxy/certs/permesi.localhost-cert.pem config/haproxy/certs/permesi.localhost-key.pem \\\n  \u003e config/haproxy/certs/permesi.localhost.pem\n\npodman run -d --name permesi-haproxy \\\n  --add-host=host.containers.internal:host-gateway \\\n  -p 443:8080 \\\n  -v \"$(pwd)/config/haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro\" \\\n  -v \"$(pwd)/config/haproxy/certs:/usr/local/etc/haproxy/certs:ro\" \\\n  docker.io/haproxy:latest\n```\n\nOn Linux, binding to `:443` may require allowing unprivileged ports: `sudo sysctl -w net.ipv4.ip_unprivileged_port_start=443` (persist with a sysctl.d config if desired). If you want to avoid IPv6 resolution issues, add IPv4 host entries via `just localhost-hosts`.\nYou can run `just haproxy-sysctl` to apply the sysctl setting.\n\n### Testing Admin Claim (Platform Operator)\nTo test bootstrapping the first admin or elevating privileges, you need a Vault token with the `permesi-operators` policy.\nThe dev bootstrap automatically generates one and prints it to stdout (or exports it via `just dev-envrc`).\n\n1. Copy the **Operator Token** from startup logs or run `echo $PERMESI_OPERATOR_TOKEN`.\n2. Navigate to `https://permesi.localhost/admin/claim`.\n3. Paste the token and submit to claim the operator role.\n\n## API Contract (OpenAPI)\n\nThis repo treats the OpenAPI specs as versioned artifacts, checked in under:\n\n- `docs/openapi/permesi.json`\n- `docs/openapi/genesis.json`\n\nRegenerate them from code:\n\n- `cargo run -p permesi --bin permesi-openapi \u003e docs/openapi/permesi.json`\n- `cargo run -p genesis --bin genesis-openapi \u003e docs/openapi/genesis.json`\n\n## Containers\n\n- `podman build -f services/permesi/Dockerfile -t permesi:dev .`\n- `podman build -f services/genesis/Dockerfile -t genesis:dev .`\n\n## Local Tracing (Jaeger)\n\nSend OTLP traces directly to the local Jaeger collector:\n\n```sh\nexport OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317\n```\n\nOpen the Jaeger UI at http://localhost:16686 to inspect traces.\n\n## 🤝 Contributing\n\nWe welcome contributions of all kinds!\n\n1.  **Read the [Agent \u0026 Contributor Contract](AGENTS.md)**: It contains mandatory guidelines on code style, security invariants, and module organization.\n2.  **Pick an issue**: Check the [TODO.md](TODO.md) or open issues.\n3.  **Run tests**: `just test` covers the full workspace.\n4.  **Linting**: We use strict Clippy rules. Run `just clippy` before submitting.\n\n*Note: This project uses a \"Reference Quality\" approach. We prefer small, well-documented, and secure diffs over large refactors.*\n\n## CI Commands\n\n- `cargo fmt --all -- --check`\n- `cargo clippy --all-targets --all-features`\n- `cargo test --workspace`\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpermesi%2Fpermesi","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpermesi%2Fpermesi","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpermesi%2Fpermesi/lists"}