Client settings enforcement
ssoossh client settings can be locked so a user cannot override them, through
three mechanisms: one per platform’s native fleet-management tooling, plus the
cross-platform enforce YAML file.
Precedence
Section titled “Precedence”Lowest to highest. Each source’s value is overridden by any later one that also sets the same key:
- Built-in defaults
- User config (
~/.config/ssoossh.yaml, or%AppData%\ssoossh\ssoossh.yaml) - The file named by
--config - CLI flags (
--server,--key-type,--key-size, and the--no-*extension flags) - The
enforcefile named by the machine-wide config (/etc/ssoossh/ssoossh.yaml, or%ProgramData%\ssoossh\ssoossh.yaml) - Platform-native policy: the Windows registry key or the macOS managed preferences described below
A value simply absent from a source is left to whatever the next source down
sets. A policy source that only locks server, for example, leaves every other
setting free.
Settings
Section titled “Settings”Every setting below can be set through the enforce YAML file, the Windows
registry, or macOS managed preferences. Same setting, three delivery
mechanisms, admin’s choice.
| YAML key | Windows registry value | macOS plist key |
|---|---|---|
server |
Server (REG_SZ) |
server (string) |
capubkey |
CAPubkey (REG_SZ) |
capubkey (string) |
insecure_skip_verify |
SkipVerifySSL (REG_DWORD 0/1) |
insecure_skip_verify (bool) |
use_agent |
UseAgent (REG_DWORD) |
use_agent (bool) |
fallback_file_agent |
FallbackFileAgent (REG_DWORD) |
fallback_file_agent (bool) |
key_filename |
KeyFilename (REG_SZ) |
key_filename (string) |
try_open_browser |
TryOpenBrowser (REG_DWORD) |
try_open_browser (bool) |
fips |
FIPS (REG_DWORD; absent = unset) |
fips (bool; absent = unset) |
sshkey.type |
SSHKeyType (REG_SZ) |
sshkey.type (string) |
sshkey.size |
SSHKeySize (REG_DWORD) |
sshkey.size (integer) |
forbidden_certificate_extensions |
ForbiddenCertificateExtensions (REG_MULTI_SZ) |
forbidden_certificate_extensions (array of strings) |
fips: true from any locked source – the enforce file or a platform policy
– makes a non-FIPS-approved sshkey.type a hard error at startup rather than
a silent downgrade.
forbidden_certificate_extensions
Section titled “forbidden_certificate_extensions”Worth spelling out, because what it does is narrower than the name suggests.
It is subtracted from the set ssh login asks for, before the request is
sent, and it cannot be re-added by a flag. That is the whole of its effect:
it shapes a request, made by a client that is choosing to honour it. Per
the caution above, it is a guardrail rather than a boundary – a user who
does not want it can run their own binary or call the API directly.
Two limits follow, and neither is obvious from the setting’s name:
- It does not apply to service certificates. The subtraction happens only
on the interactive login path. A service certificate is bounded by
cert_options.service.extensionson the server and by nothing on the client, so an extension forbidden here can still reach a service certificate on this machine. - The server cannot infer it. It sees an extension absent from the
request and, on its own, cannot tell your policy from the user having
passed
--no-port-forwarding.
The client therefore reports the setting alongside the request, so the approval page can say “the requesting machine reports that its administrative policy forbids permit-port-forwarding” rather than leaving an approver to guess. That report is a claim like every other self-reported field on that page: it is displayed and used to explain, never to decide, and a client that omits it is not refused anything.
Agent preflight verification
Section titled “Agent preflight verification”Before requesting a certificate, ssoossh ssh login verifies that the resolved
key storage will accept and release a key. The preflight runs a real
store-then-remove round trip with a throwaway keypair, to catch storage
failures before the user approves a certificate. That prevents the “approve
then lose it” hazard, where the server issues a certificate the client then
cannot store.
What it does depends on use_agent and fallback_file_agent:
| Settings | Behaviour |
|---|---|
use_agent: true, fallback_file_agent: true (both defaults) |
Probes the live agent. If that fails, probes file storage as a fallback. If file storage works, the login proceeds using files instead of the agent and prints “Agent storage check failed, falling back to file-based key storage.” to stderr. If both fail, the login aborts before requesting a certificate |
use_agent: true, fallback_file_agent: false |
Probes the live agent. If that fails, the login aborts before requesting a certificate, with an error explaining that fallback is disabled |
use_agent: false |
Probes file-based storage directly. If that fails, the login aborts before requesting a certificate |
Locking fallback_file_agent: false across a fleet is therefore a decision
about failure mode as much as about key storage: it turns a broken agent into a
refused login rather than a file on disk.
Windows: Group Policy
Section titled “Windows: Group Policy”The client reads HKLM\SOFTWARE\Policies\com.github.mnestor\ssoossh,
following the conventional Policies\<Vendor>\<Product> layout with
com.github.mnestor as the vendor key – the same identifier the macOS
preference domain is built from.
SOFTWARE\Policies is the conventional location for Group Policy-delivered
settings, and HKLM is writable only by administrators and SYSTEM: the same
trust boundary %ProgramData% gives the enforce file.
There is no ADMX/ADML template yet, so there is no Group Policy Editor UI.
Push the values above with whatever mechanism already manages the fleet’s
registry – Group Policy Preferences, Intune, or a login script. String
settings are REG_SZ; boolean and integer settings are REG_DWORD, booleans
as 0 or 1. A value simply absent from the key is unset, the same as an absent
YAML key.
macOS: managed preferences
Section titled “macOS: managed preferences”The client reads the two plist locations macOS materialises for an installed
configuration profile targeting the preference domain
com.github.mnestor.ssoossh:
| Profile scope | Path |
|---|---|
| Device-scoped | /Library/Managed Preferences/com.github.mnestor.ssoossh.plist |
| User-scoped | /Library/Managed Preferences/<username>/com.github.mnestor.ssoossh.plist |
When both exist, the user-scoped file wins for any key it sets – matching the
user channel outranking the device channel for managed preferences generally.
Push either through an MDM’s Custom Settings or Managed Preferences payload
naming that domain (Jamf, Kandji, Mosyle, Apple Business Manager), or with
profiles install for local testing.
The plist
Section titled “The plist”Write it as XML, as below. macOS rewrites what you upload: every file under
/Library/Managed Preferences is stored as an Apple binary property list,
Apple’s own com.apple.MCX.plist included, so the file on disk will not be
the XML you sent. The client reads both formats and does not care which one
it finds – file -b on a managed Mac reporting Apple binary property list
is the expected state, not a sign the profile is wrong.
This is the payload content, with every supported key set. Upload it as the
custom settings payload for the domain above, or drop it in as the
PayloadContent of a .mobileconfig. Delete the keys you do not want to
enforce: an omitted key leaves the user’s own value in effect, which is not
the same as setting it to the default.
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict> <key>server</key> <string>https://ssh.example.com</string>
<key>capubkey</key> <string>ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... ssoossh-ca</string>
<key>insecure_skip_verify</key> <false/>
<key>use_agent</key> <true/>
<key>fallback_file_agent</key> <true/>
<key>key_filename</key> <string>~/.ssh/ssoossh</string>
<key>try_open_browser</key> <true/>
<!-- Three-state: omit the key entirely to say nothing. Present-and-false is a decision, not the absence of one. --> <key>fips</key> <true/>
<!-- These two key names contain a literal dot; they are flat keys, not a nested <dict>. --> <key>sshkey.type</key> <string>ecdsa</string>
<key>sshkey.size</key> <integer>384</integer>
<key>forbidden_certificate_extensions</key> <array> <string>permit-agent-forwarding</string> <string>permit-port-forwarding</string> </array></dict></plist>Two things that catch people out, both visible above:
sshkey.typeandsshkey.sizeare flat keys containing a dot. They are not a nested<dict>undersshkey. A nested dict is skipped: the parser reads a flat<dict>of scalars and string arrays, so the setting silently does not apply.fipsis three-state. Omitted means unset, and unset is notfalse. Only set it when you intend to decide.
Jamf custom profile schema
Section titled “Jamf custom profile schema”deploy/macos/com.github.mnestor.ssoossh.json
is a Jamf Pro custom profile schema for the same domain, so the settings can be
filled in through the Jamf UI with titles, descriptions and validation instead
of hand-written XML. In Jamf Pro: Computers → Configuration Profiles → New →
Application & Custom Settings → External Applications, source Custom Schema,
preference domain com.github.mnestor.ssoossh, then upload that file.
It follows the conventions used across the
Jamf-Custom-Profile-Schemas
org: propertyOrder, options.infoText naming the raw key, links to this
page, enum/enum_titles for the key algorithm, and the anyOf with a
"Not Configured" null branch for the two settings where unset is meaningfully
different from a value – fips and sshkey.size. remove_empty_properties
is on, so a field left blank in the UI is omitted from the profile rather than
written as an empty value.
The schema describes only what the client reads. It is not validated at load time by anything in this repository, so if you add a key here, add it there too.
The client reads the plist files directly rather than going through
CFPreferencesCopyAppValue, the officially documented API: that call requires
CGo and a macOS SDK, and the release pipeline cross-compiles the macOS build
from Linux CI with CGO_ENABLED=0 and no Apple cross-toolchain. The on-disk
location is not a stable public API, but it has been used directly by other
enterprise Mac tooling for years. If the build pipeline ever moves to a native
macOS runner, switching to CFPreferencesCopyAppValue – which would also add
real CFPreferencesAppValueIsForced detection – is worth revisiting.
Verified on a real Mac: the per-user directory
(/Library/Managed Preferences/<username>/) is not writable by that user, the
same root-and-admin-only write property the Linux and Windows locations have
for /etc and HKLM. A user cannot place their own plist there to defeat the
lock.
Still worth confirming on an MDM-enrolled test Mac if this is deployed against a macOS version not already covered: that the profile really materialises a plist at these exact paths, and that the user-scope value really does outrank the device-scope one when both are set.
No platform-native mechanism, because the enforce file already matches how
Linux fleets are normally provisioned – configuration management tooling
dropping files under root-owned /etc. Point /etc/ssoossh/ssoossh.yaml at an
enforce: target the same way as on the other platforms.
See also
Section titled “See also”- Client configuration for what each setting means and where a user’s own file lives.
ssoossh.yamlreference for every key, its type and its default.