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
64 changes: 35 additions & 29 deletions distribution/all-in-one/devportal-config.toml
Original file line number Diff line number Diff line change
@@ -1,48 +1,54 @@
# Developer Portal configuration for the all-in-one (build-from-source) compose
# stack. Mounted at /app/configs/config.toml — replaces the image's shipped
# configs/config.toml, which fails closed at startup on the required security
# secrets and only wires the SQLite `file` key (this stack shares one Postgres
# secrets and only wires the SQLite `path` key (this stack shares one Postgres
# server with platform-api).
#
# There is NO automatic APIP_DP_* environment-variable override (see
# src/config/configLoader.js) — an env var set in docker-compose.yaml's
# `devportal.environment` block only takes effect where a key below explicitly
# references it via a {{ env "..." }} token.
# Every key lives under the single [developer_portal] table; configLoader.js
# unwraps it and IGNORES anything outside it, so a section at the top level is
# silently dropped and its defaults (src/config/configDefaults.js) win instead.
#
# There is NO automatic APIP_DP_* environment-variable override — an env var set
# in docker-compose.yaml's `devportal.environment` block only takes effect where
# a key below explicitly references it via a {{ env "..." }} token.

[server]
base_url = '{{ env "APIP_DP_SERVER_BASEURL" "http://localhost:3001" }}'
port = 3000
[developer_portal.server]
port = '{{ env "APIP_DP_SERVER_PORT" "3000" }}'

[tls]
[developer_portal.server.https]
# Plain HTTP — nothing in this stack talks to devportal over TLS.
enabled = false
enabled = '{{ env "APIP_DP_SERVER_HTTPS_ENABLED" "false" }}'

[logging]
console_only = true
[developer_portal.logging]
console_only = '{{ env "APIP_DP_LOGGING_CONSOLE_ONLY" "true" }}'

[database]
type = '{{ env "APIP_DP_DATABASE_TYPE" "postgres" }}'
[developer_portal.database]
driver = '{{ env "APIP_DP_DATABASE_DRIVER" "postgres" }}'
host = '{{ env "APIP_DP_DATABASE_HOST" "postgres" }}'
port = '{{ env "APIP_DP_DATABASE_PORT" "5432" }}'
name = '{{ env "APIP_DP_DATABASE_NAME" "devportal" }}'
username = '{{ env "APIP_DP_DATABASE_USERNAME" "postgres" }}'
user = '{{ env "APIP_DP_DATABASE_USER" "postgres" }}'
password = '{{ env "APIP_DP_DATABASE_PASSWORD" "postgres" }}'

[organization]
default_name = '{{ env "APIP_DP_ORGANIZATION_DEFAULTNAME" "default" }}'
[developer_portal.organization]
default_name = '{{ env "APIP_DP_ORGANIZATION_DEFAULT_NAME" "default" }}'

[developer_portal.auth]
mode = "local"

[platform_api]
# platform-api signs admin JWTs with RS256 — there is no shared HMAC secret to
# set here. jwt_secret left empty makes the devportal decode the token payload
# without verifying its signature, trusting the direct HTTPS connection
# (insecure=true) instead — see extractPlatformJwtClaims in
# src/utils/platformJwt.js. platform-api's cert is self-signed (platform-api-certgen).
base_url = '{{ env "APIP_DP_PLATFORMAPI_BASEURL" "https://platform-api:9243" }}'
insecure = true
[developer_portal.auth.local]
# platform-api signs its tokens with RS256; the devportal verifies them against
# the public half of that keypair, bind-mounted read-only at /etc/devportal/keys.
# Only jwt_public.pem is exposed here (the private key stays with platform-api);
# the devportal reads it from that path.
platform_api_url = '{{ env "APIP_DP_AUTH_LOCAL_PLATFORM_API_URL" "https://platform-api:9243" }}'
public_key_path = '{{ env "APIP_DP_AUTH_LOCAL_PUBLIC_KEY_PATH" "/etc/devportal/keys/jwt_public.pem" }}'
# platform-api's cert is self-signed (platform-api-certgen).
tls_skip_verify = '{{ env "APIP_DP_AUTH_LOCAL_TLS_SKIP_VERIFY" "true" }}'
Comment thread
Piumal1999 marked this conversation as resolved.

