Skip to content

Configuration

Overview#

The different configuration files serve different purposes:

Each file has a specific role in the authentication and user management pipeline, working together to provide OIDC-based SSH access.

motley_cue.conf#

The main configuration file is generally self documenting: it contains examples for typical use. Configuration is structured into several sections as follows.

General config#

[mapper] - Core service settings:

  • log_level: Logging level (default: WARNING)
  • log_file: Log file location (default: /dev/stderr)
  • enable_docs: Enable Swagger/ReDoc documentation (default: False)
  • docs_url, redoc_url: Documentation endpoints
  • api_version: API version to use (default: v1)

[mapper.otp] - One-Time Password settings:

This section configures creation and storage of short lived one time passwords (OTPs).

When using PAM authentication in ssh-oidc, the OIDC access token is passed by the ssh-client through the password field. Some ssh-client have a length limitation of 1kb for the password field. Hence, Motley Cue supports the dynamic creation of one time passwords, that clients can use instead of large access tokens:

  • use_otp: Use OTP instead of tokens as SSH password (default: True)
  • backend: Storage backend - memory, sqlite, sqlitedict (default: memory)
  • db_location: Token database location (default: /tmp/tokenmap.db)
  • keyfile: Encryption key file (default: /tmp/motley_cue.key)

Example:

[mapper.otp]
# Enable OTP for tokens > 1KB
use_otp = True
# Backend for storing OTP-AT mapping
backend = sqlite
# Database location
db_location = /run/motley_cue/tokenmap.db
# Encryption key file
keyfile = /run/motley_cue/motley_cue.key

[privacy] - Privacy policy:

  • privacy_contact: Service operator email (MUST be configured)
  • privacy_file: Privacy statement location (default: /etc/motley_cue/privacystatement.md)

Authorisation#

Authorisation is configured in one or more [authorisation.*] sections. It contains per-OP authorisation.

Defaults for the authorisation.* sections can be set in the magic section [DEFAULT]. These options are available and explained below.

  • op_url: OpenID Provider URL (required)
  • scopes: Required OIDC scopes
  • authorise_all: Allow all users from this OP (default: False)
  • authorised_users: List of authorized users by ‘sub’ claim
  • authorised_vos: List of authorized Virtual Organizations
  • vo_claim: OIDC claim containing VOs (default: eduperson_entitlement)
  • vo_match: How many VOs to match - all, one, or integer (default: one)
  • audience: Audience claim for this service (optional)
  • authorised_admins: Admin users who can suspend/resume accounts
  • authorise_admins_for_all_ops: Allow admins to manage users from any OP

Note

  • The ‘op_url’ field is mandatory for each authorisation section
  • Unknown claims in tokens are ignored (won’t cause authorisation failure)
  • We use delegated Access Tokens I.e. there is no need to register a client_id/client_secret with a provider.
  • JWT access tokens are supported natively (issuer is read from the token). Non-JWT access tokens are also supported: oinit will prompt the user for the issuer URL in that case. When using oidc-agent with a non-JWT token, oidc-agent automatically supplies the issuer.

[DEFAULT] section#

The [DEFAULT] section sets the defaults for all [authorisation.<provider_name>] sections. These options are supported:

  • scopes - The required scopes from the OP to access the service
    default: scopes = ["openid", "profile", "email", "eduperson_entitlement"]

  • authorise_all - Authorise all users from trusted OP.
    default: authorise_all = False

  • authorised_vos - List of VOs whose members are authorised to use the service
    default: authorised_vos []

  • vo_claim - the OIDC claim containing the VOs specified above
    default: vo_claim = eduperson_entitlement

  • vo_match - how many VOs need to be matched from the list, valid options: all, one, or an int
    default: vo_match = one

  • authorised_users = list of individual users authorised to use the
    service specified through OIDC ‘sub’, relative to the section’s OP defaults to empy list if not specified
    default: authorised_users = []

    Info

    You can find out your sub and VOs you are a member of by using flaat:

         $ pip install flaat
         $ flaat-userinfo $TOKEN
    
    where $TOKEN contains an Access Token from the OP you are interested it.

    A commandline tool to retrieve access tokens is: oidc-agent

  • audience - audience claim specific to this service (OPTIONAL); it can
    be a string or a list of strings if empty or not specified, audience checking will not be used for authorisation
    Be aware that many OPs do not support this feature, in which case it will be ignored, and not be used for authorisation.
    default: audience = ""

  • authorised_admins - Admin Authorisation

    Info

    Each OP includes authorisation for the /admin endpoint, which allows suspending and resuming access for individual users.
    Only individual admins can be authorised (by ‘sub’ claim). This is meant for infrastructure security contacts. In the future, we plan to support this feature for VO managers too, to suspend members of their own VO (by using VO roles encoded in AARC-G002 entitlements).

    default: authorised_admins = []

