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
- optional per-container DNS servers for operator-managed egress allowlisting
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.
Runner network allowlisting is an operator-managed control. The runner
config records runner.network_allowlist and passes
engine.dns_servers to each step container; the deployment role renders
a dnsmasq allowlist template. DNS filtering must be paired with host
firewall rules on the runner bridge to block direct-IP egress. Do not
treat DNS-only filtering as a complete network sandbox.
Actions secrets are decrypted only for the runner job claim that needs them. Repo secrets shadow org secrets with the same name. The runner receives the resolved secret map plus an exact-value mask set; runner logs are scrubbed before upload, and the web API scrubs again before persisting chunks. The server-side scrubber carries possible secret prefix tails across adjacent chunks so a bypassing runner cannot leak a secret by splitting it over multiple log POSTs. Base64-encoded or transformed secrets are not masked; workflows must not print secrets in derived forms.
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 | - optional per-container DNS servers for operator-managed egress |
| 95 | allowlisting |
| 96 | |
| 97 | The writable `/workspace` mount is deliberate. The v1 engine starts one |
| 98 | container per step, so checkout/build outputs need a host-backed job |
| 99 | workspace to survive into later steps. The root filesystem remains |
| 100 | read-only; writeable job state is confined to the per-job workspace that |
| 101 | the runner sweeps after completion. |
| 102 | |
| 103 | `DAC_OVERRIDE` is the load-bearing concession that lets the non-root |
| 104 | container user write to the bind-mounted workspace owned by the runner |
| 105 | host user. It is not a general privilege grant: `CAP_SYS_ADMIN` is not |
| 106 | present, no-new-privileges is set, and the default seccomp profile still |
| 107 | filters dangerous syscalls. |
| 108 | |
| 109 | Runner network allowlisting is an operator-managed control. The runner |
| 110 | config records `runner.network_allowlist` and passes |
| 111 | `engine.dns_servers` to each step container; the deployment role renders |
| 112 | a dnsmasq allowlist template. DNS filtering must be paired with host |
| 113 | firewall rules on the runner bridge to block direct-IP egress. Do not |
| 114 | treat DNS-only filtering as a complete network sandbox. |
| 115 | |
| 116 | Actions secrets are decrypted only for the runner job claim that needs |
| 117 | them. Repo secrets shadow org secrets with the same name. The runner |
| 118 | receives the resolved secret map plus an exact-value mask set; runner |
| 119 | logs are scrubbed before upload, and the web API scrubs again before |
| 120 | persisting chunks. The server-side scrubber carries possible secret |
| 121 | prefix tails across adjacent chunks so a bypassing runner cannot leak a |
| 122 | secret by splitting it over multiple log POSTs. Base64-encoded or |
| 123 | transformed secrets are not masked; workflows must not print secrets in |
| 124 | derived forms. |
| 125 | |
| 126 | Root containers are opt-in per job through an explicit shithub-only |
| 127 | permissions key: |
| 128 | |
| 129 | ```yaml |
| 130 | permissions: |
| 131 | shithub-runner-root: write |
| 132 | ``` |
| 133 | |
| 134 | Broad `write-all` permissions do not imply root. This escape hatch is |
| 135 | for trusted maintenance workflows that need package-manager behavior |
| 136 | inside the container. Prefer a prebuilt runner image instead. |
| 137 | |
| 138 | The runner host remains trusted infrastructure because Docker socket |
| 139 | access is equivalent to host-root in ordinary Docker deployments. Do |
| 140 | not run `shithubd-runner` on the web/database host. |
| 141 | |
| 142 | Runner job JWTs are signed from an HKDF subkey derived from |
| 143 | `auth.totp_key_b64` with label `actions-runner-jwt-v1`. To rotate the |
| 144 | runner JWT root, rotate `auth.totp_key_b64`, restart web and worker |
| 145 | processes, then restart runners so fresh claims use the new signer. |
| 146 | Existing in-flight job JWTs are short-lived and single-use; let them |
| 147 | expire or cancel the affected jobs before completing the rotation. |
| 148 | |
| 149 | ## Our threat model |
| 150 | |
| 151 | Published at |
| 152 | [`docs/internal/threat-model.md`](./docs/internal/threat-model.md). |
| 153 | Useful context on what we defend against and what we don't. |