Skip to content
Merged
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
21 changes: 12 additions & 9 deletions src/pages/docs/api.astro
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@ import DocsLayout from '@/layouts/DocsLayout.astro';
no GraphQL endpoint.
</p>
<p>
Every <code>/v1</code> route except <code>GET /v1/health</code> checks
<code>OM_API_SECRET</code>. Send it as <code>Authorization: Bearer …</code> or
<code>X-API-Key</code>. The console HTML does not. Treat the API as an internal service.
Details are on <a href="/docs/security/">Security</a>.
Machine clients send that admin's personal API token as <code>Authorization: Bearer …</code> or
<code>X-API-Key</code>. A console session cookie is the other accepted credential. The examples
below use <code>$OM_API_TOKEN</code> for the value the console showed once.
<code>GET /v1/health</code>, <code>GET /v1/auth/session</code>, and
<code>POST /v1/auth/logout</code> are open. Admin management requires the session.
Treat the API as an internal service. Details are on <a href="/docs/security/">Security</a>
and <a href="/docs/console/">Console</a>.
</p>

<h2>Reads</h2>
Expand All @@ -33,7 +36,7 @@ import DocsLayout from '@/layouts/DocsLayout.astro';
<tbody>
<tr>
<td><code>GET /v1/health</code></td>
<td><code>{`{"status":"ok","service":"opensourceom-api"}`}</code>. Does not check the API secret. Helm probes this path.</td>
<td><code>{`{"status":"ok","service":"opensourceom-api"}`}</code>. Does not check a token. Helm probes this path.</td>
</tr>
<tr>
<td><code>GET /v1/graph/stats</code></td>
Expand Down Expand Up @@ -105,9 +108,9 @@ import DocsLayout from '@/layouts/DocsLayout.astro';
</table>
</div>
<pre><code>{`curl -s http://localhost:8080/v1/health
curl -s -H "Authorization: Bearer $OM_API_SECRET" \\
curl -s -H "Authorization: Bearer $OM_API_TOKEN" \\
http://localhost:8080/v1/graph/stats
curl -s -H "Authorization: Bearer $OM_API_SECRET" \\
curl -s -H "Authorization: Bearer $OM_API_TOKEN" \\
-H 'Content-Type: application/json' \\
-d '{"nodes":[],"edges":[]}' \\
http://localhost:8080/v1/ingest`}</code></pre>
Expand Down Expand Up @@ -151,9 +154,9 @@ curl -s -H "Authorization: Bearer $OM_API_SECRET" \\
<code>GET /v1/identity/blast-radius</code> returns the identity, <code>reachable</code>
(at most 200 nodes), <code>max_depth</code> (6), and <code>summary</code>. Pass
<code>identity_id</code> or <code>name</code>. A name that matches no identity is 400.
This route requires the API secret, like the other <code>/v1</code> reads.
This route requires a personal API token or a console session, like the other <code>/v1</code> reads.
</p>
<pre><code>{`curl -s -X POST -H "Authorization: Bearer $OM_API_SECRET" \\
<pre><code>{`curl -s -X POST -H "Authorization: Bearer $OM_API_TOKEN" \\
-H 'Content-Type: application/json' \\
-d '{"webhook":"https://hooks.slack.com/services/..."}' \\
http://localhost:8080/v1/export/slack`}</code></pre>
Expand Down
2 changes: 1 addition & 1 deletion src/pages/docs/architecture.astro
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ import DocsLayout from '@/layouts/DocsLayout.astro';
The CLI opens Postgres and writes batches, findings, and export calls itself. The API opens
the same database. <code>POST /v1/ingest</code> is the HTTP equivalent of an upsert.
<code>om scan</code> does not call the API. That split is why scans work when you only start
the Postgres container, and why the console API key is irrelevant to the CLI.
the Postgres container, and why a console session is irrelevant to the CLI.
</p>

<h2>Deployment shapes</h2>
Expand Down
2 changes: 1 addition & 1 deletion src/pages/docs/cli.astro
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import DocsLayout from '@/layouts/DocsLayout.astro';
<h1>OpenSourceOM CLI</h1>
<p>
The <code>om</code> binary talks to Postgres directly. Scans, rules, queries, and exports do
not go through the HTTP API, so they do not send <code>OM_API_SECRET</code>. Build it from
not go through the HTTP API, so they do not send a personal API token. Build it from
the core repo with <code>go build -o om ./cmd/om</code>, or use a release archive.
</p>
<p>
Expand Down
30 changes: 25 additions & 5 deletions src/pages/docs/configuration.astro
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import DocsLayout from '@/layouts/DocsLayout.astro';

<DocsLayout
title="Configure OpenSourceOM"
description="Environment variables for the OpenSourceOM CLI, API secret, Postgres, NVD, and AWS, Azure, GCP, and Kubernetes collectors."
description="Environment variables for the OpenSourceOM CLI, console login, SMTP, Postgres, NVD, and AWS, Azure, GCP, and Kubernetes collectors."
>
<h1>Configure OpenSourceOM</h1>
<p>
Expand Down Expand Up @@ -45,17 +45,37 @@ import DocsLayout from '@/layouts/DocsLayout.astro';
<tr>
<td><code>OM_PUBLIC_URL</code></td>
<td><code>http://localhost:8080</code></td>
<td>Public URL hint. Requests use the listen address.</td>
<td>Browser origin for the console and optional OAuth callbacks. No trailing path.</td>
</tr>
<tr>
<td><code>OM_API_PORT</code></td>
<td><code>8080</code></td>
<td><code>om serve</code></td>
</tr>
<tr>
<td><code>OM_API_SECRET</code></td>
<td><code>change-me-in-production</code></td>
<td>Shared secret for every <code>/v1</code> route except <code>GET /v1/health</code>. See <a href="/docs/security/">Security</a>.</td>
<td><code>OM_ADMIN_EMAIL</code></td>
<td>empty</td>
<td>When set, only this address can become the first admin.</td>
</tr>
<tr>
<td><code>OM_SMTP_HOST</code> / <code>OM_SMTP_PORT</code> / <code>OM_SMTP_FROM</code></td>
<td>empty / <code>587</code> / empty</td>
<td>Optional. When set, adding an admin also emails the temporary password.</td>
</tr>
<tr>
<td><code>OM_SMTP_USERNAME</code> / <code>OM_SMTP_PASSWORD</code></td>
<td>empty</td>
<td>Optional SMTP authentication. Set both when the server requires a login.</td>
</tr>
<tr>
<td><code>OM_GOOGLE_CLIENT_ID</code> / <code>OM_GOOGLE_CLIENT_SECRET</code></td>
<td>empty</td>
<td>Google sign-in. Both are required. Callback <code>{'{OM_PUBLIC_URL}'}/auth/google/callback</code>.</td>
</tr>
<tr>
<td><code>OM_GITHUB_CLIENT_ID</code> / <code>OM_GITHUB_CLIENT_SECRET</code></td>
<td>empty</td>
<td>GitHub sign-in. Both are required. Callback <code>{'{OM_PUBLIC_URL}'}/auth/github/callback</code>.</td>
</tr>
</tbody>
</table>
Expand Down
40 changes: 30 additions & 10 deletions src/pages/docs/console.astro
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,39 @@ import DocsLayout from '@/layouts/DocsLayout.astro';
list and is not drawn.
</p>

<h2>Run CSPM rules from the browser</h2>
<h2>Sign in</h2>
<p>
The console signs people in with an email and password. There is no API-key field.
While no admin exists, the page is a setup form. Keep the API port private until you
finish it, or run <code>om admin create --email you@example.com</code> on the machine
first. Set <code>OM_ADMIN_EMAIL</code> when that first address should be fixed ahead of
time. See <a href="/docs/configuration/">Configuration</a>.
</p>
<p>
An admin adds the next person from the <strong>Admins</strong> panel. The temporary
password is shown once. If SMTP is configured, it is emailed too. That person chooses
their own password at the next sign-in. Every signed-in account is an admin, and the last
admin cannot be removed.
</p>
<p>
The <strong>Run CSPM rules</strong> button calls <code>POST /v1/rules/run</code>. That route
requires the API secret. Enter it in the <strong>API key</strong> field and choose
<strong>Save API key</strong>. The page stores it in <code>localStorage.om_api_key</code> and
sends <code>X-API-Key</code> on <code>/v1</code> calls. The key is not written into the graph.
Clearing the field and saving removes it. The HTML page loads without a key. Stats, findings,
and the graph need the secret. <code>GET /v1/health</code> does not.
Google and GitHub buttons appear only when that provider is configured. Register
<code>{'{OM_PUBLIC_URL}'}/auth/google/callback</code> or
<code>{'{OM_PUBLIC_URL}'}/auth/github/callback</code>. A verified email from the provider
must already be an admin. The <strong>Security</strong> panel turns on optional
authenticator-app two-factor authentication.
</p>
<p>
Use the value of <code>OM_API_SECRET</code> from the environment the API process actually
has. Running <code>om rules run</code> on the CLI does the same work without the browser key,
because the CLI uses Postgres directly.
The browser keeps an httpOnly session cookie for 7 days. Creating an admin also creates a
personal API token. The console shows it once. Scripts send that token as
<code>Authorization: Bearer</code> or <code>X-API-Key</code>. The token cannot add or
remove admins. Rotate it from the Admins panel when you need a new one.
</p>

<h2>Run CSPM rules from the browser</h2>
<p>
The <strong>Run CSPM rules</strong> button calls <code>POST /v1/rules/run</code> with the
session cookie. Running <code>om rules run</code> on the CLI does the same work without a
browser session, because the CLI uses Postgres directly.
</p>

<h2>Graph script</h2>
Expand Down
5 changes: 4 additions & 1 deletion src/pages/docs/docker.astro
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ docker compose ps`}</code></pre>
Postgres listens on host port 5432 with user, password, and database <code>opensourceom</code>
unless you override <code>POSTGRES_USER</code>, <code>POSTGRES_PASSWORD</code>, and
<code>POSTGRES_DB</code>. The API listens on <code>OM_API_PORT</code> (default 8080) and
receives <code>OM_API_SECRET</code> from <code>.env</code>. A volume named
receives <code>OM_ADMIN_EMAIL</code> and the optional Google, GitHub, and SMTP settings from
<code>.env</code>. Open the console and create the admin, or run
<code>om admin create --email</code> on the host first. Keep port 8080 private until an
admin exists. A volume named
<code>postgres-data</code> keeps the database across restarts.
</p>

Expand Down
2 changes: 1 addition & 1 deletion src/pages/docs/exports.astro
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ import DocsLayout from '@/layouts/DocsLayout.astro';
<p>
The API posts the same digest:
</p>
<pre><code>{`curl -s -X POST -H "Authorization: Bearer $OM_API_SECRET" \\
<pre><code>{`curl -s -X POST -H "Authorization: Bearer $OM_API_TOKEN" \\
-H 'Content-Type: application/json' \\
-d '{"webhook":"https://hooks.slack.com/services/..."}' \\
http://localhost:8080/v1/export/slack`}</code></pre>
Expand Down
2 changes: 1 addition & 1 deletion src/pages/docs/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ import DocsLayout from '@/layouts/DocsLayout.astro';
<li><a href="/docs/architecture/">Architecture</a> — components and data flow</li>
<li><a href="/docs/docker/">Docker Compose</a> — local Postgres and API</li>
<li><a href="/docs/kubernetes/">Kubernetes</a> — Helm chart</li>
<li><a href="/docs/security/">Security</a> — API secret, read-only collectors, vulnerability reports</li>
<li><a href="/docs/security/">Security</a> — personal API tokens, read-only collectors, vulnerability reports</li>
<li><a href="/docs/open-source/">Open source scope</a> — what stays in Core versus planned commercial work</li>
</ul>
</DocsLayout>
32 changes: 26 additions & 6 deletions src/pages/docs/kubernetes.astro
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,13 @@ import DocsLayout from '@/layouts/DocsLayout.astro';

<h2>Install</h2>
<p>
<code>api.secret</code> and <code>postgres.password</code> are required. The image repository
<code>postgres.password</code> is required. The image repository
defaults to <code>ghcr.io/opensourceom/core</code>. An empty <code>image.tag</code> falls
back to the chart <code>appVersion</code> (<code>0.3.0</code>). Set the tag when you are not
running that release.
</p>
<pre><code>{`docker build -t ghcr.io/opensourceom/core:0.3.0 .
helm install om deploy/helm/opensourceom \\
--set api.secret='change-me' \\
--set postgres.password='change-me' \\
--set image.tag=0.3.0`}</code></pre>
<p>
Expand Down Expand Up @@ -78,9 +77,29 @@ helm install om deploy/helm/opensourceom \\
<td>Written to <code>OM_ENV</code>.</td>
</tr>
<tr>
<td><code>api.secret</code></td>
<td>empty, required</td>
<td><code>OM_API_SECRET</code>.</td>
<td><code>api.smtp.host</code> / <code>api.smtp.port</code> / <code>api.smtp.from</code></td>
<td>empty / <code>587</code> / empty</td>
<td>Optional SMTP for temporary admin passwords. Setting the host requires <code>from</code>. A password stays in the chart Secret.</td>
</tr>
<tr>
<td><code>api.publicURL</code></td>
<td>empty</td>
<td><code>OM_PUBLIC_URL</code>. Set this to the browser origin before optional Google or GitHub login works.</td>
</tr>
<tr>
<td><code>api.adminEmail</code></td>
<td>empty</td>
<td><code>OM_ADMIN_EMAIL</code>. When set, only this address can become the first admin.</td>
</tr>
<tr>
<td><code>api.google.clientId</code> / <code>api.google.clientSecret</code></td>
<td>empty</td>
<td>Google sign-in. Setting the id requires the secret. The secret stays in the chart Secret.</td>
</tr>
<tr>
<td><code>api.github.clientId</code> / <code>api.github.clientSecret</code></td>
<td>empty</td>
<td>GitHub sign-in. Same pattern as Google.</td>
</tr>
<tr>
<td><code>postgres.enabled</code></td>
Expand Down Expand Up @@ -144,6 +163,7 @@ helm install om deploy/helm/opensourceom \\
</p>
<p>
Keep the Service off the public internet, or put your own gateway in front of it. See
<a href="/docs/security/">Security</a> for how <code>OM_API_SECRET</code> applies.
<a href="/docs/security/">Security</a> for personal API tokens and console login. Set
<code>api.publicURL</code> to that gateway’s origin when you enable Google or GitHub.
</p>
</DocsLayout>
11 changes: 6 additions & 5 deletions src/pages/docs/rules.astro
Original file line number Diff line number Diff line change
Expand Up @@ -94,14 +94,15 @@ import DocsLayout from '@/layouts/DocsLayout.astro';
</p>

<h2>HTTP</h2>
<pre><code>{`curl -s http://localhost:8080/v1/rules
curl -s -X POST -H "Authorization: Bearer $OM_API_SECRET" \\
<pre><code>{`curl -s -H "Authorization: Bearer $OM_API_TOKEN" \\
http://localhost:8080/v1/rules
curl -s -X POST -H "Authorization: Bearer $OM_API_TOKEN" \\
http://localhost:8080/v1/rules/run
curl -s -X POST -H "Authorization: Bearer $OM_API_SECRET" \\
curl -s -X POST -H "Authorization: Bearer $OM_API_TOKEN" \\
'http://localhost:8080/v1/rules/run?id=cspm-internet-workload'`}</code></pre>
<p>
The console button does the same POST. See <a href="/docs/console/">Console</a> for the
browser API key.
The console button does the same POST with the signed-in session. See
<a href="/docs/console/">Console</a>.
</p>

<h2>Adding a check</h2>
Expand Down
56 changes: 39 additions & 17 deletions src/pages/docs/security.astro
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,58 @@ import DocsLayout from '@/layouts/DocsLayout.astro';

<DocsLayout
title="OpenSourceOM security model"
description="How OM_API_SECRET protects every /v1 route except health, why the API stays on a private network, and how to report bugs."
description="How passwords, optional two-factor authentication, and personal API tokens protect /v1, why the API stays on a private network, and how to report bugs."
>
<h1>OpenSourceOM security model</h1>
<p>
Core assumes it runs in a network you control. The database holds a copy of your inventory,
IAM relationships, and findings. Protect Postgres and the API the way you protect that data.
</p>

<h2>API secret</h2>
<h2>Personal API tokens</h2>
<p>
<code>OM_API_SECRET</code> defaults to <code>change-me-in-production</code> when the variable
is unset or empty. Every <code>/v1</code> route except <code>GET /v1/health</code> requires
<code>Authorization: Bearer &lt;secret&gt;</code> or <code>X-API-Key: &lt;secret&gt;</code>.
That includes graph reads, findings, the rules list, blast radius, ingest, rules run, and
Slack export. A missing or wrong secret is 401.
Each admin has a personal API token. The console shows the raw value once, after the account
is created or after a rotation. Graph and ingest routes require
<code>Authorization: Bearer &lt;token&gt;</code>, <code>X-API-Key: &lt;token&gt;</code>, or
a console session cookie. That includes graph reads, findings, the rules list, blast radius,
ingest, rules run, and Slack export. A missing or wrong credential is 401. There is no shared
secret and no open mode.
</p>
<p>
<code>GET /v1/health</code> does not check the secret, so probes can call it. The console
HTML at <code>/</code> and <code>/ui/</code> is also unauthenticated. The page sends the key
from local storage on API calls. See <a href="/docs/console/">Console</a>.
<code>GET /v1/health</code>, <code>GET /v1/auth/session</code>,
<code>POST /v1/auth/logout</code>, <code>POST /v1/auth/setup</code>,
<code>POST /v1/auth/login</code>, and <code>POST /v1/auth/totp</code> do not require a session.
<code>GET /v1/admins</code>, <code>POST /v1/admins</code>,
<code>POST /v1/admins/token</code>, and
<code>DELETE /v1/admins/{'{id}'}</code> require a console session. A personal token cannot
add or remove admins. The console HTML at <code>/</code> and <code>/login</code> loads
without a session so the sign-in page can render. See <a href="/docs/console/">Console</a>.
</p>
<p>
Change the secret before anyone else can reach the port. Helm refuses to install until
<code>api.secret</code> is set. The CLI never sends this header. Anyone who can open a local
shell as a user with database credentials can scan and export without it.
Keep the API port private until the first admin exists. Whoever can open it first can become
that admin. Create the account with <code>om admin create</code> before you publish the
port when you can. The CLI never sends this header. Anyone who can open a local shell as a
user with database credentials can scan and export without a token.
</p>
<p>
The secret is one shared key, not a user login. Do not publish the Service to the internet.
Put it on a private network or behind a gateway you operate.
People use an email and password. Scripts use a personal token. Do not publish the Service
to the internet. Put it on a private network or behind a gateway you operate.
</p>

<h2>Console admins</h2>
<p>
People sign in with an email and password. Every account is an admin. The first account is
created from the setup form, or with <code>om admin create</code> before the port is
reachable. <code>OM_ADMIN_EMAIL</code> pins that first address when you set it. Later admins
receive a temporary password and choose their own at the next sign-in. Passwords are stored
as Argon2id hashes. Optional authenticator-app two-factor authentication is per admin.
Sessions are httpOnly cookies. The cookie is marked <code>Secure</code> when
<code>OM_PUBLIC_URL</code> uses <code>https</code>.
</p>
<p>
Google and GitHub are optional. A verified provider email must match an existing admin.
OAuth client secrets and the SMTP password are deployment secrets. Password login works
without them, including on an air-gapped console.
</p>

<h2>Collectors</h2>
Expand All @@ -54,8 +77,7 @@ import DocsLayout from '@/layouts/DocsLayout.astro';
<h2>Console script</h2>
<p>
The graph view loads vis-network from <code>/ui/vis-network.min.js</code> on the API. It is
not loaded from a CDN. The API key saved in the console stays in browser local storage and
is not written into the graph.
not loaded from a CDN. The session cookie is httpOnly, so page script cannot read it.
</p>

<h2>Reporting a vulnerability</h2>
Expand Down
Loading