Skip to main content

Connecting Custos

This page takes a system administrator through running Apache Airavata Custos and connecting it to a Slurm cluster. Custos is Cybershuttle's security component and our view of what a control plane for HPC operators should look like: it turns allocation decisions into cluster accounts, Slurm associations and usage charges; see Managing allocations.

Ongoing Every step is implemented on master of apache/airavata-custos and exercised only in development deployments. Once connected, Custos writes Slurm accounts and associations and, on enrolled nodes, changes how sshd authenticates. Rehearse on a cluster where that is acceptable.

The first three rows are needed by every connection; each other row only by the connection it names.

PrerequisiteFor
A host with Go 1.25+, Node 22+ and pnpmBuilding and running the server and portal
PostgreSQL (development uses 17)Custos state; a second database for the signer
An OpenID Connect issuer: CILogon or the centre's ownSign-in to the portal and API
ACCESS AMIE site code and API keyAllocations from ACCESS
COmanage Registry with the UnixCluster plugin, publishing to LDAPPOSIX accounts and groups
slurmrestd on slurmdbd, with JWT authenticationSlurm accounts, associations, usage
RHEL 9 or 10 login nodes; Ansible 2.14+Node enrolment
Vault or OpenBao with KV version 2The SSH certificate signer only

Components​

A deployment spans these components:

ComponentRuns on
Custos server, with every connector compiled inA service host
Portal (Next.js)A service host, behind TLS
PostgreSQLA database host
OIDC issuerCILogon, or the centre's
COmanage Registry and LDAPThe centre
slurmrestdThe cluster's management host
PAM module and SSSDLogin nodes
SSH certificate signer (optional)A service host, separate process

The server's source is cmd/server, the portal's web/, the PAM module's extensions/CILogon-SSH-PAM with the playbook dev-ops/account-provisioning, and the signer's extensions/SSH-Certificate-Signer.

The repository ships source only, with no container image or service unit. dev-ops/compose (loopback, password admin, Keycloak start-dev, Vault without TLS), dev-ops/local-amie and dev-ops/local-slurm (Slurm 24.05.5) are for development; dev-ops/terraform/aws deploys only Keycloak and Vault. The centre builds the production deployment.

The server speaks plain HTTP with no TLS option; put it and the portal behind a TLS proxy. The cluster never connects to the server, and the optional signer talks only to Vault and its own database.

The diagram shows the connections between components; each arrow starts at the side that opens the connection.

A firewall needs these rules:

FromToFor
BrowserPortalSign-in and administration
PortalServer, port 8080Every API call, with the user's bearer token
ServerPostgreSQLCustos state
ServerOIDC issuerDiscovery and keys; required to start
ServerAMIE APIPackets and replies
ServerCOmanage RegistryPeople, groups, identifiers, cluster accounts
ServerslurmrestdAccounts, associations, jobs
Login nodesLDAP (636); CILogon (443)Account lookup; device-code sign-in
SignerVault; PostgreSQLCA keys; issuance records

Start the server and portal​

  1. Create a PostgreSQL database custos and a role that owns it. The server creates and migrates its tables at every start.

  2. Register the portal with the OIDC issuer as a confidential client with the redirect URI https://<portal host>/api/auth/callback/oidc. The portal requests openid email profile; Custos needs the sub and email claims.

  3. Write the configuration file, starting from the repository's config/custos.yaml:

    • Set enabled: false on every connector for now; each needs a cluster record and credentials made later.
    • Set core.auth.oidc.audience to the aud of the token the portal forwards: the access token when it is a JWT, otherwise the ID token, whose audience is the portal's client ID.
  4. Build and start the server, with the environment variable CUSTOS_BOOTSTRAP_ADMIN_EMAIL set to the first administrator's e-mail address; without it a new deployment has no administrator:

    go build -o custos ./cmd/server
    CUSTOS_BOOTSTRAP_ADMIN_EMAIL=admin@example.org CONFIG_PATH=/etc/custos/custos.yaml ./custos

    It logs JSON to standard output. A good start logs database migrations applied successfully, connector is disabled for each connector turned off, and http server listening; a failure logs server exited with error and exits.

    Run it under a supervisor of your own. On SIGTERM it drains HTTP requests for up to 15 seconds and connector workers for up to 30.

  5. Set the portal variables in web/.env.local, starting from web/.env.example, then run pnpm install, pnpm build and pnpm start.

  6. Sign in to the portal with the bootstrap address. Your identity is linked to the pending user, which becomes active with super_admin.

