Security policy
Reporting a vulnerability
Email security@shithub.sh. PGP-encrypt the report
using the key fingerprint published at
https://shithub.sh/.well-known/pgp-key.asc if your finding
is sensitive.
The mailbox auto-acknowledges receipt within minutes. A human response (initial assessment + next steps) follows within 72 hours.
Please do not file public issues for security findings. Coordinated disclosure is the norm; we will credit you in the hall of fame on resolution unless you ask not to be named.
Scope
In scope:
- The hosted shithub instance (
shithub.sh). - The shithub source as published on GitHub
(
github.com/tenseleyFlow/shithub), exploited against any reasonably-deployed self-hosted instance running an unmodified release tag.
Out of scope:
- Findings against third-party services we depend on (DigitalOcean, Postmark, Let's Encrypt). Report those to the vendor.
- Misconfiguration of a self-hosted instance (e.g., operator
exposed
/metricswithout auth) — unless the misconfiguration is the default of a current release. - Rate-limit-bypass via heroic distributed-IP infrastructure —
outside the threat model
(
docs/internal/threat-model.md). - Issues that require physical access to the server.
- DoS via resource exhaustion that requires sustained heavy traffic from many unique IPs.
- Best-practice findings without an exploit path (e.g., "you're
not setting
X-Permitted-Cross-Domain-Policies") — file these as regular issues.
Bug bounty
shithub does not currently run a paid bounty program. We welcome findings regardless and will publicly credit you.
Severity
Coarse 4-level scale:
| Severity | Examples | Target fix |
|---|---|---|
| Critical | RCE; auth bypass; mass-account-takeover; private-data leak | < 24h |
| High | Per-user privilege escalation; SSRF into internal infra | < 7d |
| Medium | Stored XSS limited to an attacker's own scope; CSRF on a non-destructive route | < 30d |
| Low | Information disclosure of non-sensitive data | best-effort |
What you'll receive
- Acknowledgement within 72 hours (auto-ack faster).
- Triage decision — accepted, duplicate, out-of-scope, or needs-more-info — within 7 days for High+ and 30 days for Medium/Low.
- Fix timeline based on severity.
- Coordinated disclosure on patched release; we publish a brief writeup naming you (with consent) and the affected versions.
Hall of fame
Reporters who responsibly disclosed accepted findings:
(Empty for now — first credit goes to the first reporter.)
Actions runner sandbox
Workflow authors with repository write access are treated as untrusted
code execution users. shithubd-runner executes each run: step in a
fresh Docker or Podman container with these defaults:
- read-only container root filesystem
- writable, executable
/tmptmpfs capped at 1 GiB - writable
/workspacebind mount for step-to-step job state --cap-drop=ALLwith onlyDAC_OVERRIDE,SETGID, andSETUIDadded back--security-opt=no-new-privileges- pinned seccomp profile at
/etc/shithubd-runner/seccomp.json --user 65534:65534- PID, file-descriptor, process, CPU, memory, and log-size caps
The writable /workspace mount is deliberate. The v1 engine starts one
container per step, so checkout/build outputs need a host-backed job
workspace to survive into later steps. The root filesystem remains
read-only; writeable job state is confined to the per-job workspace that
the runner sweeps after completion.
DAC_OVERRIDE is the load-bearing concession that lets the non-root
container user write to the bind-mounted workspace owned by the runner
host user. It is not a general privilege grant: CAP_SYS_ADMIN is not
present, no-new-privileges is set, and the default seccomp profile still
filters dangerous syscalls.
Root containers are opt-in per job through an explicit shithub-only permissions key:
permissions:
shithub-runner-root: write
Broad write-all permissions do not imply root. This escape hatch is
for trusted maintenance workflows that need package-manager behavior
inside the container. Prefer a prebuilt runner image instead.
The runner host remains trusted infrastructure because Docker socket
access is equivalent to host-root in ordinary Docker deployments. Do
not run shithubd-runner on the web/database host.
Runner job JWTs are signed from an HKDF subkey derived from
auth.totp_key_b64 with label actions-runner-jwt-v1. To rotate the
runner JWT root, rotate auth.totp_key_b64, restart web and worker
processes, then restart runners so fresh claims use the new signer.
Existing in-flight job JWTs are short-lived and single-use; let them
expire or cancel the affected jobs before completing the rotation.
Our threat model
Published at
docs/internal/threat-model.md.
Useful context on what we defend against and what we don't.
View source
| 1 | # Security policy |
| 2 | |
| 3 | ## Reporting a vulnerability |
| 4 | |
| 5 | Email **`security@shithub.sh`**. PGP-encrypt the report |
| 6 | using the key fingerprint published at |
| 7 | `https://shithub.sh/.well-known/pgp-key.asc` if your finding |
| 8 | is sensitive. |
| 9 | |
| 10 | The mailbox auto-acknowledges receipt within minutes. A human |
| 11 | response (initial assessment + next steps) follows within |
| 12 | **72 hours**. |
| 13 | |
| 14 | Please **do not** file public issues for security findings. |
| 15 | Coordinated disclosure is the norm; we will credit you in the |
| 16 | hall of fame on resolution unless you ask not to be named. |
| 17 | |
| 18 | ## Scope |
| 19 | |
| 20 | In scope: |
| 21 | |
| 22 | - The hosted shithub instance (`shithub.sh`). |
| 23 | - The shithub source as published on GitHub |
| 24 | (`github.com/tenseleyFlow/shithub`), exploited against any |
| 25 | reasonably-deployed self-hosted instance running an unmodified |
| 26 | release tag. |
| 27 | |
| 28 | Out of scope: |
| 29 | |
| 30 | - Findings against third-party services we depend on |
| 31 | (DigitalOcean, Postmark, Let's Encrypt). Report those to the |
| 32 | vendor. |
| 33 | - Misconfiguration of a self-hosted instance (e.g., operator |
| 34 | exposed `/metrics` without auth) — unless the misconfiguration |
| 35 | is the *default* of a current release. |
| 36 | - Rate-limit-bypass via heroic distributed-IP infrastructure — |
| 37 | outside the threat model |
| 38 | (`docs/internal/threat-model.md`). |
| 39 | - Issues that require physical access to the server. |
| 40 | - DoS via resource exhaustion that requires sustained heavy |
| 41 | traffic from many unique IPs. |
| 42 | - Best-practice findings without an exploit path (e.g., "you're |
| 43 | not setting `X-Permitted-Cross-Domain-Policies`") — file these |
| 44 | as regular issues. |
| 45 | |
| 46 | ## Bug bounty |
| 47 | |
| 48 | shithub does not currently run a paid bounty program. We welcome |
| 49 | findings regardless and will publicly credit you. |
| 50 | |
| 51 | ## Severity |
| 52 | |
| 53 | Coarse 4-level scale: |
| 54 | |
| 55 | | Severity | Examples | Target fix | |
| 56 | |----------|----------------------------------------------------------------|-----------:| |
| 57 | | Critical | RCE; auth bypass; mass-account-takeover; private-data leak | < 24h | |
| 58 | | High | Per-user privilege escalation; SSRF into internal infra | < 7d | |
| 59 | | Medium | Stored XSS limited to an attacker's own scope; CSRF on a non-destructive route | < 30d | |
| 60 | | Low | Information disclosure of non-sensitive data | best-effort | |
| 61 | |
| 62 | ## What you'll receive |
| 63 | |
| 64 | - **Acknowledgement** within 72 hours (auto-ack faster). |
| 65 | - **Triage decision** — accepted, duplicate, out-of-scope, or |
| 66 | needs-more-info — within 7 days for High+ and 30 days for |
| 67 | Medium/Low. |
| 68 | - **Fix timeline** based on severity. |
| 69 | - **Coordinated disclosure** on patched release; we publish a |
| 70 | brief writeup naming you (with consent) and the affected |
| 71 | versions. |
| 72 | |
| 73 | ## Hall of fame |
| 74 | |
| 75 | Reporters who responsibly disclosed accepted findings: |
| 76 | |
| 77 | *(Empty for now — first credit goes to the first reporter.)* |
| 78 | |
| 79 | ## Actions runner sandbox |
| 80 | |
| 81 | Workflow authors with repository write access are treated as untrusted |
| 82 | code execution users. `shithubd-runner` executes each `run:` step in a |
| 83 | fresh Docker or Podman container with these defaults: |
| 84 | |
| 85 | - read-only container root filesystem |
| 86 | - writable, executable `/tmp` tmpfs capped at 1 GiB |
| 87 | - writable `/workspace` bind mount for step-to-step job state |
| 88 | - `--cap-drop=ALL` with only `DAC_OVERRIDE`, `SETGID`, and `SETUID` |
| 89 | added back |
| 90 | - `--security-opt=no-new-privileges` |
| 91 | - pinned seccomp profile at `/etc/shithubd-runner/seccomp.json` |
| 92 | - `--user 65534:65534` |
| 93 | - PID, file-descriptor, process, CPU, memory, and log-size caps |
| 94 | |
| 95 | The writable `/workspace` mount is deliberate. The v1 engine starts one |
| 96 | container per step, so checkout/build outputs need a host-backed job |
| 97 | workspace to survive into later steps. The root filesystem remains |
| 98 | read-only; writeable job state is confined to the per-job workspace that |
| 99 | the runner sweeps after completion. |
| 100 | |
| 101 | `DAC_OVERRIDE` is the load-bearing concession that lets the non-root |
| 102 | container user write to the bind-mounted workspace owned by the runner |
| 103 | host user. It is not a general privilege grant: `CAP_SYS_ADMIN` is not |
| 104 | present, no-new-privileges is set, and the default seccomp profile still |
| 105 | filters dangerous syscalls. |
| 106 | |
| 107 | Root containers are opt-in per job through an explicit shithub-only |
| 108 | permissions key: |
| 109 | |
| 110 | ```yaml |
| 111 | permissions: |
| 112 | shithub-runner-root: write |
| 113 | ``` |
| 114 | |
| 115 | Broad `write-all` permissions do not imply root. This escape hatch is |
| 116 | for trusted maintenance workflows that need package-manager behavior |
| 117 | inside the container. Prefer a prebuilt runner image instead. |
| 118 | |
| 119 | The runner host remains trusted infrastructure because Docker socket |
| 120 | access is equivalent to host-root in ordinary Docker deployments. Do |
| 121 | not run `shithubd-runner` on the web/database host. |
| 122 | |
| 123 | Runner job JWTs are signed from an HKDF subkey derived from |
| 124 | `auth.totp_key_b64` with label `actions-runner-jwt-v1`. To rotate the |
| 125 | runner JWT root, rotate `auth.totp_key_b64`, restart web and worker |
| 126 | processes, then restart runners so fresh claims use the new signer. |
| 127 | Existing in-flight job JWTs are short-lived and single-use; let them |
| 128 | expire or cancel the affected jobs before completing the rotation. |
| 129 | |
| 130 | ## Our threat model |
| 131 | |
| 132 | Published at |
| 133 | [`docs/internal/threat-model.md`](./docs/internal/threat-model.md). |
| 134 | Useful context on what we defend against and what we don't. |