[authorisation.<provider_name>]#

Each [authorisation.<provider_name>] section is used to configure one OP (OpenID Provider). You can set the provider_name yourself.

Each [authorisation.<provider_name>] section overrides the any of the [DEFAULT] options. the op_url must be specified.

  • op_url - OIDC provider URL (mandatory)

Examples#

Basic example#

Here is an example for how to configure the EGI Checkin OP to authorise any user of the VO identified by its entitlement "urn:mace:egi.eu:group:my-group:role=member". In addition, also allow th user "ea1b8e21-3654-4178-bd04-98c6adf58951" known at the EGI OP.

[authorisation.egi]
# The complete URL for the OIDC provider (MANDATORY)
op_url = https://aai.egi.eu/auth/realms/egi

# Authorise based on Virtual Organisation membership (entitlements)
authorised_vos = ["urn:mace:egi.eu:group:my-group:role=member"]
vo_claim = eduperson_entitlement

# Authorise specific individual users
authorised_users = ["ea1b8e21-3654-4178-bd04-98c6adf58951"]

To treat users who have shown their passport at their Home-IdP differently from those who have not, use assurance-based shell tiers. They are configured in the same [DEFAULT] / [authorisation.*] sections:

assurance_based_shell_tier_full = profile/espresso | profile/cappuccino
assurance_based_shell_tier_restricted = *

Users matching the first expression get the normal shell; everyone else gets the restricted one.

Multiple Identity Providers#

You can configure different authorisation rules for each provider:

[authorisation.kit]
op_url = https://oidc.scc.kit.edu/auth/realms/kit
# Only allow members of KIT
vo_claim = eduperson_scoped_affiliation
authorised_vos = ["member@kit.edu", "student@kit.edu"]

[authorisation.google]
op_url = https://accounts.google.com
# Only allow specific email domains
vo_claim = email
vo_list = [".*@example\.com"]
[authorisation.helmholtz]
op_url = https://login.helmholtz.de/oauth2/
# Require specific entitlements (from the default claim: eduperson_entitlement)
authorised_vos = ["urn:geant:helmholtz.de:group:my-project:role=member"]

Assurance-based shell tiers#

Not every federated identity is equally trustworthy. A user who logged in with a second factor at a well-run Home-IdP is a different proposition from one who self-registered with an email address. Assurance-based shell tiers let you accept both, but give them different login shells.

motley-cue evaluates each user’s assurance and MFA claims against a set of boolean expressions, arriving at one of three tiers – full, limited or restricted. The tier is handed to feudalAdapter, which maps it to a shell in feudal_adapter.conf.

Assurance does not reject anyone

Every user who passes authorisation is deployed. The tier only decides which shell they get. This is deliberate: a user with weak assurance receives an account and an explanatory message rather than an opaque login failure.

This moved out of feudal_adapter.conf

Earlier versions configured a single global [assurance] require expression in feudal_adapter.conf, which rejected users who did not match. That mechanism is gone. Assurance is now configured per OP in motley_cue.conf, because only motley-cue has the per-OP context and sees the full token. If you have a require = line in your feudal_adapter.conf, port it to assurance_based_shell_tier_full as shown below.

Configuration#

The options live in the [authorisation.*] sections of /etc/motley_cue/motley_cue.conf, with [DEFAULT] supplying the values an OP does not override. So you can set a policy once globally and adjust it for individual providers – which is the whole point, since OPs differ widely in what assurance they can actually assert.

  • assurance_prefix - prefix prepended to relative claim values
    default: assurance_prefix = https://refeds.org/assurance/

  • assurance_claims - token claims whose values are collected into the assurance set
    default: assurance_claims = [assurance, eduperson_assurance, acr]

  • assurance_based_shell_tier_full - expression for the full tier
    default: empty (never matches)

  • assurance_based_shell_tier_limited - expression for the limited tier
    default: empty (never matches)

  • assurance_based_shell_tier_restricted - expression for the restricted tier
    default: empty (never matches)

  • assurance_based_shell_default_tier - tier used when no expression matches. Left unset it is decided per OP: full for an OP with no tier expressions at all, the least privileged tier (restricted) for an OP that does have a policy. Set it explicitly to override in either direction.
    default: empty, i.e. decided automatically

  • assurance_based_shell_max_tier - caps the resulting tier; it can only ever lower a tier, never raise one. Most useful per OP.
    default: empty, i.e. no cap

