Skip to content

Operations

Going live

The operator runbook: create the Slack app, connect a directory, start the program, and what to add before signup takes real traffic.

This is the runbook for turning a deployment into a service a customer can use: create the Slack app, point the deployment at a directory, and start the program. It is written for the person who owns the deployment, not for the customer.

Everything here is read-only towards the customer's systems. The agent reads their directory and their findings; it never writes to their IdP, their cloud or their repos.


1. Create the Slack app

The console can show a "Connect Slack" button, but only one Slack app exists per deployment: every customer installs your app into their workspace, and the bot token that comes back is stored per workspace, sealed.

  1. Go to https://api.slack.com/apps → Create New App → From an app manifest.
  2. Pick the workspace you want to develop against (any workspace you can administer).
  3. Paste this manifest, replacing SERVICE_URL with the deployment's origin (the Cloud Run URL from terraform output service_url, or your custom domain):
{
  "display_information": {
    "name": "CISO Express",
    "description": "Your virtual CISO: runs the security program and asks the right people the right questions.",
    "background_color": "#0b1020"
  },
  "features": {
    "bot_user": { "display_name": "CISO Express", "always_online": true },
    "app_home": {
      "home_tab_enabled": false,
      "messages_tab_enabled": true,
      "messages_tab_read_only_enabled": false
    }
  },
  "oauth_config": {
    "redirect_urls": ["SERVICE_URL/api/slack/oauth/callback"],
    "scopes": {
      "bot": [
        "app_mentions:read",
        "chat:write",
        "im:write",
        "im:history",
        "team:read",
        "users:read",
        "users:read.email"
      ]
    }
  },
  "settings": {
    "event_subscriptions": {
      "request_url": "SERVICE_URL/api/webhooks/slack/events",
      "bot_events": ["app_mention", "message.im"]
    },
    "interactivity": {
      "is_enabled": true,
      "request_url": "SERVICE_URL/api/webhooks/slack/interactions"
    },
    "org_deploy_enabled": false,
    "socket_mode_enabled": false,
    "token_rotation_enabled": false
  }
}
  1. Basic Information → App Credentials: copy the Client ID, Client Secret and Signing Secret.
  2. Install App: you can install it into your own workspace first to try it. Every customer installs it themselves from their console.

Why these scopes

Scope Why it is needed
app_mentions:read The agent answers when somebody mentions it in a channel.
chat:write Sends the DMs that do the actual work, and the channel summary.
im:write, im:history Opens the DM conversation and reads the reply.
users:read, users:read.email Builds the roster from Slack: without it the agent cannot match a person to a Slack id, and cannot DM anybody.
team:read Records which workspace an install belongs to, so inbound events resolve to the right tenant.

No admin scopes, no channels:history, no ability to read message content outside a conversation the agent is part of. If a customer asks what the app can see, the honest answer is "the DMs it is in, the channels it is mentioned in, and the public profile of their people".

Point the deployment at it

cd deploy/terraform
terraform apply \
  -var 'slack_client_id=1234567890123.1234567890123' \
  -var 'slack_client_secret=...' \
  -var 'slack_signing_secret=...'

Or set TF_VAR_slack_client_secret / TF_VAR_slack_signing_secret in the environment so the values never touch a file, and slack_client_id in terraform.tfvars.

Both secrets land in Secret Manager and are read by the service at boot. Without them the service still runs, and the console says "Slack app is not configured for this deployment" — a state the browser suite tests, because a dead button would be worse.

Rotating the signing secret: the per-workspace copy is taken at install time. After rotating in Slack, customers have to reinstall (the console's Setup page has the button) for their inbound webhooks to verify again.


2. Connect a directory

The agent can only ask the right person if it knows who works there. Pick whichever the customer already has, and give the agent read-only credentials:

Directory Credential What to create
Okta API token A read-only admin token (Security → API → Tokens). Read-only scopes are enough.
Entra ID App registration An app with User.Read.All, plus a client secret. Read-only.
Google Workspace Service account A service account with domain-wide delegation for https://www.googleapis.com/auth/admin.directory.user.readonly, and the email of an admin to impersonate.

Enter them on the console's Connectors page (they are sealed with AES-256-GCM before they touch storage, and the API only ever reports which secrets exist). Then run a sync from Setup → Import your people.

Slack is also a directory: with the Slack app installed, users.list gives the agent the ids it needs to DM people. A customer with only Slack gets a working roster; the report says which sources it read.

What a sync does, and what it refuses to do

  • Matches a person on the directory's own id first, then on email — a rename or a mailbox change updates the record instead of creating a second one.
  • Re-derives a role it guessed from a job title; never overwrites a role a human set.
  • Deactivates a leaver the directory reports.
  • Deactivates people it can no longer see only when the source finished reading. A revoked token or a page limit means absence proves nothing, so the roster is left alone.
  • Retires the placeholder roster a new workspace starts with, role by role, as real people replace them.
  • Leaves hand-entered people alone: a sync cannot prove somebody typed in by hand has left.

3. Start the program

From the console, Setup → Review the roster to correct the roles the agent inferred (this is the step that makes the rest trustworthy), then Start the program:

  • one control, one owner, one specific question — the catalog's own question for that control, not a generated one;
  • a seven-day cooldown per question, so re-running is safe;
  • controls with no plausible owner produce exactly one message, to the security lead;
  • run it with Preview only on first: it composes the asks and delivers nothing.

Replies arrive in Slack, are interpreted by the rules engine, and become evidence attached to the control — visible on the Program checklist.

Autonomy

A kickoff started by a human delivers even when the workspace is in suggest_only, because that setting exists to stop the sweep acting unsupervised and queueing drafts when somebody presses "start" would make the button a lie. The scheduled sweep still respects it. Move the workspace to act_and_report when the routing looks right.


4. Self-serve signup

The marketing site creates workspaces through POST /api/signup: rate limited to five per address per hour, never taking a slug from the caller, always provisioning a clean workspace (never the demo data), returning the API key once.

Two things to add before pointing real traffic at it:

  1. Email verification — today anybody can type somebody else's address; the workspace is still theirs, but you cannot follow up, and a typo is unrecoverable because the key is shown once.
  2. A captcha (Turnstile/hCaptcha) and a shared rate-limit store. The current limit is in-process, so it holds for one Cloud Run instance and not across a fleet — the same caveat as the workspace cache in docs/architecture.md.

5. Check it worked

# The service is up and knows what it can do
curl -s "$SERVICE_URL/api/health" | jq

# A workspace can be created (do this against a staging deployment)
curl -s -X POST "$SERVICE_URL/api/signup" \
  -H 'Content-Type: application/json' \
  -d '{"email":"you@example.com","company":"Acme"}' | jq '{slug: .tenant.slug, console: .consolePath}'

Then, in the console for that workspace: Setup shows four steps, Overview shows zero issues and full control coverage, and the Program checklist shows the controls that are waiting on a human.

Source: docs/going-live.md in the repository.