Skip to main content

Developers Windmill

Default Keycloak realm templates

Windmill loads the default tenant and election-event Keycloak realm templates from the private S3-compatible object store. The object keys are configured with the following environment variables and are relative to AWS_S3_BUCKET:

VariableDevelopment value
KEYCLOAK_TENANT_REALM_CONFIG_S3_KEYdefaults/keycloak/tenant-90505c8a-23a9-4cdf-a26b-4e19f6a097d5.json
KEYCLOAK_ELECTION_EVENT_REALM_CONFIG_S3_KEYdefaults/keycloak/tenant-90505c8a-23a9-4cdf-a26b-4e19f6a097d5-event-33f18502-a67c-4853-8333-a58630663559.json

Keys must not start with /. The files in .devcontainer/keycloak/import/ are the source of truth. In development, remote-compose, and air-gapped deployments, configure-minio uploads both templates to the private bucket before Windmill starts. It overwrites same-key objects each time it runs, so the logs.reset.minio VS Code task re-provisions templates after local edits. Keycloak mounts the source directory directly for its startup realm import.

Production provisioning

Upload both realm templates before deploying a Windmill version that uses the S3 key variables. In AWS, AWS_S3_BUCKET is normally the private logical prefix inside the physical client bucket.

export PRIVATE_BUCKET="<physical-client-bucket>"
export PRIVATE_PREFIX="election-event-documents"
export KEYCLOAK_TENANT_REALM_CONFIG_S3_KEY="defaults/keycloak/tenant-90505c8a-23a9-4cdf-a26b-4e19f6a097d5.json"
export KEYCLOAK_ELECTION_EVENT_REALM_CONFIG_S3_KEY="defaults/keycloak/tenant-90505c8a-23a9-4cdf-a26b-4e19f6a097d5-event-33f18502-a67c-4853-8333-a58630663559.json"

PRIVATE_ROOT="s3://${PRIVATE_BUCKET}"
if [ -n "$PRIVATE_PREFIX" ]; then
PRIVATE_ROOT="${PRIVATE_ROOT}/${PRIVATE_PREFIX}"
fi
aws s3 cp \
.devcontainer/keycloak/import/tenant-90505c8a-23a9-4cdf-a26b-4e19f6a097d5.json \
"${PRIVATE_ROOT}/${KEYCLOAK_TENANT_REALM_CONFIG_S3_KEY}"
aws s3 cp \
.devcontainer/keycloak/import/tenant-90505c8a-23a9-4cdf-a26b-4e19f6a097d5-event-33f18502-a67c-4853-8333-a58630663559.json \
"${PRIVATE_ROOT}/${KEYCLOAK_ELECTION_EVENT_REALM_CONFIG_S3_KEY}"

Verify each upload against the physical bucket. The object key is the logical prefix, when present, followed by the configured realm key.

TENANT_OBJECT_KEY="${KEYCLOAK_TENANT_REALM_CONFIG_S3_KEY}"
EVENT_OBJECT_KEY="${KEYCLOAK_ELECTION_EVENT_REALM_CONFIG_S3_KEY}"
if [ -n "$PRIVATE_PREFIX" ]; then
TENANT_OBJECT_KEY="${PRIVATE_PREFIX}/${TENANT_OBJECT_KEY}"
EVENT_OBJECT_KEY="${PRIVATE_PREFIX}/${EVENT_OBJECT_KEY}"
fi

aws s3api head-object --bucket "$PRIVATE_BUCKET" --key "$TENANT_OBJECT_KEY"
aws s3api head-object --bucket "$PRIVATE_BUCKET" --key "$EVENT_OBJECT_KEY"

For an existing deployment, replace KEYCLOAK_TENANT_REALM_CONFIG_PATH and KEYCLOAK_ELECTION_EVENT_REALM_CONFIG_PATH with the corresponding *_S3_KEY variables. Upload the objects first, then deploy the Step, beyond, and gitops configuration changes together. The Windmill error for a missing object names the configured bucket and key.

Login hint prefill configuration

Voting Portal notification links can carry bounded login_hint__<field> query parameters on tenant/event /login and /enroll routes. The portal validates the complete hint set, removes it from browser history, and appends accepted values to the URL generated by keycloak-js. A username hint is also sent as the standard OIDC login_hint.

Keycloak stores the additional authorization-request parameters as client notes. The voter-enrollment provider accepts at most five hints, with field names matching [A-Za-z0-9._-]+, names no longer than 128 characters, and values no longer than 255 characters. Any invalid hint rejects the complete set.

A pre-matching request validator in the voter-enrollment provider reads the raw query of the auth and registrations authorization endpoints and rejects an invalid hint set with HTTP 400 before Keycloak parses or stores it. It sees duplicate parameters and malformed percent escapes that framework decoding would otherwise discard, so callers that bypass the Voting Portal are held to the same contract. Hint values never appear in the error response.

The validator covers the query string only. Hints delivered inside a signed request object or a pushed authorization request are validated later, from the stored client notes, where Keycloak has already applied its own additional parameter limits — an over-limit set can therefore be truncated rather than rejected on those paths. Step's Voting Portal uses neither, but a deployment that introduces request objects or PAR should re-check this boundary.

Stock registration flow

In each realm that uses Keycloak's stock registration form:

  1. Open Authentication and duplicate or edit the active registration flow.
  2. Add the Sequent: Login hint registration prefill execution.
  3. Set its requirement to Required.
  4. Place it before the execution that creates the registration user.
  5. Bind the updated registration flow to the realm.

The action applies hints only while rendering the initial GET form. It intersects them with declarative user-profile metadata and passes only managed, writable attributes to the form. Submitted POST values always take precedence.

Deferred registration flow

The Deferred Registration User Profile Creation execution has a Prefill Parameters Policy setting:

ValueBehavior
IGNOREDefault. Do not prefill fields from login hints.
ACCEPTPrefill managed, writable fields on the initial GET form.

Set ACCEPT only in realms where notification-link prefill is an approved workflow. Password fields, unmanaged or read-only fields, hidden profile attributes, and the configured verification-status attribute are never prefilled. Redirect-to-registration flows retain the same authentication session and therefore require no separate hint configuration.

Realm template rollout

Update the source realm JSON files under .devcontainer/keycloak/import/ so new tenant and election-event realms receive the intended flow configuration. Then upload the templates using the S3 provisioning process above. Existing realms are not changed by replacing the default template: update or re-import their authentication flow configuration separately and verify both direct enrollment and login-to-registration redirects.

Treat every hint as untrusted, user-editable input. Prefill must not mark email, phone, identity, eligibility, or other verification state as trusted. Do not log hint values. Never place passwords, one-time passwords (OTPs), tokens, secrets, government identifiers, or other inappropriate sensitive values in notification URLs. URL encoding prevents query-structure injection but does not make values confidential or authentic. Dynamic attributes with dots or dashes in their names must be read with the Handlebars lookup helper; attributes is reserved for the complete attribute map.

Tally

Discarded/Auditable Ballots

Discarded Ballots that for some reason are not included in the tally. Possible reasons:

  • The voter was disabled.
  • The voter was deleted.
  • The ballot was cast outside the Voting Period.
  • The voter is not authorized for the election of the ballot.
  • The voter is not assigned to the area of the ballot.
  • The ballot is from a previous revote (only the last vote counts). (Note, this is not counted as a discarded ballot yet).

Eligible Voters

Eligible voters are voters that can vote. Voters not included here are disabled and deleted voters.