[security]
[developer_portal.security]
# Required — devportal fails closed at startup if either doesn't resolve to a
# 64-char hex string. Set APIP_DP_SECURITY_ENCRYPTIONKEY / APIP_DP_SECURITY_SESSIONSECRET
# 64-char hex string. Set APIP_DP_SECURITY_ENCRYPTION_KEY / APIP_DP_SECURITY_SESSION_SECRET
# in the environment before `docker compose up` (e.g. `openssl rand -hex 32` each).
encryption_key = '{{ env "APIP_DP_SECURITY_ENCRYPTIONKEY" }}'
session_secret = '{{ env "APIP_DP_SECURITY_SESSIONSECRET" }}'
encryption_key = '{{ env "APIP_DP_SECURITY_ENCRYPTION_KEY" }}'
session_secret = '{{ env "APIP_DP_SECURITY_SESSION_SECRET" }}'
26 changes: 18 additions & 8 deletions distribution/all-in-one/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,31 +41,41 @@ services:
dockerfile: Dockerfile
container_name: devportal
environment:
- APIP_DP_SERVER_BASEURL=http://localhost:3001
- APIP_DP_DATABASE_TYPE=postgres
- APIP_DP_SERVER_HTTPS_ENABLED=false
- APIP_DP_DATABASE_DRIVER=postgres
- APIP_DP_DATABASE_HOST=postgres
- APIP_DP_DATABASE_PORT=5432
- APIP_DP_DATABASE_NAME=devportal
- APIP_DP_DATABASE_USERNAME=postgres
- APIP_DP_DATABASE_USER=postgres
- APIP_DP_DATABASE_PASSWORD=postgres
- APIP_DP_PLATFORMAPI_BASEURL=https://platform-api:9243
# platform-api signs its tokens with RS256; the devportal verifies them
# against jwt_public.pem from the shared keypair volume mounted below
# (auth.local.jwt_public_key in devportal-config.toml). There is no shared
# HMAC secret. tls_skip_verify covers platform-api's self-signed cert.
- APIP_DP_AUTH_LOCAL_PLATFORM_API_URL=https://platform-api:9243
- APIP_DP_AUTH_LOCAL_TLS_SKIP_VERIFY=true
# Required — devportal fails closed at startup if either doesn't resolve
# to a 64-char hex string. Set both before `docker compose up`, e.g.:
# export APIP_DP_SECURITY_ENCRYPTIONKEY=$(openssl rand -hex 32)
# export APIP_DP_SECURITY_SESSIONSECRET=$(openssl rand -hex 32)
- APIP_DP_SECURITY_ENCRYPTIONKEY=${APIP_DP_SECURITY_ENCRYPTIONKEY:-}
- APIP_DP_SECURITY_SESSIONSECRET=${APIP_DP_SECURITY_SESSIONSECRET:-}
# export APIP_DP_SECURITY_ENCRYPTION_KEY=$(openssl rand -hex 32)
# export APIP_DP_SECURITY_SESSION_SECRET=$(openssl rand -hex 32)
- APIP_DP_SECURITY_ENCRYPTION_KEY=${APIP_DP_SECURITY_ENCRYPTION_KEY:-}
- APIP_DP_SECURITY_SESSION_SECRET=${APIP_DP_SECURITY_SESSION_SECRET:-}
ports:
- "3001:3000"
volumes:
# Replaces the image's shipped configs/config.toml (SQLite-only, fails
# closed on security secrets) — see devportal-config.toml for why.
- ./devportal-config.toml:/app/configs/config.toml:ro
# Same RS256 keypair volume platform-api signs with — the devportal reads
# only jwt_public.pem (0644) from it to verify those tokens.
- platform-api-jwt-keys:/etc/devportal/keys:ro
depends_on:
postgres:
condition: service_healthy
platform-api:
condition: service_healthy
platform-api-jwtkeygen:
condition: service_completed_successfully