The feature is opt-in, and enabling it is per OP

With the defaults above, all three tier expressions are empty, so nothing matches and every user resolves to full. feudalAdapter then uses its normal shell. In other words: if you configure nothing, behaviour is exactly as it was before shell tiers existed. You enable the feature by writing expressions.

Enabling follows ordinary [DEFAULT] inheritance. A policy in [DEFAULT] applies to every OP, including one that has no section of its own. A policy in a single [authorisation.<op>] section applies to that OP only and leaves the others exactly as they were.

Once an OP has a policy, unmatched users fail closed

For an OP that does have a policy, a user matching none of its expressions gets the least privileged tier, not full. This matters because the most likely mistake – an expression that can never match, such as the MFA prefix trap below – would otherwise silently grant everyone a full shell. Set assurance_based_shell_default_tier explicitly if you want something else.

The expression grammar#

E -> E "&" E | E "|" E | "(" E ")" | string

& (AND) binds more strongly than | (OR); use parentheses to override. This is the same grammar as the old feudal assurance.require, so existing expressions carry over unchanged.

A bare string matches if the user’s assurance set contains it verbatim, or contains it prefixed with assurance_prefix. That dual match is what makes both worlds reachable from one option:

  • profile/cappuccino matches the REFEDS URL https://refeds.org/assurance/profile/cappuccino
  • 1 matches an acr claim whose literal value is 1
  • mfa matches an amr value of mfa

A string starting with http:// or https:// is matched verbatim only and never prefixed – so https://refeds.org/profile/mfa works even though it does not live under the assurance prefix.

REFEDS MFA is not under the assurance prefix

The REFEDS MFA profile is https://refeds.org/profile/mfa, while the REFEDS Assurance Framework lives under https://refeds.org/assurance/. They are different namespaces.

Writing the relative token profile/mfa therefore expands to https://refeds.org/assurance/profile/mfa, which no provider ever asserts – the expression silently never matches, and the user falls through to assurance_based_shell_default_tier (which defaults to full). Always write MFA as the absolute URL:

# correct
assurance_based_shell_tier_full = https://refeds.org/profile/mfa & profile/cappuccino
# WRONG -- can never match, everyone falls through to the default tier
assurance_based_shell_tier_full = profile/mfa & profile/cappuccino

Alternatively widen assurance_prefix to https://refeds.org/ and write profile/mfa and assurance/profile/cappuccino.

Two special tokens:

  • + matches if the user has any assurance claim at all
  • * always matches

An empty expression never matches.

Which claims are inspected#

Each claim named in assurance_claims is looked up in all three token sources and the results are combined:

  1. the userinfo endpoint
  2. the access token body, when it is a JWT
  3. token introspection

The union of everything found is what the expressions are evaluated against. This matters: a provider may put the assurance profile in eduperson_assurance at the userinfo endpoint while the MFA signal rides in acr inside the access token. An expression requiring both is only satisfiable if the two sources are merged, which is why motley-cue does not simply take the first source that carries a claim.

A claim’s value may be a list, a single string, or a space-separated list of values (as acr often is) – all are handled.

How the tier is chosen#

  1. The assurance set is collected as described above.
  2. The tier expressions are tested in order of decreasing privilege: full, then limited, then restricted. The first match wins.
  3. If none matches, assurance_based_shell_default_tier is used – which, left unset, is the least privileged tier for an OP that has a policy, and full for one that does not.
  4. Finally assurance_based_shell_max_tier is applied, if set. It can only lower the result.

Expressions are parsed once at startup, so a syntax error surfaces immediately as a configuration error naming the OP and the offending option, rather than failing later on a user’s login. The same applies to assurance_based_shell_default_tier and assurance_based_shell_max_tier: an unrecognised tier name is rejected at startup rather than ignored, because ignoring it would hand the user more privilege than intended.

Still configure a catch-all

Setting assurance_based_shell_tier_restricted = * is recommended even though unmatched users now fail closed by themselves: it makes the intent explicit in the config rather than relying on the fallback.

Mapping tiers to shells#

The tier names mean nothing by themselves; feudal_adapter.conf turns them into actual shells:

[backend.local_unix]
# the "full" tier / default shell
shell = /bin/bash
# the lower tiers
shell_limited = /etc/motley_cue/shells/mc-rbash
shell_restricted = /etc/motley_cue/shells/mc-message

Two shells ship with motley-cue for this purpose:

  • mc-rbash – a restricted bash permitting only a safe whitelist of commands (ls, id, whoami, …)
  • mc-message – prints an explanatory message and exits

shell_limited and shell_restricted both default to /sbin/nologin, i.e. they fail closed until you configure them.

LDAP backends

With the ldap backends the login shell is written into the directory, so the tier shells must exist, be executable, and usually be listed in /etc/shells on every target system the user logs in to – not just on the host running motley-cue. Note also that /etc is mounted noexec on some hosts; install the shells elsewhere in that case, for example under /usr/lib/motley-cue/shells/, and adjust the paths.

Examples#

A three-tier policy#

Full shell only with MFA and a REFEDS cappuccino profile; a restricted shell for cappuccino without MFA; a message-and-exit shell for everyone else.

[DEFAULT]
assurance_based_shell_tier_full = https://refeds.org/profile/mfa & profile/cappuccino
assurance_based_shell_tier_limited = profile/cappuccino
assurance_based_shell_tier_restricted = *
Capping a weak provider#

Google asserts no REFEDS assurance at all, so its users would be classified by whatever [DEFAULT] says. Cap the OP so none of them can ever reach the full shell, without touching the global policy:

[authorisation.google]
op_url = https://accounts.google.com/
scopes = ["openid", "profile"]
assurance_based_shell_max_tier = restricted
An OP that signals MFA in acr#

Some providers express the authentication context as a bare acr value rather than a REFEDS URL. Match it directly:

[authorisation.example]
op_url = https://op.example.org/
assurance_claims = [acr, amr, eduperson_assurance]
assurance_based_shell_tier_full = 2 | mfa
assurance_based_shell_tier_restricted = *
Migrating from the old assurance.require#

The old feudal_adapter.conf stanza:

[assurance]
prefix = https://refeds.org/assurance/
require = profile/espresso | profile/cappuccino

becomes, in motley_cue.conf:

[DEFAULT]
assurance_prefix = https://refeds.org/assurance/
assurance_based_shell_tier_full = profile/espresso | profile/cappuccino
assurance_based_shell_tier_restricted = *

The expression is unchanged. The difference is the outcome: users who do not match are no longer rejected, they get the restricted shell. To keep them out entirely, point shell_restricted at /sbin/nologin.

Unknown providers#

A token from an OP that has no [authorisation.*] section of its own is not authorised anyway, but should it ever be evaluated, the [DEFAULT] policy applies. A configured assurance_based_shell_tier_restricted = * catch-all therefore also covers unknown providers, rather than granting them full.

Troubleshooting#

Set log_level = DEBUG in the [mapper] section of motley_cue.conf. The evaluation then logs, for every request, each configured claim and the values found for it per source, followed by the tier that was selected and any cap that was applied. That tells you directly whether a claim you expected is missing, or is simply arriving under a name or in a source you have not configured.

If a user’s claims look right but the shell does not change, check that the OP is actually being asked for the relevant scopes (scopes in the same [authorisation.*] section – eduperson_assurance in particular), and that feudalAdapter is recent enough (see below).

Version requirements

Assurance-based shell tiers need feudalAdapter >= 2.0.0 on the receiving side; the ldap backends gained tier support in 2.0.1. An older feudalAdapter silently ignores the tier and gives every user the default shell.

feudal_adapter.conf#

Feudal andles the backend user management configuration and the just-in-time provisioning of accounts. It is very flexible.

Feudal supports several so called “backends” for the account creation. Also, different strategies for generating local usernames is supported. Existing usernames are always preferred, and are automatically found, when an existing mapping to them exists.

To support use-cases in which an adminstrator needs to approve new users before admins triggers a provisioning flow themselves, a so called “approval flow” is supported.

Provisioning#

feudal_adapter.conf is structured in sections as follows.

[ldf_adapter]#

Core backend settings of the leudal_adapter (also ldf_adapter).

  • backend: Backend type - local_unix, bwidm, ldap (default: local_unix)
  • backend_supports_preferring_existing_user: Detect and reuse existing usernames (default: False) To be deprecated
  • primary_group: Default primary group for users
  • fallback_group: Group for users without groups (default: nogroup)
  • additional_groups: Extra groups to add users to
  • interactive: Deprecated; Always False

