Enabling auth-services on a running MainNet participant for party-scoped private contract queries?

Hi everyone,

We’re building a governance dApp (DAOs, proposals, voting) on Canton MainNet with a self-hosted participant (web34ever::12205...). Go backend talks to the participant via gRPC Ledger API.

We’re adding private contracts where the operator party is excluded from observers:

template DAO with
    admin      : Party
    members    : [Party]
    visibility : DAOVisibility
  where
    signatory admin
    observer case visibility of
              Public  -> [operator] <> members
              Private -> members   -- operator excluded

From researching this forum (thanks WallaceKelly & bernhard — your posts on JWT authorization, actAs/readAs semantics, and PQS vs JSON API were exactly what we needed), we understand that:

  • Per-party JWT with actAs=[memberParty] is required to query private contracts — operator readAs won’t work
  • actAs implies readAs, so a single right grant is sufficient
  • JSON API is fine for our scale (~50-200 DAOs), no need for PQS yet
  • Ledger API User creation via /v2/users + /v2/users/{id}/rights is the right pattern

Two remaining questions:

1. Adding auth-services to an already-running MainNet participant

We did not configure ledger-api.auth-services when we initially set up our participant. We plan to add:

canton.participants.web34ever {
  ledger-api {
    auth-services = [{
      type = jwt-rs-256-crt
      certificate = "/path/to/our-public.crt"
    }]
  }
}

Our backend would sign JWTs with the corresponding private key, including actAs=[partyId] claims for each user.

Are there any gotchas with enabling auth on a participant that already has existing contracts and allocated parties? Does a restart with auth-services added require any migration, or is it purely additive?

2. Coexistence with Splice validator stack

We’re also preparing for Splice validator onboarding (splice-validator-app + splice-wallet-app). Does the Splice stack configure its own auth on the participant (e.g. Keycloak)? If so, can multiple auth-services entries coexist — ours (cert-based) alongside Splice’s (JWKS)?

auth-services = [
  { type = jwt-rs-256-crt, certificate = "our-key.crt" },       # our backend
  { type = jwt-jwks, url = "http://keycloak:8080/.../jwks" }     # splice stack
]

Or would we need to consolidate into a single auth provider?

Any pointers appreciated. Happy to share more about our contract model if helpful.

Thanks!

hey @web3
so on above, some pointers I’d give is

  • Canton stores auth configuration at the process level so your contracts, parties, and transaction history live in the participant’s database and are completely unaffected by adding auth-servicesAFIK the change affects only how the Ledger API authenticates incoming requests going forward.

  • the user Identity management layer is separate from auth-services configuration. The users and their actAs/readAs rights you’ve created persist in the participant DB. Auth-services just controls how the participant verifies that the JWT presented actually belongs to the claimed user. Your existing user/rights setup carries over intact.

  • When you onboard with the Splice the validator app configures its own auth against the participant’s Ledger API using the OIDC provider you specify at onboarding time (Auth0 or Keycloak). Splice expects to be the configured auth provider by default in its Docker Compose config. It will set the ledger-api-auth Kubernetes secret or the LEDGER_API_AUTH_* env vars which typically overwrite your configuration if you’re using the Splice deployment tooling.

  • also Recommended is to set Splice’s splice-app-validator-ledger-api-auth secret to point to your Keycloak instance but ensure the participant itself has both auth entries configured in your own config file, not overridden by Splice or your custom auth.

Your setup looks correct.

  1. Adding auth-services later

Yes — you can add ledger-api.auth-services to an existing MainNet participant without migration.

You just:

  • update config
  • restart participant
  • start sending JWTs

Existing:

  • contracts
  • parties
  • data

will continue working normally.

Main thing to remember:
after enabling auth, every Ledger API request must include a valid JWT.

Your per-party JWT approach with:

actAs=[partyId]

is the correct design for private contracts.


  1. Splice + your auth together

Yes — multiple auth-services can coexist.

Example:

auth-services = [
  { type = jwt-rs-256-crt, certificate = "our.crt" },
  { type = jwt-jwks, url = "http://keycloak/.../jwks" }
]

This is supported.

So:

  • your backend can use cert-signed JWTs
  • Splice can use Keycloak/JWKS

at the same time.

Just make sure JWT claims (actAs, aud, expiration, etc.) are configured correctly.


One important note:

If a contract is private and operator is excluded:

Private -> members

then operator truly cannot read/query it.

So your backend must always issue JWTs for the actual member party when accessing private contracts.

Your architecture already matches this properly.

Thanks @Jatin_Pandya_cf and @evoandro — both replies cleared up exactly
what we needed.

Phase 1 update — backend JWT signer landed. The signer is in main
and unit-tested; we minted a token for one of our member parties and
verified the wire format is what Canton expects:

{
  "alg": "RS256", "typ": "JWT"
}
{
  "sub": "ledger-api-user",
  "aud": ["https://daml.com/jwt/aud/participant/web34ever::1220…"],
  "exp": <unix>, "iat": <unix>,
  "https://daml.com/ledger-api": {
    "ledgerId": null,
    "applicationId": "syncvotes",
    "actAs": ["dao-ethereum::1220…"]
  }
}
  • RS256 signed against an X.509 cert we’ll add to
    auth-services.certificate in Phase 2.
  • Token is 856 bytes — comfortably fits the Authorization header.
  • Library: github.com/golang-jwt/jwt/v5, the URL-keyed claim is
    encoded literally so Canton’s parser picks it up by string match.
  • Smoke-tested against the MainNet participant (auth-services not yet
    required) — request authenticates fine and the JWT isn’t rejected
    for malformed shape. Side-channel sanity check before Phase 2 makes
    it actually mandatory.

Quick recap of how we’ll apply the rest of the guidance:

  • Adding auth-services is non-destructive. Backend signing rolls
    first; participant restart flips to required-auth so every Ledger
    API call already carries a valid token by the time it’s mandatory.
  • Per-party JWT with actAs=[memberPartyId] for private contracts.
    Backend signs a short-lived (60s) JWT per request; operator
    genuinely cannot see Private DAOs at the ledger.
  • Splice coexistence. Two auth-services entries (cert-based for
    our backend, JWKS for Splice). We’ll point
    splice-app-validator-ledger-api-auth at our IdP so Splice
    onboarding doesn’t overwrite the participant’s config.

One open question: we’re holding off on the participant restart
until our DSO-side FeaturedAppRight lands — there are still pieces
of state we want to settle (party ↔ user rights, DAR vetting status
on the new packages) before we tighten auth across the API. Best
guess: ~2–3 weeks. Will report back here when Phase 2 ships.

Related, different layer: the host-validator trust problem (our parties
all live on a single participant today) is tracked separately at
canton-foundation/canton-dev-fund#70
— composing with BitSafe’s Decentralization Manager (PR
#298)
for multi-hosted parties.

Thanks again.