Skip to content
FlowOS Guide
Esc
↑↓navigate↵open⌘Jpreview
On this page

Entra sign-in certificate

The certificate apps/web signs in to Entra with, one per environment, and how to replace it.

apps/web authenticates to Microsoft Entra ID with a certificate, not a client secret. It signs a private-key JWT with ENTRA_CLIENT_PRIVATE_KEY and names the certificate in the JWT’s x5t header with ENTRA_CLIENT_CERT_THUMBPRINT. Entra accepts the sign-in only when that thumbprint belongs to a certificate uploaded to the app registration and the key is that certificate’s private key.

One certificate per environment

Each Heizen Studio environment carries its own pair:

Studio environment Used by Certificate subject
dev-aws the AWS dev box CN=flowos-dev-aws
development pnpm dev on a laptop CN=flowos-local-dev

The app registration holds all of them at once. A key copied off a laptop signs in only as local development, and its certificate is removed without touching dev-aws.

Both values come from Studio and nowhere else. A line in apps/web/.env.local, or in /opt/flowos/.env on the box, shadows the Studio value, and a key from the file paired with a thumbprint from Studio fails every sign-in. Neither file holds ENTRA_CLIENT_PRIVATE_KEY.

Replacing a certificate

Replace a certificate before it expires, and at once for every environment whose key has been exposed. Keys stay outside the repository and never reach a terminal’s output.

  1. Generate the key and certificate in a private folder outside the repository:

    D=~/.flowos-secrets/entra-<env>-<date>
    mkdir -p "$D" && chmod 700 "$D" && cd "$D"
    openssl req -x509 -newkey rsa:2048 -nodes -sha256 -days 365 \
      -subj "/CN=flowos-<env>" -keyout key.pem -out cert.pem
    chmod 600 key.pem

    The certificate is valid for a year, and Entra refuses it after that.

  2. Upload the certificate to the FlowOS app registration, beside the current one:

    az login --tenant <tenant-id> --allow-no-subscriptions
    az ad app credential reset --id <client-id> --append --cert "@$D/cert.pem"

    --append is required. Without it, credential reset replaces every credential on the app, including the certificates the other environments sign in with. <tenant-id> and <client-id> are ENTRA_TENANT_ID and ENTRA_CLIENT_ID in Studio; sign in with an account in the client’s tenant that can manage the app registration. Uploading cert.pem under Certificates & secrets → Certificates in the Azure portal does the same.

  3. Set both values in Studio, in that environment, in one sitting — a thumbprint and a key from different pairs fail every sign-in.

    • ENTRA_CLIENT_CERT_THUMBPRINT is the certificate’s SHA-1 thumbprint, base64url-encoded:

      openssl x509 -in cert.pem -outform DER | openssl dgst -sha1 -binary \
        | openssl base64 | tr '+/' '-_' | tr -d '='
    • ENTRA_CLIENT_PRIVATE_KEY is the key, copied without printing it. Studio keeps a multi-line PEM intact; the key on one line, with its line breaks removed, works as well and survives a paste box that mangles line breaks:

      tr -d '\r\n' < key.pem | pbcopy

      A key with its line breaks escaped as \n is rejected by jose.

  4. Restart what reads them. Both values are read when the process starts.

    • dev-aws: recreate web on the image it already runs, over SSM (the box has no SSH). The next deploy does the same.

      cd /opt/flowos
      IMAGE=$(docker inspect flowos-deploy-web-1 --format '{{.Config.Image}}')
      ECR_REGISTRY=${IMAGE%%/*} IMAGE_TAG=${IMAGE##*:} \
        docker compose -f docker-compose.deploy.yml up -d --no-deps --force-recreate web
    • Local development: restart pnpm dev.

    Then sign in on each.

  5. Remove the old certificate once every environment signs in with its new one. The old key stops working at this step and not before.

    az ad app credential list --id <client-id> --cert \
      --query '[].{name:displayName, keyId:keyId, thumbprint:customKeyIdentifier, expires:endDateTime}' -o table
    az ad app credential delete --id <client-id> --cert --key-id <old-key-id>

    thumbprint there is the hex form; openssl x509 -in cert.pem -noout -fingerprint -sha1 prints the same for a local certificate. Then delete the local folder with trash "$D".

Checking what Studio holds

Run a check through flowos-secrets from the repository root, not from apps/web. Inside a package directory the check also reads that package’s .env.local, so a stale key there is what it reports.

Was this page helpful?