Self-hosted Open Attack Surface Management stack: web console, core API, scan workers, PostgreSQL, Redis, RustFS object storage, and a GeoIP service.
For authorized security testing only. Scan only systems you own or have written permission to test.
| Item | Minimum |
|---|---|
| Docker Engine | 24+ with Compose v2 |
| GNU Make | any (optional, convenience wrapper) |
| CPU | 4 cores |
| RAM | 4 GB (8 GB recommended) |
| Disk | 20 GB free |
The worker mounts /var/run/docker.sock and spawns connector containers on the host daemon, so the host must expose a working Docker daemon to the worker container.
1. Create .env
cp .env.example .env2. Generate the required secrets in .env
Important
The stack will not start until these are set. Generate a separate value for each with:
openssl rand -base64 32| Variable | Value |
|---|---|
ENCRYPTION_KEYS |
A generated value. Encrypts stored credentials: back it up, losing it loses them |
BETTER_AUTH_SECRET |
Another generated value. Signs sessions and email verification links |
OASM_CLOUD_APIKEY |
Another generated value. The cloud-worker key, also used by the bundled worker: anyone who knows it can claim every workspace's scan jobs and read the credentials they carry |
BASE_URL |
Public URL of the console, e.g. https://cloud.oasm.io. Leave empty on localhost |
Before exposing the stack beyond localhost, also change the default Postgres, Redis and RustFS passwords.
3. Start
make # pull latest commit + pull images + startWithout Make (on native Linux, first set DOCKER_GID in .env to the output of stat -c '%g' /var/run/docker.sock, or connector scans such as nuclei fail):
docker compose pull
docker compose up -d| Service | URL |
|---|---|
| Web console | http://localhost:6276 |
| Core API (gRPC) | 127.0.0.1:16276 — bound to loopback, for a local CLI worker or debugging |
| Worker connector gRPC | :26276 on the oasm_connectors network (internal, not published) |
Check readiness:
docker compose ps
curl -f http://localhost:6276/api/health| Container | Image | Role |
|---|---|---|
oasm-console |
oasm/oasm-console |
nginx serving the UI, proxies /api/ to core-api |
oasm-api |
oasm/oasm-api |
Core API + gRPC, gRPC published on 127.0.0.1:16276 |
oasm-migration |
oasm/oasm-api |
One-shot TypeORM migration, must exit 0 before API starts |
oasm-worker |
oasm/oasm-worker |
Scan orchestration in node mode; spawns connector containers |
oasm-postgres |
postgres:18 |
Primary database |
oasm-redis |
redis:alpine |
Queue, cache, rate limiting |
oasm-rustfs |
rustfs/rustfs |
S3-compatible object storage |
geo-ip-database |
ghcr.io/oasm-platform/dbip |
GeoIP lookups, linux/amd64 only |
Named volumes: pgdata18, redis-data, geoip-data, rustfs-data, worker-tools-cache.
All settings come from .env (created from .env.example).
| Variable | Default | Notes |
|---|---|---|
IMAGE_TAG |
latest |
Image tag for all oasm/* images |
OASM_CLOUD_APIKEY |
— | Required. Cloud-worker key: core-api accepts it at POST /api/workers/join. A random value of 32+ characters (openssl rand -base64 32); the stack refuses to start without it. Anyone who knows it can claim the scan jobs of every workspace in this stack and read the decrypted credentials they carry |
WORKER_API_KEY |
= OASM_CLOUD_APIKEY |
Key the bundled worker joins with. It must be a value core-api accepts — today that means the cloud key or a console-issued join key — so the default keeps the two in sync |
WORKER_REPLICAS |
3 |
Number of oasm-worker replicas started by make |
WORKER_NAME |
oasm-builtin-worker |
Name the bundled workers show in the console instead of their container ID. All replicas share it |
DOCKER_GID |
0 |
Group owning /var/run/docker.sock, so the non-root worker can start connector containers. make detects it; set it by hand for plain docker compose on native Linux |
POSTGRES_HOST |
postgres |
|
POSTGRES_USERNAME |
postgres |
|
POSTGRES_PASSWORD |
postgres |
Change before exposing anything |
POSTGRES_PORT |
5432 |
|
POSTGRES_DB |
open_asm |
|
POSTGRES_SSL |
false |
Set true for managed Postgres |
PORT |
6277 |
Core API listen port inside the container network |
REDIS_URL |
redis://:open_asm@redis:6379/0 |
Must match REDIS_PASSWORD |
REDIS_PASSWORD |
open_asm |
|
BETTER_AUTH_SECRET |
— | Required. Signs sessions and email verification links; a random value of 32+ characters (openssl rand -base64 32). Newer images won't start without it |
BASE_URL |
— | Public URL of the console, origin only (e.g. https://cloud.oasm.io); used for email verification links and the Telegram webhook |
TRUSTED_PROXIES |
— | Comma-separated IPs/CIDRs of reverse proxies in front of core-api (e.g. the oasm_net subnet behind a tunnel). core-api then takes the client IP from X-Forwarded-For for rate limits, sessions and worker IPs. List only addresses clients cannot connect from |
ENCRYPTION_KEYS |
— | Required. Comma-separated keys; last key encrypts, earlier keys decrypt only. core-api won't start unless the last key is random and 32+ characters (openssl rand -base64 32) |
GEO_IP_URL |
geo-ip-database:4360 |
|
RUSTFS_ENDPOINT |
http://rustfs:9000 |
|
RUSTFS_ACCESS_KEY |
rustfsadmin |
|
RUSTFS_SECRET_KEY |
rustfssecret |
Notes:
BETTER_AUTH_SECRETmust stay private and stable: anyone who knows it can forge sessions and verification links, and changing it signs every user out.OASM_CLOUD_APIKEYis the highest-value secret in this stack afterENCRYPTION_KEYS: the worker that joins with it is cloud-scoped, so it claims jobs — and receives decrypted connector config and repository tokens — for every workspace in the deployment, not just one. Until core-api can enrol a worker into a single workspace, run one stack per tenant, keep the key out of images, logs and tickets, and rotate it by generating a new value and recreating core-api and the workers together.ENCRYPTION_KEYSis a secret store of last resort: losing every key makes stored credentials unrecoverable. Keep a backup outside this host.- Rotating: append the new key at the end and restart; core-api re-encrypts stored secrets with it at boot. Drop an old key only once the core-api log says no stored secret needs it.
- Stacks set up before
ENCRYPTION_KEYSreached core-api stored credentials under a built-in default key, whatever.envsaid. Set a new key and restart: that data stays readable and is re-encrypted with the new key. - The compose file's
POSTGRES_*and Redis defaults apply only when a variable is unset;.envalways wins.
| Target | Action |
|---|---|
make / make all |
git pull + docker compose pull + up -d |
make pull |
Pull images |
make run |
Start services without pulling |
make update |
Pull latest commit, pull images, recreate services |
make pull-latest-commit |
git pull only |
make update-main |
Pull and recreate main services |
make down |
docker compose down --remove-orphans |
docker compose logs -f # all logs
docker compose logs -f core-api # one service
docker compose up -d --force-recreate # apply .env changes
docker compose down # stop, keep data
docker compose down -v # stop and DELETE all volumesPlain docker compose down keeps data. down -v destroys databases, object storage, GeoIP data, and the tool cache irreversibly.
make update # or: docker compose pull && docker compose up -dThe migration service runs on every start and is idempotent. If it exits non-zero the API will not start — inspect docker compose logs migration.
Experimental — first tested on OASM v0.91. Back up before you start.
Postgres now runs on postgres:18 with a new volume, pgdata18, mounted at /var/lib/postgresql (the 18 image keeps its data in /var/lib/postgresql/18/docker). The old PG17 volume <project>_pgdata is not mounted any more and is never modified. PG18 cannot read PG17 data files, so after updating the stack comes up with an empty database until you restore the old data into it.
The steps below work whether or not you have already updated, because they dump straight from the old volume using a temporary PG17 container.
# 0. Update to the PG18 compose file and let the stack start once
# (creates pgdata18 and an empty open_asm schema)
make update
# 1. Find the old PG17 volume (usually <project>_pgdata, e.g. oasm-docker_pgdata)
docker volume ls | grep pgdata
OLD_VOLUME=oasm-docker_pgdata
# 2. Dump open_asm from the old volume with a temporary PG17 container
docker run -d --name pg17-restore -e POSTGRES_PASSWORD=x \
-v "$OLD_VOLUME":/var/lib/postgresql/data postgres:17
until docker exec pg17-restore pg_isready -U postgres -q; do sleep 1; done
docker exec pg17-restore pg_dump -U postgres -Fc open_asm > open_asm_pg17.dump
docker rm -f pg17-restore
# 3. (optional) Back up the current PG18 database
docker compose exec -T postgres pg_dump -U postgres -Fc open_asm > open_asm_pg18.dump
# 4. Stop everything that writes to the database
docker compose stop console core-api oasm-worker
# 5. Replace open_asm in PG18 with the PG17 data
docker compose exec -T postgres psql -U postgres -d postgres \
-c "DROP DATABASE open_asm WITH (FORCE)" -c "CREATE DATABASE open_asm"
docker compose exec -T postgres pg_restore -U postgres -d open_asm --no-owner < open_asm_pg17.dump
# 6. Start the stack again; migration should report "No migrations are pending"
docker compose up -d
docker compose logs migration --tail 5Notes:
- Replace
postgres/open_asmif you changedPOSTGRES_USERNAME/POSTGRES_DBin.env. - On Windows Git Bash, prefix the
docker runin step 2 withMSYS_NO_PATHCONV=1so the volume path is not rewritten. - Check the data is back, e.g.
docker compose exec -T postgres psql -U postgres -d open_asm -c "select count(*) from assets", then reload the console. - Keep the old volume for a few days. Once you are satisfied:
docker volume rm "$OLD_VOLUME". To roll back instead, revert topostgres:17with thepgdata:/var/lib/postgresql/datamount — the old volume is unchanged.
State lives in named volumes. Back up Postgres at minimum:
docker compose exec postgres pg_dump -U postgres open_asm > backup.sql- Private bridge network
oasm_net; only6276is published on every interface.16276(gRPC) is bound to127.0.0.1for local workers and debugging — the replicas reach core-api overoasm_net, so it does not need to be public. - The console proxies
/api/tocore-api:${PORT}(seenginx.conf) — changePORTand the console upstream must change with it. - Spawned connector containers join the separate
oasm_connectorsnetwork (WORKER_CONNECTOR_NETWORK) and dial the worker replica that started them on its own IP, port26276. They are not onoasm_net, so they cannot reach postgres or redis. oasm-workerrunsWORKER_REPLICASreplicas (default3), somake/docker compose up -dstarts three workers. Override once withdocker compose up -d --scale oasm-worker=N. Nothing is published per replica, so they never collide.WORKER_MAX_CONCURRENCYapplies per replica — lower it (or setcpus/mem_limit) when several share one host.
/var/run/docker.sockis mounted intooasm-workerand is root-equivalent on the host. Run only trusted images and treat the worker as privileged.- The image runs as uid/gid
1000;group_add: ['0']grants socket access. On a host with a dedicateddockergroup, use that gid instead. - Change every default in
.env— Postgres password, Redis password and RustFS keys — and setENCRYPTION_KEYS,BETTER_AUTH_SECRETandOASM_CLOUD_APIKEYbefore exposing the stack beyond localhost. - The GeoIP image is pinned to
linux/amd64; on ARM hosts it runs under emulation or not at all.