The API calls below need an administrator's bearer token, shown as $TOKEN: the token the portal forwards in step 3. While you are signed in to the portal, https://<portal host>/api/auth/session returns it in the accessToken or idToken field.

Connect the cluster​

Connect in this order: ACCESS creates the allocations and accounts that COmanage and Slurm provision; the signer is independent. Each connection ends by setting the connector's keys, enabling it and restarting the server; the startup log then shows loading connector with its name.

Register the cluster​

A cluster record's name is the Slurm cluster name, and a resource's name is a partition name, as AMIE packets also name it. The portal cannot create clusters or resources; use the API.

  1. Create the cluster, named as ClusterName in slurm.conf:

    curl -sS -X POST https://custos.example.org/compute-clusters \
    -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
    -d '{"name": "expanse"}'

    Export the returned id as CUSTOS_CLUSTER_ID and AMIE_CLUSTER_ID.

  2. Create one resource per partition with POST /compute-allocation-resources: name (the partition), resource_type (the TRES limits apply to, such as cpu or gres/gpu), resource_amount (capacity in that TRES) and compute_cluster_id.

  3. Give each resource a rate with POST /compute-allocation-resource-rates (compute_allocation_resource_id, rate in SUs per billing-unit hour, start_time, end_time), or in the portal under Admin → Resources. Jobs ending outside every rate period are not charged.

Connect ACCESS​

The amie-processor connector applies each AMIE packet and replies to ACCESS. request_project_create creates the allocation, named by the packet's GrantNumber, which becomes the Slurm account name, and maps it to each resource in ResourceList that exists on the cluster; others are skipped with a warning. A failed packet is retried twice, then marked failed.

  1. Register the cluster and every resource ACCESS will name.

  2. Check the credentials from the server host; the response lists waiting packets:

    curl -sS -H "XA-SITE: $AMIE_SITE_CODE" -H "XA-API-KEY: $AMIE_API_KEY" \
    "$AMIE_BASE_URL/packets/$AMIE_SITE_CODE"
  3. Set the keys, enable the connector and restart the server.

  4. Follow packets under Admin → AMIE, or with GET /connectors/amie/packets and GET /connectors/amie/stats.

For rehearsal, connectors/ACCESS/AMIE-Processor/mock-server is a mock AMIE server that generates valid, invalid or mixed packets.

Connect COmanage and LDAP​

When Custos creates a cluster account, the comanage-identity-provisioner connector gives the person a POSIX identity in the registry, the registry's LDAP provisioner publishes it, and SSSD resolves it. Custos never writes to LDAP and never deprovisions in COmanage; an administrator does.

StepRegistry object
Find the person by stored COmanage ID, then e-mail, or create oneCoPerson
Read the uidnumber identifierNone; fails with missing_uidnumber if unassigned
Record the OIDC subject, after the user's first sign-inIdentifier of type oidcsub
Create a group named after the login nameCoGroup with uid and gidnumber (= uidnumber)
Add the person to itCoGroupMember, as a member
Attach group and account to the UnixClusterUnixClusterGroup; UnixClusterAccount with shell and home, manual sync
Cluster administrators onlyMembership of admin_group

The account is then marked provisioned, which the Slurm connector waits for. A login name is the prefix, a hyphen, the first initial and the last name (custos-jdoe), at most 32 characters, with a numeric suffix from 2 on collision.

  1. In the registry, enable the Core API (/api/co/<co id>/core/v1/people) and REST API version 1 for an API user; create a UnixCluster; define identifier types uid, gidnumber, oidcsub and the one in person_id_type; turn on automatic uidnumber assignment; and, for cluster administrators, create a group the centre grants sudo.
  2. Configure the registry's LDAP provisioner to write posixAccount entries and publish oidcsub as voPersonExternalID, which the PAM module searches. The repository contains no registry configuration.
  3. Set the keys, enable the connector and restart the server.