[messages]#

Select which information will be logged

  • log_file: Log file location (default: /var/log/feudal/feudal.log)
  • log_level: Logging level (default: INFO)
  • log_to_console: Console logging control
  • log_name_changes: Log username/group name normalization (default: True)
  • log_primary_group_definition: Log primary group selection (default: True)
  • log_username_creation: Log username generation attempts (default: False)

[approval]#

Configure the approval workflow for deploying local accounts. When active, instead of adding the user to a backend, the administrator is notified and asked to create the account manually.

Until the admin approved the account, pending users will be shown a message to return later.

  • enabled: Require admin approval for accounts (default: False)
  • user_db_location: Pending requests database, sqlite (/var/lib/feudal/pending_users.db)
  • notifier: Notification method (default: email)

[notifier.email]#

Email notification settings

  • smtp_server: SMTP server hostname (default: localhost)
  • smtp_port: SMTP server port (default: 25)
  • use_ssl: Enable SSL/TLS (default: False)
  • sent_from: Sender email address (default: admin@localhost)
  • sent_from_password: Sender password for auth
  • admin_email: Admin recipient (default: admin@localhost)
  • templates_dir: Email template directory (default: /etc/feudal/templates)

[assurance]#

Moved to motley_cue.conf

Assurance used to be configured here, as a single global require expression that rejected users who did not match. That mechanism has been replaced by per-OP assurance-based shell tiers in motley_cue.conf, which classify users instead of turning them away. See migrating from the old assurance.require.

What remains here is the shell each tier maps to, configured per backend – shell, shell_limited and shell_restricted, see backend.local_unix.

  • verified_undeploy: Block undeployment for unauthorized users (default: false)

[username_generator]#

We support several different username creation strategies.

These modes are currently available:

  • friendly: Friendly implements a list of strategies that are tried one after another. The input is based on different claims of the incoming userinfo object:
    • preferred_username claim (if present)
    • combine a varying number of letters of given_name + family_name
    • email-address if all else fails
  • pooled: Pooled implements the pool-account behaviour known from “the grid”. By default, we use the primary group name and append the digits, in the order of incoming users. In practice, group names may be much longer. Therefore, a prefix may be configured.

  • mode: Username generation mode

    • friendly: Try preferred_username, then given_name+family_name combinations, then email
    • pooled: Use group prefix + sequential numbers (grid-style)
  • pool_prefix: Prefix for pooled accounts (default: primary group name)
  • pool_digits: Number of digits for pooled accounts (default: 3)
  • strip_sub_groups: Remove subgroups from long federated names (default: no) This may be useful in situations where you receive very long group names. (See the [groups] section for a better way.)

[groups]#

Configure how entitlements and groups are being translated into local unix names, and whether to create them at all. This is probably the most specific part about feudal.

Warning

If you change the group mapping after the first groups have been created, new groups according to the new configuration could be created. Please carefully review your configuration e.g. by using tools such as feudal-simulator.

  • policy: Group creation policy
    • all: Create all groups from user attributes (potentially many)
    • listed: Only create groups in supported_entitlements/supported_groups
  • supported_entitlements: Regex list of allowed entitlement-based groups
  • supported_groups: Regex list of allowed group names
  • method: Group name mapping method
    • classic: Default mapping (pre-feudal-v1 compatibility)
    • regex: Custom regex-based mapping rules
  • mapping: List of regex replacement rules (for method=regex)
Example: Only allow specific entitlements#
[groups]
policy = listed
supported_entitlements = 
    urn:mace:egi.eu:group:eosc-synergy.eu.*
    urn:mace:egi.eu:group:vo.example.org:role=member
    urn:geant:helmholtz.de:group:.*
Example: Only allow specific group names#
[groups]
policy = listed
supported_groups = 
    users
    developers
    test-vo-.*
    wlcg-.*
Example: Allow any group, but map it#

For this we use regular expressions in the mapping option. This allows automatically generating all groups that come in, but make sure they are mapped to short and digestable names.

[groups]
policy = all
method = regex
mapping = 
    # Remove role information
    :role=(owner|member) -> 
    # Shorten long prefixes
    urn:mace:egi.eu:group: -> egi_
    urn:geant:helmholtz.de:group: -> helm_
    # Replace dots and colons with hyphens
    [\.:] -> -
    # Specific renamings
    eosc-synergy.eu -> synergy
    data.kit.edu -> kit
    # Shorten admin groups
    :admins -> :adm
