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 actAsimpliesreadAs, 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}/rightsis 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/readAsrights 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-authKubernetes secret or theLEDGER_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-authsecret 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.
- 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.
- 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.certificatein 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-servicesis 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-servicesentries (cert-based for
our backend, JWKS for Splice). We’ll point
splice-app-validator-ledger-api-authat 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.