# One-shot init container: generates the TLS pair the platform-api HTTPS
# listener requires (the server no longer generates a self-signed fallback).
Expand Down
43 changes: 23 additions & 20 deletions portals/developer-portal/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,14 +141,17 @@ Use this for active development, custom IdP configuration, or when you prefer to

### 2. Use `npm run start:local`, not `npm start`

`configs/config.toml`'s own defaults are wired for the Docker Compose topology (TLS on, pointing at a cert only the containers have, `platform_api.base_url` pointing at the `platform-api` hostname that only resolves inside the compose network). Plain `npm start` inherits those as-is and will fail — there's no `/app` filesystem or bind-mounted cert here. `npm run start:local` (`package.json`) overrides all of it in one place: TLS off, `http://localhost:3000`, and `platform_api.base_url` pointed at `localhost` (see [Local auth](#local-auth) if you're running the Platform API sidecar).
`configs/config.toml`'s own defaults are wired for the Docker Compose topology (TLS on, pointing at a cert only the containers have, `auth.local.platform_api_url` pointing at the `platform-api` hostname that only resolves inside the compose network). Plain `npm start` inherits those as-is and will fail — there's no `/app` filesystem or bind-mounted cert here. `npm run start:local` (`package.json`) overrides all of it in one place: TLS off, `auth.local.platform_api_url` pointed at `localhost`, and `auth.local.public_key_path` pointed at the host-side `resources/keys/` that `scripts/setup.sh` writes rather than the container mount path (see [Local auth](#local-auth) if you're running the Platform API sidecar).

### 3. Configure the Identity Provider (optional)

The portal's login flow requires a valid OAuth2/OIDC provider. Update the `[idp]` block in `configs/config.toml`:
The portal's login flow requires a valid OAuth2/OIDC provider. Set `[developer_portal.auth]` `mode = "idp"` and fill in the `[developer_portal.auth.idp]` block in `configs/config.toml`:

```toml
[idp]
[developer_portal.auth]
mode = "idp"

[developer_portal.auth.idp]
issuer = "https://<your-idp>/oauth2/token"
authorization_url = "https://<your-idp>/oauth2/authorize"
token_url = "https://<your-idp>/oauth2/token"
Expand All @@ -164,7 +167,7 @@ For local exploration you can skip IdP setup by using the Platform API sidecar i

#### SQLite (default — no setup required)

The portal uses SQLite out of the box. The database file is created automatically at the path configured by `database.file` (default: `./devportal.db`). No installation or schema migration step is needed.
The portal uses SQLite out of the box. The database file is created automatically at the path configured by `database.path` (default: `./devportal.db`). No installation or schema migration step is needed.

#### PostgreSQL (optional)

Expand All @@ -179,23 +182,23 @@ docker run --name devportal-postgres \
-d postgres:16
```

Then update the `[database]` block in `configs/config.toml`:
Then update the `[developer_portal.database]` block in `configs/config.toml`:

```toml
[database]
type = "postgres"
[developer_portal.database]
driver = "postgres"
host = "localhost"
port = 5432
name = "devportal"
username = "postgres"
user = "postgres"
password = "postgres"
```

In production, set the password via the `APIP_DP_DATABASE_PASSWORD` environment variable instead of storing it in the config file.

### 5. Seed default organization

The default organization is seeded automatically on startup when `organization.default_name` is set in config (or via `APIP_DP_ORGANIZATION_DEFAULTNAME` env var).
The default organization is seeded automatically on startup when `organization.default_name` is set in config (or via `APIP_DP_ORGANIZATION_DEFAULT_NAME` env var).
No manual step is required.

### 6. Install and run
Expand Down Expand Up @@ -238,16 +241,16 @@ password_hash = "$2y$10$..." # bcrypt hash — generate with: htpasswd -bnBC 1
scopes = "dp:org_manage dp:api_manage ..."
```

The portal config (or `APIP_DP_PLATFORMAPI_*` env vars) must point to the Platform API. `config.toml`'s own defaults assume Docker Compose, where `platform-api` is a resolvable hostname on the compose network — `npm run start:local` already overrides `base_url` to `https://localhost:9243` (the sidecar's port published to the host) and `insecure = true` (self-signed cert), so no manual edit is needed for that flow:
The portal config (or `APIP_DP_AUTH_LOCAL_*` env vars) must point to the Platform API. `config.toml`'s own defaults assume Docker Compose, where `platform-api` is a resolvable hostname on the compose network — `npm run start:local` already overrides `platform_api_url` to `https://localhost:9243` (the sidecar's port published to the host) and `tls_skip_verify = true` (self-signed cert), so no manual edit is needed for that flow:

```toml
[platform_api]
base_url = "https://localhost:9243" # env: APIP_DP_PLATFORMAPI_BASEURL
jwt_private_key = "" # PEM RSA private key that signs portal-minted tokens; must match the Platform API's auth.jwt.public_key — env: APIP_DP_PLATFORMAPI_JWTPRIVATEKEY
insecure = true # Platform API uses a self-signed cert
[developer_portal.auth.local]
platform_api_url = "https://localhost:9243" # env: APIP_DP_AUTH_LOCAL_PLATFORM_API_URL
public_key_path = "/etc/devportal/keys/jwt_public.pem" # path to the Platform API's auth.jwt.public_key PEM — env: APIP_DP_AUTH_LOCAL_PUBLIC_KEY_PATH
tls_skip_verify = true # Platform API uses a self-signed cert
```

Tokens are signed asymmetrically (RS256): the portal signs with the RSA private key above and the Platform API verifies against its `auth.jwt.public_key`. There is no shared HMAC secret the two sides never exchange signing material.
Tokens are signed asymmetrically (RS256): the Platform API mints them with its `auth.jwt.private_key` and the portal verifies them against the matching public key above. There is no shared HMAC secret, and the private key never leaves the Platform API — `scripts/setup.sh` generates the keypair into `resources/keys/`, and `docker-compose.yaml` mounts only `jwt_public.pem`'s directory into the portal (at `/etc/devportal/keys`).

For production, configure an OIDC identity provider per organization instead of local auth.

Expand All @@ -265,12 +268,12 @@ Every config key can be overridden with an `APIP_DP_*` environment variable. You
|---------|-------------|
| `APIP_DP_DATABASE_HOST` | `config.database.host` |
| `APIP_DP_DATABASE_PORT` | `config.database.port` |
| `APIP_DP_TLS_ENABLED` | `config.tls.enabled` |
| `APIP_DP_IDP_CLIENTID` | `config.idp.clientId` |
| `APIP_DP_IDP_ISSUER` | `config.idp.issuer` |
| `APIP_DP_SERVER_BASEURL` | `config.server.baseUrl` |
| `APIP_DP_SERVER_HTTPS_ENABLED` | `config.server.https.enabled` |
| `APIP_DP_IDP_CLIENTID` | `config.auth.idp.clientId` |
| `APIP_DP_IDP_ISSUER` | `config.auth.idp.issuer` |
| `APIP_DP_SERVER_PORT` | `config.server.port` |
| `APIP_DP_DATABASE_SSL_ENABLED` | `config.database.ssl.enabled` |
| `APIP_DP_SERVER_BASE_URL` | `config.server.baseUrl` |
| `APIP_DP_DATABASE_SSL_MODE` | `config.database.sslMode` |

`.env` example:
```dotenv
Expand Down
Loading
Loading