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:
| Variable | Development value |
|---|---|
KEYCLOAK_TENANT_REALM_CONFIG_S3_KEY | defaults/keycloak/tenant-90505c8a-23a9-4cdf-a26b-4e19f6a097d5.json |
KEYCLOAK_ELECTION_EVENT_REALM_CONFIG_S3_KEY | defaults/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:
- Open Authentication and duplicate or edit the active registration flow.
- Add the Sequent: Login hint registration prefill execution.
- Set its requirement to Required.
- Place it before the execution that creates the registration user.
- 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:
| Value | Behavior |
|---|---|
IGNORE | Default. Do not prefill fields from login hints. |
ACCEPT | Prefill 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.