Connect Slurm​

The slurm-association-mapper connector keeps Slurm in line with Custos:

Custos changeSlurm effect
Allocation createdAccount named after the allocation, with the PI's organisation, on the cluster
Resource attachedAccount limits: GrpTRES from the amount, GrpTRESMins from the time
Membership active, or member override createdOne user association per resource: partition = resource name, QoS normal, limits from the override
Membership inactive or deletedThe user's associations under the account removed
Allocation inactive or deletedEvery user association removed; the account kept
Allocation active againActive members' associations rewritten

A user association is written only once the member's COmanage account is provisioned. A reconciler runs at startup and every association_reconcile_interval. It writes associations provisioned at least association_provision_grace earlier, and removes unexpected user associations in Custos-managed accounts; see Reconciliation.

Cluster requirementWhy
slurmrestd on slurmdbd, reachable from the serverEvery call uses /slurmdb/
AuthAltTypes=auth/jwt and a jwt_key in slurm.conf and slurmdbd.confThe server authenticates with a JWT
A user with AdminLevel=AdministratorWrites accounts and associations
QoS normal allowed on the clusterEvery association uses it
AccountingStorageEnforce with associations and limitsRefuses removed users; applies limits
Cluster record name = ClusterNameScopes account and association calls

Without QoS normal and with qos enforced, jobs fail with "Invalid account or account/partition combination specified".

  1. Issue a token for the Slurm user, as root or SlurmUser, lasting until your next rotation. The server reads it once at startup and does not renew it:

    scontrol token username=custos lifespan=2592000
  2. Check the endpoint from the server host (version 41 against Slurm 24.05 is the tested combination):

    curl -sS -H "X-SLURM-USER-NAME: custos" -H "X-SLURM-USER-TOKEN: $SLURM_TOKEN" \
    "https://slurm.example.org:6820/slurmdb/v0.0.41/associations?cluster=expanse"
  3. Set the keys, enable the connector and restart the server.

Charge usage​

The slurm-usage-monitor connector reads jobs from slurmdbd every 30 seconds. It charges a finished job when the job's account is an allocation on the cluster, its user a Custos cluster account and its partition a registered resource. The charge is billing TRES × elapsed hours × the resource's rate at the job's end. Jobs with no billing or no rate are skipped with a log line. The first poll after each start reads slurmdbd's whole job history.

Without TRESBillingWeights, billing is the CPU count and GPU and memory go uncharged. Set weights per partition, combined as the maximum with PriorityFlags=MAX_TRES:

PriorityFlags=MAX_TRES
PartitionName=gpu Nodes=g[1-8] TRESBillingWeights="CPU=1.0,Mem=0.25G,GRES/gpu=16.0"

Then set the keys, enable the connector and restart the server.

Enrol nodes​

Enrolment changes the nodes themselves and replaces /etc/pam.d/sshd; try it on one login node before the rest. The playbook in dev-ops/account-provisioning enrols RHEL 9 and 10 nodes: SSSD resolves accounts from the COmanage LDAP, and sshd authenticates by CILogon device-code sign-in (the PAM module).

After sign-in the module searches (&(objectClass=posixAccount)(voPersonExternalID=<sub>)) and admits the user only if uid equals the login name. Device-code login therefore works only after the user's first sign-in to Custos.

The playbook's tags enrol a node in stages:

TagChanges on the node
prereqsInstalls openldap-clients; checks LDAP TLS, bind and search base
sssdInstalls SSSD and oddjob-mkhomedir; writes /etc/sssd/sssd.conf (LDAP identity, auth_provider = none, ldap_tls_reqcert = demand); runs authselect select sssd with-mkhomedir; sets authlogin_nsswitch_use_ldap; checks getent passwd
pamBuilds /usr/lib64/security/pam_oauth2_device.so at pam_oauth2_device_version (default master); writes /etc/pam_oauth2_device/config.json (0600, holds both secrets); replaces /etc/pam.d/sshd; adds an SELinux module; restarts sshd
sshdAdds /etc/ssh/sshd_config.d/99-pam-oauth2-device.conf, turning on PAM, keyboard-interactive and password authentication; restarts sshd

