Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions docs/deployment-strategy.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,70 @@ Production container deployments must adhere to the following security baselines
- **Scope:** The final runtime image (not intermediate build stages), scanned directly from the local Docker daemon.
- **Trade-off:** Setting the threshold to `critical` blocks deployment for any unpatched critical CVE in the final image. This is the strictest policy and may require occasional triage of false positives or accept-risk overrides. A more permissive "advisory-only" approach would log findings without blocking the pipeline; the current configuration chooses security gate over velocity. Teams may relax to `high` after evaluating their vulnerability management process.
- **Artifacts:** Scan reports in SARIF format are uploaded as workflow artifacts for every run (including PRs).

## Content Security Policy (CSP) Configuration

The frontend production image (`frontend/nginx.conf`) ships a strict Content-Security-Policy:

```
default-src 'self';
script-src 'self';
style-src 'self' https://fonts.googleapis.com;
font-src 'self' https://fonts.gstatic.com;
img-src 'self' data:;
connect-src 'self' ${API_ORIGIN} https://soroban-testnet.stellar.org https://horizon-testnet.stellar.org;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
report-uri /csp-report;
report-to csp-endpoint;
```

### Why no `'unsafe-inline'` in `style-src`

The codebase renders all styles through Tailwind CSS class utilities plus a small
set of stylesheet classes (`global.css`). There are no inline `style="..."` attributes
or `<style>` tags in the production bundle:

- `ProgressBar` sets its fill width via the CSSOM (`element.style.width`) instead of
an inline style attribute.
- `QueuesPage` card layout hints (`content-visibility`, `contain-intrinsic-size`) live
in the `.queue-card` stylesheet class.
- The built `dist/index.html` contains no inline scripts, so `script-src 'self'`
needs no `sha256-*` hash allowlist.

Before adding an inline style to the codebase, prefer a stylesheet class or CSSOM
assignment; otherwise a `sha256-*` hash must be added to the CSP.

### Configuring the API origin

`connect-src` allows the backend API origin through the nginx `$api_origin`
variable, which defaults to `http://localhost:4000`. For deployments where the API
is served from a different origin (e.g. `https://api.lineproof.com`), override it at
container startup:

```bash
# Edit the `set $api_origin "..."` line in the copied nginx.conf, or mount a
# replacement config:
docker run -v ./custom-nginx.conf:/etc/nginx/conf.d/default.conf:ro \
-p 8080:80 lineproof-frontend
```

`connect-src` additionally allows the Stellar Horizon and Soroban RPC endpoints the
frontend speaks to at runtime; extend the list if additional integrations are added.

### Violation reporting

Violations are reported to the same-origin `/csp-report` endpoint via
`report-uri`/`Reporting-Endpoints`. Point `report-uri` at your CSP collector if the
backend does not serve that path, and confirm `report-to`/`Reporting-Endpoints` are
consistent before enforcing a strict policy in production.

### Rollout guidance

1. Deploy with the CSP in `Report-Only` mode first
(`Content-Security-Policy-Report-Only`) and collect violations for a soak period.
2. Fix or allowlist any legitimate violations.
3. Switch to enforcement (`Content-Security-Policy`) once the report stream is clean.
4. Keep the violation collector wired up after enforcement; a clean report stream
confirms the policy is not breaking the app.
17 changes: 16 additions & 1 deletion frontend/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ server {
root /usr/share/nginx/html;
index index.html;

# Backend API origin used by the CSP connect-src directive.
# Defaults to the local development backend; override at deploy time if the
# API is served from a different origin (see docs/deployment-strategy.md).
set $api_origin "http://localhost:4000";

# Serve static assets with long cache
location ~* \.(js|css|woff2?|png|svg|ico)$ {
expires 1y;
Expand All @@ -19,5 +24,15 @@ server {
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;" always;
# Strict CSP:
# - style-src: no 'unsafe-inline' — Tailwind emits class-based styles only,
# and every inline style attribute has been removed from the codebase.
# - script-src: 'self' — the Vite production build emits no inline scripts
# (verified on the built dist/index.html), so no sha256 hash is required.
# - connect-src: explicit allow-list for the API origin and Stellar/Soroban
# endpoints used at runtime.
# - report-uri: collect violations server-side; adjust the endpoint to your
# reporting collector if /csp-report is not served by the backend.
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data:; connect-src 'self' $api_origin https://soroban-testnet.stellar.org https://horizon-testnet.stellar.org; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; report-uri /csp-report; report-to csp-endpoint;" always;
add_header Reporting-Endpoints 'csp-endpoint="/csp-report"' always;
}
16 changes: 14 additions & 2 deletions frontend/src/components/ProgressBar.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { useEffect, useRef } from 'react';

interface Props {
value: number; // 0–100
label?: string;
Expand All @@ -7,6 +9,16 @@ interface Props {

export default function ProgressBar({ value, label, className = '', ariaHidden }: Props) {
const pct = Math.min(100, Math.max(0, value));
const fillRef = useRef<HTMLDivElement | null>(null);

// Set the fill width via the CSSOM instead of an inline style attribute so the
// app stays compatible with the strict CSP (style-src 'self', no 'unsafe-inline').
useEffect(() => {
if (fillRef.current) {
fillRef.current.style.width = `${pct}%`;
}
}, [pct]);

return (
<div className={`space-y-1 ${className}`} aria-hidden={ariaHidden}>
{label && (
Expand All @@ -17,8 +29,8 @@ export default function ProgressBar({ value, label, className = '', ariaHidden }
)}
<div className="h-2 w-full overflow-hidden rounded-full bg-slate-100 dark:bg-slate-700">
<div
ref={fillRef}
className="h-2 rounded-full bg-slate-800 dark:bg-slate-300 transition-all duration-500"
style={{ width: `${pct}%` }}
role="progressbar"
aria-label={label ?? 'Progress'}
aria-valuenow={pct}
Expand All @@ -28,4 +40,4 @@ export default function ProgressBar({ value, label, className = '', ariaHidden }
</div>
</div>
);
}
}
6 changes: 1 addition & 5 deletions frontend/src/pages/QueuesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,7 @@ export default function QueuesPage() {
key={queue.id}
to={`/queues/${queue.id}`}
aria-label={queue.name}
className="rounded-2xl border border-slate-200 bg-white dark:bg-slate-800 dark:border-slate-700 p-5 shadow-sm transition hover:border-slate-300 dark:hover:border-slate-600 hover:shadow-md"
style={{
contentVisibility: 'auto',
containIntrinsicSize: 'auto 200px',
}}
className="queue-card rounded-2xl border border-slate-200 bg-white dark:bg-slate-800 dark:border-slate-700 p-5 shadow-sm transition hover:border-slate-300 dark:hover:border-slate-600 hover:shadow-md"
>
<div className="flex items-start justify-between gap-2">
<h2 className="text-base font-semibold text-slate-900 dark:text-slate-50">{queue.name}</h2>
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,12 @@ body {
margin: 0;
min-height: 100vh;
}

/* Rendering optimization for queue cards: content-visibility skips rendering
* work for cards scrolled out of view. Kept as a stylesheet class instead of
* an inline style attribute so the app stays compatible with the strict CSP
* (style-src 'self' — no 'unsafe-inline'). */
.queue-card {
content-visibility: auto;
contain-intrinsic-size: auto 200px;
}