markdown · 6205 bytes Raw Blame History

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 /metrics without 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 /tmp tmpfs capped at 1 GiB
  • writable /workspace bind mount for step-to-step job state
  • --cap-drop=ALL with only DAC_OVERRIDE, SETGID, and SETUID added 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.