The new stack tries the device-code module as sufficient, then password-auth, so local passwords still work; public-key authentication is unchanged. The 99- file also sets PasswordAuthentication yes; where password login is forbidden, review that line before applying the tag.

  1. Register a confidential CILogon client for the device flow, separate from the portal's, with scopes openid profile email org.cilogon.userinfo.

  2. Copy inventory/hosts.example.yml to inventory/hosts.yml and group_vars/all.yml.example to group_vars/all.yml; set the LDAP URI, search base, bind DN, CILogon client ID, SSSD domain and test_username.

  3. Encrypt the secrets:

    ansible-vault encrypt_string 'ldap-password' --name 'vault_ldap_bind_password' >> group_vars/all.yml
    ansible-vault encrypt_string 'cilogon-secret' --name 'vault_cilogon_client_secret' >> group_vars/all.yml
  4. Pin pam_oauth2_device_version to a tag or commit, and back up /etc/pam.d/sshd where it is customised.

  5. Check LDAP, then enrol. On compute nodes, where accounts need only resolve, add --tags prereqs,sssd to the second command:

    ansible-playbook -i inventory/hosts.yml enroll-node.yml --tags prereqs --ask-vault-pass
    ansible-playbook -i inventory/hosts.yml enroll-node.yml --ask-vault-pass
  6. Run verify.yml with the same inventory.

Add the SSH certificate signer​

The signer issues short-lived OpenSSH user certificates, an alternative to stored keys and to the PAM module. It is a separate program with its own database and shares nothing with the Custos server.

