# Forgejo Deployment Specification ## 1. Scope This document specifies every Forgejo deployment in the cluster. Two repository authorities are implemented by three frontend groups. | Authority | Frontend groups | Role | | ---------------- | --------------------------------- | ------------------------------------------------------- | | Internal Forgejo | One private root | Private development, operations, and Flux GitOps origin | | Public Forgejo | Anonymous and authenticated roots | Public project development and release origin | ## 2. Authority and Topology ```mermaid flowchart LR Operators[Operators and Flux] Internet[Internet] Trusted[LAN or WireGuard] Internal[Internal frontend] Facade[Envoy anonymous facade] Code[Public anonymous frontend] Git[Public authenticated frontend] InternalState[(Internal Forgejo state)] PublicState[(Public Forgejo state)] Operators --> Internal --> InternalState Internet -->|code.quorate.dev| Facade --> Code --> PublicState Internet -->|git.quorate.dev| Git --> PublicState Trusted -->|git-ssh.quorate.internal| Git InternalState -. sanitized quorate tip .-> PublicState ``` | Relationship | Contract | | -------------------------------------- | --------------------------------------------------------- | | Internal ↔ public authority | No shared application state or runtime credential | | Public anonymous ↔ authenticated roots | Two roots share one public-forge application state | | Repository ↔ authority | Exactly one authoritative forge per repository | | Backup ↔ authority | Backups are recovery artifacts, not alternate Git remotes | ## 3. Common Deployment Contract ### 3.1 Best-Effort HA Profile Forgejo does not provide a coordinated multi-replica HA mode. The deployment uses shared state and removes replica-local workers so interchangeable frontends can serve the same authority. | Component | Durable HA contract | | ------------------------------- | ---------------------------------------------------------------------------------- | | Forgejo frontend group | Multiple replicas run on distinct eligible nodes | | Anonymous Envoy facade | Multiple replicas run on distinct eligible nodes | | Rollout | A replacement settles and becomes ready before an old replica is removed | | Voluntary disruption | Disruption policy preserves a ready replica for each group | | Insufficient placement capacity | Availability requirements take precedence over co-locating redundant replicas | | Manifest-owned tuning | Replica counts, resource requests and limits, rollout thresholds, and probe timing | | Shared state | Placement | HA purpose | | ------------------------------------------- | -------------------------------------------------------------- | ----------------------------------------------------------------------- | | Git repositories and SSH host material | One Longhorn RWX claim per authority | Every replica reads and writes the same Git namespace | | Users, permissions, and repository metadata | Dedicated database and role in CloudNativePG | No replica-local metadata authority | | Sessions, cache, and queue | Dedicated Dragonfly instance and ACL per authority | Shared sessions, cache invalidation, and queued work | | Attachments and avatars | Dedicated private VersityGW bucket and principal per authority | No replica-local blob state | | Public repository archives | Dedicated public-forge object prefix | Either public root can retrieve a generated archive | | Runtime secrets | Authority-scoped Vault projections | Every replica in one authority receives compatible application material | | Failure or rollout condition | Contract | | ------------------------------- | ------------------------------------------------------------------------------------------------ | | One frontend pod fails | Another ready replica continues serving that group | | One public frontend group stops | The other group continues its permitted operations against shared state | | Voluntary disruption | Disruption policy protects a ready replica; it does not protect against involuntary node failure | | Missing spread capacity | Redundant replicas do not collapse onto one node merely to satisfy replica count | | Rolling update | A replacement becomes settled and ready before an old replica is removed | | Shared dependency failure | All frontends using that authority may fail together | | In-flight queued operation loss | Automatic takeover is not guaranteed; a client or controller must be able to retry | The HA claim covers redundant stateless frontends. It does not claim seamless failover for every in-flight operation or remove the availability boundaries of the RWX volume, PostgreSQL, Dragonfly, VersityGW, DNS, or ingress path. ### 3.2 Security Profile | Control | Requirement | | ------------------ | ------------------------------------------------------------------------------------------ | | Process identity | Non-root user and group | | Privilege | No privilege escalation; all capabilities dropped except an explicitly required capability | | Root filesystem | Read-only | | Database transport | PostgreSQL TLS required | | Internal trust | Dedicated internal CA projection for private service TLS | | Network | Default-deny Cilium policy with explicit ingress and dependency egress | ### 3.3 Common Feature Profile | Area | Required state | | -------------------- | -------------------------------------------------------------------------------- | | Repository units | Code and releases enabled | | Disabled units | Issues, pull requests, wikis, projects, packages, Actions, and external trackers | | Repository migration | Disabled | | Repository indexer | Disabled to avoid divergent replica-local indexes | | Forgejo cron | Disabled to avoid independently scheduled duplicate work | | Forgejo Actions | Disabled to keep execution in the external workflow plane | | Native mirrors | Disabled to avoid replica-local workers and schedules | | Metrics endpoint | Disabled | Argo Workflows owns CI, release orchestration, scheduled reconciliation, and publication automation. Forgejo-native workers remain disabled to avoid duplicate execution across replicas. ## 4. Internal Authority ### 4.1 Access and Purpose | Surface | Hostname | Tier | Use | | ------- | ---------------------------- | ----- | ---------------------------------------- | | HTTPS | `forge.quorate.internal` | I | Authenticated browser and API access | | SSH | `forge-ssh.quorate.internal` | D / C | Operator Git and in-cluster Flux fetches | | Property | Contract | | -------------------- | -------------------------------------------------------------------------- | | Repository classes | Private, operational, and GitOps repositories | | Flux origin | Internal Forgejo is authoritative for the cluster configuration repository | | Anonymous access | Sign-in required | | Interactive identity | Invite-only Zitadel OIDC | | SSH clients | Private CIDRs and explicitly admitted in-cluster identities | | Internet exposure | None | | Runtime | Default container runtime; not a Tier O or Tier R application backend | ### 4.2 State Boundary | Dependency | Internal allocation | | ---------------- | --------------------------------------------------- | | PostgreSQL | Dedicated internal-forge database and role | | Dragonfly | Dedicated internal-forge instance and ACL | | Git repositories | Dedicated RWX claim | | Object storage | Dedicated private bucket and principal | | Vault | Dedicated application subtree and projected secrets | | Zitadel | Dedicated confidential OIDC application | ## 5. Public Authority ### 5.1 Repository Authority | Repository class | Sole authority | Publication contract | | ------------------------------------ | ---------------- | ---------------------------------------------------- | | Private and operational repositories | Internal Forgejo | Absent from public Forgejo | | Inherently public projects | Public Forgejo | Full public history; developed and released in place | | `quorate` | Internal Forgejo | Sanitized, history-free snapshot of the current tip | | Transition | Requirement | | ---------------------------- | ------------------------------------------------------------ | | Internal → public | Review the complete history and current tree before transfer | | Public → internal | Treat all previously public objects as irretractable | | Routine cross-forge transfer | Only the `quorate` snapshot | ```mermaid flowchart LR Tree[Approved quorate export tree] Orphan[Construct orphan commit] Scan{Secret scan clean?} Reject[Fail closed] Publish[Force-push public branch] Tree --> Orphan --> Scan Scan -->|No| Reject Scan -->|Yes| Publish ``` The published branch has a one-commit history, no internal ancestry, and no tags. Its public commit identity is not stable across exports. ### 5.2 Ingress Boundaries | Property | Anonymous root | Authenticated root | Trusted SSH | | ----------------------- | ------------------------------------------------- | ------------------------------------------------------- | -------------------------- | | Hostname | `code.quorate.dev` | `git.quorate.dev` | `git-ssh.quorate.internal` | | Tier | O | R | D | | Termination | Cloudflare TLS | Gateway R TLS | SSH transport | | Path | Cloudflare Tunnel → Envoy | OCI relay → WireGuard → Gateway R | Private LoadBalancer | | Supported use | Browse, public API reads, downloads, clone, fetch | OIDC, authenticated API, push, releases | Git SSH | | Secret-bearing workflow | None | OIDC codes, sessions, API tokens, HTTPS Git credentials | SSH keys and sessions | | Frontend | Anonymous root through Envoy only | Authenticated root | Authenticated root | | Internet publication | HTTPS | HTTPS | None | Split-horizon DNS for `git.quorate.dev` may select the private Gateway R address, but preserves the hostname, certificate identity, cookie origin, and OIDC callback. ### 5.3 Anonymous Envoy Facade | Boundary | Requirement | | ------------------- | ---------------------------------------------------------------------------------------------------------------------- | | Authorization | Default deny by normalized method and path | | Path handling | Normalize paths, merge slashes, and reject ambiguous escaped separators before policy evaluation | | Admitted operations | Only qualified anonymous browser, API, archive, release, and smart-HTTP fetch operations | | Denied operations | Login, OIDC, account, administration, notifications, repository mutation, `git-receive-pack`, and unclassified methods | | Request stripping | Credentials, cookies, proxy authentication, signature credentials, and trusted reverse-proxy identity headers | | Response stripping | Session headers and authentication challenges | | Query credentials | Rejected | | Logging | No request paths or credential values | | Network policy | Tunnel identity → facade only; facade → anonymous Forgejo HTTP only | | Bypass | No alternate anonymous route to either Forgejo frontend | The facade remains mandatory because distinct browser roots do not partition database-backed access tokens or HTTPS Git credentials shared by the public frontends. ### 5.4 Frontend and Session Isolation | Property | Anonymous root | Authenticated root | | ----------------------- | --------------------------- | --------------------------------------------------------- | | Canonical root | `https://code.quorate.dev/` | `https://git.quorate.dev/` | | Interactive login | Absent | Invite-only Zitadel OIDC | | First login | Not applicable | Automatic provisioning for an authorized Zitadel identity | | SSH listener | Disabled | Internal-only endpoint | | Session cookie | Distinct, secure, host-only | Distinct, secure, host-only | | Session-store keys | Distinct prefix | Distinct prefix | | Generated configuration | Private ephemeral volume | Private ephemeral volume | | Shared public state | Isolated per frontend | | ---------------------------------- | ------------------------------------ | | PostgreSQL data | Root URL and generated configuration | | Git repository volume | Session cookie name and store prefix | | Cache and queue | OIDC source and callbacks | | Object storage | Exposed network path | | Application cryptographic material | | Generated configuration never resides on the shared repository volume. Each root emits its own hostname in canonical links, clone URLs, API pagination, redirects, releases, attachments, and archives. ### 5.5 Public Feature Profile | Area | Required state | | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Anonymous use | Repository and organization exploration, source, commits, branches, tags, releases, downloads, archives, feeds, sitemap, public API reads, HTTPS clone, and fetch | | Maintainer use | OIDC, authenticated API, HTTPS push, trusted SSH, repository administration, and release publication | | Repository creation | Administrator-created and public by default | | Disabled creation paths | Regular-user creation, push-to-create, migration, and forks | | Search | Bounded repository-local Git search; global code search disabled | | Git and artifacts | HTTPS, source archives, and release attachments enabled; LFS and packages disabled | | API | Public and authenticated API enabled; Swagger and query-string tokens disabled | | Webhooks | Disabled unless restricted to an exact in-cluster Argo Events receiver by application and network policy | A newly enabled feature remains denied on Tier O until its routes, methods, resource bounds, and credential behavior pass the facade acceptance suite. ### 5.6 Public State and Runtime | Dependency | Public allocation | Isolation from internal Forgejo | | ---------------- | ------------------------------------------------- | ------------------------------------------------------------ | | PostgreSQL | Dedicated public-forge database and role | Database and role sharing prohibited | | Dragonfly | Dedicated public-forge instance and ACL | Instance, ACL, and credential sharing prohibited | | Git repositories | Dedicated RWX claim | Volume sharing prohibited | | Object storage | Dedicated private bucket, principal, and prefixes | Bucket, prefix, principal, and credential sharing prohibited | | Vault | Dedicated subtree and projected secrets | Runtime-secret sharing prohibited | | Kubernetes | Dedicated namespace and service account | API authority is not inherited from internal Forgejo | | Zitadel | Dedicated confidential OIDC application | Client and callback sharing prohibited | | Runtime control | Requirement | | ------------------------ | ------------------------------------------------------------------------------------- | | Namespace classification | `security.quorate.internal/public-runtime: gvisor` | | Runtime | Explicit `gvisor` RuntimeClass for Forgejo, facade, init, upgrade, and setup pods | | Admission | Reject a public backend outside a labeled namespace and any non-gVisor pod inside one | | Service accounts | No Kubernetes API token without a documented need | | Git hooks | Disabled | | Images | Reviewed immutable digests | | Public storage bootstrap | Requirement | | ------------------------ | ----------------------------------------------------------------------------------------------------------------- | | New RWX volume | Create data directories with ownership compatible with the rootless Forgejo process before either frontend starts | | Generated configuration | Keep it off the shared volume; bootstrap creates storage structure only | A public-forge compromise must not provide a data path or credential path into internal Forgejo. ## 6. Change and Verification Contract | Change or event | Required verification before acceptance | | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | Any Forgejo upgrade | Effective configuration, startup, Git read/write paths, release paths, dependency access, and replica replacement | | Internal ingress or identity change | Sign-in requirement, Zitadel login, Flux fetch, private SSH admission, and absence of Internet exposure | | Forgejo or Envoy change affecting Tier O | Complete admitted method and route inventory; clean reproducible facade suite | | Facade change | Anonymous positives; authenticated denials; credential stripping; malformed, encoded, and query bypass rejection | | Public network-policy change | Tunnel-to-facade path succeeds; direct tunnel-to-Forgejo path fails | | Public frontend change | Distinct roots, private configuration, host-only disjoint sessions, correct generated links, and cross-root visibility | | Dependency or replica restart | Affected frontends recover without divergent state | | New publication class | Repository-specific clone, fetch, archive, release, and cross-root update checks | | `quorate` publication | One orphan snapshot, no tags, no internal ancestry, and no secret-scan findings |