Example: Map supported entitlements to groups#
[groups]
policy = listed
supported_entitlements = 
    urn:mace:egi.eu:group:eosc-synergy.eu.*
    urn:mace:egi.eu:group:vo.example.org:role=member
    urn:geant:helmholtz.de:group:.*
method = regex
mapping = 
    urn:mace:egi.eu:group:eosc-synergy.eu. -> synergy_
    urn:mace:egi.eu:group:vo.example.org:role=member -> example
    urn:geant:helmholtz.de:group:.* -> helmholtz

login_info#

User login information display

  • description: Service description shown to users
  • login_help: Login instructions template
  • Additional custom fields supported

backend.local_unix#

Local Unix backend

  • shell: User shell (default: /bin/bash)
  • home_base: Home directory base (default: /home)
  • deploy_user_ssh_keys: Deploy SSH keys from OIDC claims (default: no)
  • punch4nfdi: Use PUNCH4NFDI-specific group name translation (default: no)
  • post_create_script: Script to run after user creation
  • shadow_compatibility_function: Username compatibility method (default/v044/punch)

backend.ldap#

LDAP backend modes and settings

  • mode: LDAP access mode
    • read_only: Read existing mapped accounts only
    • pre_created: Map existing unmapped accounts
    • full_access: Create and manage accounts
  • host: LDAP server hostname (default: localhost)
  • port: LDAP server port (default: 1389, 636 for TLS)
  • tls: Enable LDAPS protocol (default: False)
  • admin_user: Admin DN for write operations
  • admin_password: Admin password
  • user_base: User search base (default: ou=users,dc=example)
  • group_base: Group search base (default: ou=groups,dc=example)
  • attribute_oidc_uid: OIDC UID attribute (default: gecos)
  • attribute_local_uid: Local UID attribute (default: uid)
  • For full_access mode:
    • shell: User shell (default: /bin/sh)
    • home_base: Home directory base (default: /home)
    • uid_min, uid_max: UID range (default: 1000-60000)
    • gid_min, gid_max: GID range (default: 1000-60000)
    • post_create_script: Post-creation script

feudal-simulator#

This is a small tool that accepts a provisioning or deprovisioning json on stdin, and shows to which user and groups this would be mapped.

Example:

cat examples/marcus-deploy-egi.json | feudal-simulator -c feudal_adapter_template.conf 

caddy reverse proxy setup#

We recommend using this Caddyfile and a set of handlers:

Caddyfile#

{
    auto_https off
    admin off
}

:80 {
    import /config_files/handlers/*.caddy
}
:443 {
    # log { # log in a more traditional way
    #     format transform "{common_log}"
    # }
    tls /etc/ssl/caddy/caddy.crt /etc/ssl/caddy/caddy.key
    import /config_files/handlers/*.caddy
}

Motley-cue API handler#

@motley_cue path /motley_cue /motley_cue/*
handle @motley_cue {
    uri strip_prefix /motley_cue

    # CORS headers
    header Access-Control-Allow-Origin *
    header Access-Control-Allow-Credentials true
    header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
    header Access-Control-Allow-Headers "Content-Type, Accept, Authorization"

    # Handle preflight requests
    @options method OPTIONS
    header @options Access-Control-Allow-Origin *
    header @options Access-Control-Allow-Credentials true
    header @options Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
    header @options Access-Control-Allow-Headers "Content-Type, Accept, Authorization"
    header @options Content-Type "text/plain charset=UTF-8"
    respond @options "" 204

    # Proxy to Unix socket
    reverse_proxy unix//run/motley_cue/motley-cue.sock
}

nginx reverse proxy setup#

Our packages ship with a default configuration file that installs to /etc/nginx/sites-enabled/nginx.motley_cue

server {
    listen 8080;
    listen [::]:8080;

    server_name _;
    location / {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Host $http_host;
        # we don't want nginx trying to do something clever with
        # redirects, since we set the Host: header above already.
        proxy_redirect off;
        proxy_pass http://unix:/run/motley_cue/motley-cue.sock;
    }
}

Please refer to the nginx documentation for setting up further aspects:

  • HTTPS termination
  • Port configuration
  • Host certificates
  • URL routing to motley-cue service

PAM integration with SSH#

PAM integration with SSH is done via pam-ssh-oidc

Note

The PAM integration requires our PAM module on the ssh serverside. If this is not acceptable, the oinit set of tools (also part of our tools) works with ssh-certificates and does not require PAM modules.

Last change: Aug 11, 2026 10:58:31