AspectBehaviour
ClientsRows in client_ssh_configs: tenant, client, bcrypt secret, target host, lifetime, key types; no API creates them
CAVault ssh-ca/data/<tenant>/<client>/current, plus next; Ed25519 per tenant and client, made at first use
SigningPOST /api/v1/sign with client credentials and a user access token from signer.auth.allowed_issuers (default CILogon)
PrincipalsMatch ^[a-z_][a-z0-9_-]{0,31}$; validator noop, ldap or comanage checks them against the token subject
RevocationPOST /api/v1/revoke records it; no revocation list, so certificates stay valid until expiry
RotationPOST /api/v1/admin/rotate-ca promotes next; nothing rotates on a schedule
  1. Enable a KV version 2 engine and give the signer a token with read and write on ssh-ca/data/*:

    vault secrets enable -path=ssh-ca kv-v2
  2. Create a database custos_signer, copy config.example.yaml to config.yaml, and set the database, Vault address and token (or DB_*, VAULT_ADDRESS, VAULT_TOKEN), allowed_issuers and principal_validator. Leave dev_mode.enabled false; it turns off token validation.

  3. Build, migrate and start; the signer listens on port 8084:

    cd extensions/SSH-Certificate-Signer
    go build -o custos-signer .
    ./custos-signer migrate --config config.yaml
    ./custos-signer serve --config config.yaml
  4. Insert a client row with at least tenant_id, client_id, client_secret, target_host and allowed_key_types, hashing the secret with htpasswd -nbBC 10 "" "<secret>" | cut -d: -f2.

  5. Trust the client's CA on each node; the repository has no sshd configuration for this. With TrustedUserCAKeys and no AuthorizedPrincipalsFile, sshd admits a certificate whose principal equals the login name:

    curl -sS -H "X-Client-Id: tenant1:webapp" -H "X-Client-Secret: $SECRET" \
    https://signer.example.org/api/v1/ca-public-key > /etc/ssh/custos_user_ca.pub
    echo 'TrustedUserCAKeys /etc/ssh/custos_user_ca.pub' > /etc/ssh/sshd_config.d/50-custos-ca.conf
    systemctl reload sshd

    Repeat after each rotation; until then, nodes refuse certificates signed with the new key.

Onboard users and roles​

Users arrive from ACCESS through AMIE packets, or an administrator creates them with POST /users. The flags on that call decide what a user may do:

Role or flagGranted byGives
super_adminCUSTOS_BOOTSTRAP_ADMIN_EMAIL, only while nobody holds itEvery privilege
adminportal_admin: true in POST /usersEvery privilege present when the role was created, except core:privileges:grant and core:roles:manage
Cluster administratorcluster_admin: true in POST /usersA cluster account with access level ADMIN, in the COmanage admin_group
ResearcherNeither flagA cluster account and, optionally, one allocation membership

POST /users needs core:users:write; either flag also needs core:roles:manage. Its fields:

FieldMeaning
emailRequired
first_name, last_nameGenerate the login name; without both, the part of email before @ does
usernameLogin-name override, at most 32 characters
allocation_idResearchers only; also selects the cluster
compute_cluster_idThe cluster, when no allocation is given
organization_idDefault: the system organisation
portal_admin, cluster_adminThe flags above

Unless exactly one cluster is registered, give allocation_id or compute_cluster_id for a user who needs a cluster account, or onboarding fails with "pick a cluster, N are registered".

curl -sS -X POST https://custos.example.org/users \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"email": "jdoe@example.edu", "first_name": "Jane", "last_name": "Doe",
"allocation_id": "<allocation id>"}'

The user starts pending. The first sign-in whose email claim matches activates the user and links the identity; any other unlinked sign-in gets HTTP 401 identity_not_linked. email_verified is not checked. AMIE users are activated the same way, with the address ACCESS supplied.

TaskCall
Add a user to another allocationPOST /compute-allocation-memberships
Grant roles or privileges; effective at oncePOST /users/{id}/roles, POST /users/{id}/privileges
List privilegesGET /privileges/catalog, or the repository's docs/privileges.md

Verify the connections​

One check per connection, in order; a later row depends on the earlier ones.

ConnectionCheckExpected result
Servercurl https://custos.example.org/healthz{"status":"ok"}
DatabaseStartup logdatabase migrations applied successfully
OIDCGET /me with a user's tokenThe caller's profile; 401 invalid_token means issuer or audience mismatch
ConnectorsStartup logloading connector per enabled connector; no skipping for a configured one
ACCESSGET /connectors/amie/stats, or Admin → AMIEPackets processed, none failed
COmanageGET /audit/events for the onboarding trace, or Admin → TracesComanageClusterAccountAttached; ComanageProvisioningFailed names the failed step
LDAP and SSSDgetent passwd custos-jdoe on a nodeThe entry, with the COmanage uidnumber
Slurm accountssacctmgr show account <grant number>The account
Slurm associationssacctmgr show assoc where account=<grant number> format=cluster,account,user,partition,qos,grptres,grptresminsOne row per member and partition, within the reconcile interval plus grace
UsageGET /compute-allocations/{id}/usages/total after a job endsThe job's charge; successfully polled SLURM usage logged every 30 seconds
Node sign-inssh custos-jdoe@login-nodeA CILogon device code, then a shell; a user without an LDAP entry is refused
Signercurl https://signer.example.org/api/v1/health; sign a key and log inhealthy with database and Vault up; ssh-keygen -L -f key-cert.pub shows principal and validity

Configuration reference​

The server reads one YAML file, config/custos.yaml under its working directory or the path in CONFIG_PATH, and exits if it is missing. ${NAME} is replaced by the environment variable NAME; an unset variable stays as the literal ${NAME}. Set every variable the file references, or remove the blocks you do not use.

Server​

Key or variableDefaultMeaning
core.database.urlRequiredpgx DSN, such as postgres://custos:…@db.example.org:5432/custos?sslmode=verify-full
core.api.port8080HTTP port
core.log_levelinfodebug, info, warn or error
core.auth.oidc.issuerRequired/.well-known/openid-configuration under this issuer URL gives the keys
core.auth.oidc.audienceRequiredExpected aud of the bearer token
core.auth.cache_ttl30sCaller identity and privilege cache; at most 60s
CONFIG_PATHconfig/custos.yamlConfiguration file
DB_MAX_OPEN_CONNS, DB_MAX_IDLE_CONNS25, 5Connection pool
CUSTOS_BOOTSTRAP_ADMIN_EMAILUnsetAddress given super_admin at startup
POSIX_USERNAME_PREFIXcustosLogin-name prefix
CUSTOS_TRACING_MODEUnsetnoop turns off audit trace IDs

Connector keys​

Each entry under connectors has any name, a type and enabled. A connector with incomplete credentials is skipped with a log line; one that fails while loading stops the server. temp-account (routes for VIRTUAL users under /connectors/temp-account/) and analytics (usage summaries and job lists under /connectors/analytics/) have no settings.

amie-processor; the shipped file fills these from AMIE_BASE_URL, AMIE_SITE_CODE, AMIE_API_KEY and AMIE_CLUSTER_ID:

KeyDefaultMeaning
credentials.base_urlRequiredAMIE API base URL
credentials.site_code, credentials.api_keyRequiredThe centre's ACCESS site code and API key
cluster.idRequiredCluster this AMIE site serves; one site, one cluster
polling.poll_interval30sBetween polls
polling.worker_interval5sBetween batches of up to 50 packets
polling.poller_enabledtruefalse processes stored packets without fetching
timeouts.connect_timeout, timeouts.read_timeout5s, 20sHTTP timeouts

comanage-identity-provisioner; if any required key is missing, the whole configuration is read instead from COMANAGE_REGISTRY_URL, COMANAGE_CO_ID, COMANAGE_API_USER, COMANAGE_API_KEY, COMANAGE_PERSON_ID_TYPE, COMANAGE_UNIX_CLUSTER_ID, COMANAGE_CLUSTER_ADMIN_GROUP and CUSTOS_CLUSTER_ID:

KeyDefaultMeaning
registry.url, registry.co_idRequiredRegistry base URL; numeric CO ID
registry.api_user, registry.api_keyRequiredAPI user, sent with HTTP basic authentication
unix_cluster.idRequiredNumeric UnixCluster ID
unix_cluster.person_id_typeRequiredIdentifier type used to find and tag a CoPerson
unix_cluster.admin_groupUnsetGroup for cluster administrators; an unexpanded ${…} counts as unset
provisioning.custos_cluster_idRequiredCustos cluster id; other clusters' accounts are ignored
provisioning.default_shell/bin/bashLogin shell
provisioning.homedir_prefix/home/Home directory prefix
provisioning.http_timeout30sRegistry request timeout

slurm-association-mapper; the shipped file fills slurm_api from SLURM_API_URL, SLURM_API_VERSION, SLURM_API_USERNAME and SLURM_TOKEN, and an absent key falls back to SLURM_API, SLURM_USER, SLURM_TOKEN and SLURM_API_VERSION:

KeyDefaultMeaning
slurm_api.urlRequiredslurmrestd base URL
slurm_api.versionRequiredMinor version only, such as 41; paths are /slurmdb/v0.0.<version>/
slurm_api.username, slurm_api.tokenRequiredSlurm user and its JWT
association_reconcile_interval5mBetween reconciler sweeps
association_provision_grace30sWait after provisioning, for LDAP export and name caches

slurm-usage-monitor:

KeyDefaultMeaning
slurm_api.*RequiredAs for slurm-association-mapper
cluster_idSLURM_MONITOR_CLUSTER_ID, else slurm-clusterCustos cluster id; the shipped file uses CUSTOS_CLUSTER_ID
usage_lookback15mHow far before the previous poll each poll re-reads, for jobs recorded late

Portal​

Variable in web/.env.localValue
NEXTAUTH_URLThe portal's public URL
NEXTAUTH_SECRETOutput of openssl rand -base64 32
CUSTOS_CORE_API_BASE_URLThe server's URL as the portal host reaches it
OIDC_ISSUER_URL, OIDC_CLIENT_ID, OIDC_CLIENT_SECRETThe portal's OIDC client
NEXT_PUBLIC_PORTAL_NAME, NEXT_PUBLIC_PORTAL_LOGO, NEXT_PUBLIC_PORTAL_FAVICONOptional branding

Running Custos covers monitoring, backups, upgrades, cutting off users and known problems.