document the CSP the pages override; allowlist the build context
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
docs/ARCHITECTURE.md records why internal/web.render replaces the chassis JSON-API policy with a per-response nonce policy, and the failure each of the three decisions prevents: a header-only policy because two policies on one response intersect, a nonce instead of 'unsafe-inline' because the guarantee is that only the reviewed same-document script reaches the fragment key, and a fresh url-alphabet value because a reused nonce is worth 'unsafe-inline' to anyone who waits for the next load and + or / would make enforcement depend on entity decoding. README.md now names the buttons the page actually renders and says outright that there is no lifetime picker — the server's 24h default applies and ttl_seconds is where a caller chooses. base.html drops the opacity transition; nothing animates opacity. .dockerignore is an allowlist, because the build stage COPYs only go.mod, go.sum, vendor/, cmd/ and internal/. A blocklist forgets the file nobody predicted, and for this service that file is a secret. .gitignore grows the same protection for the working tree.
This commit is contained in:
parent
ac52fbe0b9
commit
b6098c28be
14
.dockerignore
Normal file
14
.dockerignore
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# Allowlist, not a blocklist. The build stage COPYs exactly go.mod, go.sum,
|
||||||
|
# vendor/, cmd/ and internal/ — so anything else in the context is bytes Kaniko
|
||||||
|
# uploads for nothing, and a `.env` or key dropped in the tree is one a build
|
||||||
|
# layer can never capture. A blocklist forgets the file nobody predicted.
|
||||||
|
*
|
||||||
|
!go.mod
|
||||||
|
!go.sum
|
||||||
|
!vendor
|
||||||
|
!cmd
|
||||||
|
!internal
|
||||||
|
|
||||||
|
# Kaniko reads this before the context is filtered; allowlisted so the two
|
||||||
|
# cannot disagree.
|
||||||
|
!Dockerfile
|
||||||
18
.gitignore
vendored
18
.gitignore
vendored
@ -1,2 +1,20 @@
|
|||||||
.build/
|
.build/
|
||||||
*.tmp
|
*.tmp
|
||||||
|
|
||||||
|
# Secrets: never tracked, never in a build context
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
.envault/
|
||||||
|
*.pem
|
||||||
|
*.key
|
||||||
|
*.p12
|
||||||
|
*.pfx
|
||||||
|
credentials.json
|
||||||
|
service-account*.json
|
||||||
|
|
||||||
|
# Editor and OS noise
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
|||||||
@ -40,9 +40,13 @@ wrong tool — use a channel with identity.
|
|||||||
### In a browser
|
### In a browser
|
||||||
|
|
||||||
1. Open <https://hush.threesix.ai>.
|
1. Open <https://hush.threesix.ai>.
|
||||||
2. Paste the secret, pick a lifetime, press **Create link**.
|
2. Paste the secret, press **create a secret**.
|
||||||
3. Copy the link and send it however you like.
|
3. Copy the link and send it however you like.
|
||||||
4. The recipient opens it, presses **Reveal**, and reads it once.
|
4. The recipient opens it, presses **reveal the secret**, and reads it once.
|
||||||
|
|
||||||
|
There is no lifetime picker: the page offers one action, and the server applies
|
||||||
|
its default TTL (24 hours). `ttl_seconds` on the API is where a caller that
|
||||||
|
cares chooses.
|
||||||
|
|
||||||
### Why there is a button
|
### Why there is a button
|
||||||
|
|
||||||
|
|||||||
@ -132,6 +132,37 @@ the panic recovery envelope, RED metrics, secure headers, the two-phase drain,
|
|||||||
and `/healthz`, `/readyz`, `/metrics`. hush contributes handlers, a store, a
|
and `/healthz`, `/readyz`, `/metrics`. hush contributes handlers, a store, a
|
||||||
rate limiter and templates — not a framework.
|
rate limiter and templates — not a framework.
|
||||||
|
|
||||||
|
### The pages override the chassis CSP
|
||||||
|
|
||||||
|
The chassis policy is written for a JSON API: `default-src 'none';
|
||||||
|
frame-ancestors 'none'`. The two pages are HTML with inline script and inline
|
||||||
|
style, so `internal/web.render` replaces that header with a per-response
|
||||||
|
nonce policy:
|
||||||
|
|
||||||
|
```
|
||||||
|
default-src 'none'; script-src 'nonce-<r>'; style-src 'nonce-<r>';
|
||||||
|
connect-src 'self'; form-action 'none'; base-uri 'none'; frame-ancestors 'none'
|
||||||
|
```
|
||||||
|
|
||||||
|
Three decisions, each with a failure it prevents:
|
||||||
|
|
||||||
|
- **A header, and only a header.** Two policies delivered on one response
|
||||||
|
intersect, so a permissive `<meta>` cannot re-enable what the header forbids.
|
||||||
|
Shipping both is how the pages ended up with their own crypto and their own
|
||||||
|
`fetch` blocked while the `<meta>` read as permitted. `frame-ancestors` is
|
||||||
|
also ignored outright in `<meta>`, so it exists only as a header.
|
||||||
|
- **A nonce, not `'unsafe-inline'`.** The guarantee is that nothing but this
|
||||||
|
reviewed same-document script can reach the key in the fragment;
|
||||||
|
`'unsafe-inline'` would extend that permission to anything an injection got
|
||||||
|
onto the page.
|
||||||
|
- **Fresh per response, url-alphabet base64.** A reused nonce is worth
|
||||||
|
`'unsafe-inline'` to an attacker who can wait for the next load, and `+` or
|
||||||
|
`/` in the value would be escaped to character references inside the HTML
|
||||||
|
attribute, making what the browser enforces depend on entity decoding. The
|
||||||
|
nonce is fixed-length, so it adds no id-correlated variation to the reveal
|
||||||
|
page — `TestTheRevealPageDoesNotDiscloseWhetherASecretExists` compares the
|
||||||
|
page with it masked and asserts constant length.
|
||||||
|
|
||||||
The public Ingress routes `/` (exact), `/s/` and `/api/` only. `/metrics`,
|
The public Ingress routes `/` (exact), `/s/` and `/api/` only. `/metrics`,
|
||||||
`/healthz` and `/readyz` share the port but are unreachable from the internet;
|
`/healthz` and `/readyz` share the port but are unreachable from the internet;
|
||||||
vmagent scrapes the pod IP directly. This is why there is no metrics basic-auth
|
vmagent scrapes the pod IP directly. This is why there is no metrics basic-auth
|
||||||
|
|||||||
@ -58,7 +58,7 @@ textarea:focus,button:focus-visible{outline:0;border-color:var(--line-lit);box-s
|
|||||||
button{
|
button{
|
||||||
margin-top:14px;padding:11px 16px;border:1px solid transparent;
|
margin-top:14px;padding:11px 16px;border:1px solid transparent;
|
||||||
background:var(--fg);color:#08090a;font-weight:600;font-size:14px;
|
background:var(--fg);color:#08090a;font-weight:600;font-size:14px;
|
||||||
cursor:pointer;transition:background .12s,border-color .12s,color .12s,opacity .12s;
|
cursor:pointer;transition:background .12s,border-color .12s,color .12s;
|
||||||
}
|
}
|
||||||
button:hover:not(:disabled){background:#fff}
|
button:hover:not(:disabled){background:#fff}
|
||||||
button:active:not(:disabled){transform:translateY(.5px)}
|
button:active:not(:disabled){transform:translateY(.5px)}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user