Your MDM can own the SSH policy on a Linux machine and still not decide who gets in. Usually two files it didn't write do.
Disclosure: I work on Swif, which does device management for Linux alongside macOS, Windows and Android. This post is about SSH on managed Linux machines, and about a limit that applies to any Linux management tool, including ours: a policy is only authoritative over the lines it wrote.
On a Linux box, who can SSH in is decided by a handful of plain text files. Your management tool writes some of them. People, installers, cloud-init, old Ansible runs and occasionally attackers write the rest. The dashboard shows you the first group.
There are two places where that gap matters most. Here's how our tooling handles each, including the parts it doesn't cover.
First file: authorized_keys. The policy removes what it added, and nothing else
Our Linux SSH User Policy does what you'd expect. You list usernames and public keys, and the agent creates the accounts if needed and installs the keys on every assigned device. If you remove a user from the list or unassign the policy, the agent removes the keys it gave that user. If Swif created the account, it gets locked, not deleted, so the files stay on the device.
The important part is the next line in our own doc: accounts and keys that Swif didn't add are never changed.
That's on purpose. If a management agent deleted every authorized_keys line it didn't recognize, the first rollout would lock out the on-call engineer, break a backup job's service account, and stop a deploy pipeline. "Only manage what you own" is the only safe default for a tool you're going to assign to machines that already exist.
Now think about what that means for revocation. An engineer gets access through the policy. Six months later, during an incident, a teammate pastes that engineer's laptop key into a service account's authorized_keys so they can debug as that user. Or the engineer adds their desktop key by hand because it was faster than filing a ticket. When that person leaves and you remove them from the policy, the dashboard says done. The hand-added lines still work, because the policy never knew they existed.
NIST covered this ten years ago in IR 7966, its report on SSH access management. Section 4.4, "Backdoor Keys," says creating one is as easy as generating a key pair and adding a line to an authorized keys file, and that because those files are often not audited, "an added key may remain unnoticed for years." The same report notes that many organizations don't know how many SSH keys grant access to their systems or who has copies of them, and that admins often set up new keys without any approval. MITRE ATT&CK tracks the attacker version as T1098.004, SSH Authorized Keys: a persistence technique where the adversary simply adds their own public key.
One more detail makes this worse. A plain authorized_keys line never expires. OpenSSH does support an expiry-time="..." option on the line, but someone has to add it, and almost nobody does. To be honest, our policy doesn't help with that yet. It doesn't accept key options such as from= or command= at the start of a line, so you can't set expiry or source restrictions through it today.
So the policy is how you provision access. It isn't your inventory. We tell customers to treat those as two separate controls. Push access through the policy, so there's one list that's supposed to be the truth. Then regularly read what's actually on disk and diff it against that list. osquery has an authorized_keys table (uid, key, options, comment, key_file), and you can run it across the fleet with Swif's osquery command type. Any key on a device that isn't in a policy is either an undocumented exception that needs an owner, or a problem.
NIST's recommended process is basically the same. Periodically discover every configured authorized key, check each one against an approval, and alert on anything without a match. It also says to identify and remove orphan keys with no known owner, because they may be backdoors.
One caveat about the scan: osquery reads authorized_keys files in user home directories. If someone has changed AuthorizedKeysFile or configured an AuthorizedKeysCommand, keys can come from somewhere else entirely. That leads to the second file.
Second file: sshd_config.d. Whichever file sorts first wins
Our Linux SSHD Policy writes snippets into /etc/ssh/sshd_config.d/. Each rule has a priority from 0 to 99 (default 10), which becomes the filename prefix. A rule named baseline at priority 10 becomes 10-baseline.conf.
Here's the trap. The sshd_config man page says that, unless noted otherwise, for each keyword the first obtained value is used. Include expands wildcards in lexical order. So in sshd config, earlier beats later. If something else has dropped a file that sorts before yours and it sets PasswordAuthentication yes, your PasswordAuthentication no is parsed, valid, and ignored. Our doc says this explicitly: a higher priority number doesn't mean a stronger override. It means a later file, and in sshd a later file is the weaker one. We spell it out because "priority" means the opposite almost everywhere else.
Who drops those other files? Cloud images are the classic case. cloud-init applies its ssh_pwauth setting by writing /etc/ssh/sshd_config.d/50-cloud-init.conf. Other sources include distro packages, a hardening script from three years ago, or a vendor agent. None of them show up in your MDM's policy view, because your MDM didn't write them.
A few more points the doc spells out, because they're commonly misread. Disabling PasswordAuthentication alone doesn't disable KbdInteractiveAuthentication, and with PAM handling passwords, keyboard-interactive can still prompt for one. MaxAuthTries limits attempts per connection, so it isn't an account lockout. ClientAliveInterval detects a dead client, but a responsive client that's just sitting idle stays connected, so it isn't an idle-session timeout either. And our policy doesn't expose PubkeyAuthentication or AuthenticationMethods, so if you need strict key-only or key-plus-MFA, you have to configure and verify those separately.
The only check that tells you the truth is the effective config. sudo sshd -T prints what sshd will actually use after every file is merged, and sshd -T -C user=...,addr=... evaluates Match blocks for a specific user and source address. If your compliance evidence for "SSH password auth is disabled" is "the policy says so," that's a statement about intent. sshd -T output from the device is evidence.
The same pattern shows up in sudo
Briefly, because it's the same lesson. Our Linux Sudoer Policy has rule priorities, and the doc says plainly that priority only resolves conflicts between Swif-managed rules. Anything already in /etc/sudoers or /etc/sudoers.d/, or delivered by another tool, can still grant access. A clean least-privilege policy sitting next to an old "%devs ALL=(ALL) NOPASSWD: ALL" line in sudoers.d is not least privilege.
What I'd take away from this
On macOS and Windows, the OS has a management channel that's designed to be the authority, and it mostly behaves that way. On Linux, the management tool is just one more process writing files, alongside many others. That's not a knock on Linux. It's a big part of why people like it. But it means an honest Linux MDM has two jobs: write the files it owns, and keep reading the files it doesn't.
For SSH, these are the questions I'd want a team to be able to answer for each device. Which authorized_keys lines exist that no policy put there, and who owns each one? What are the effective PasswordAuthentication, KbdInteractiveAuthentication and PermitRootLogin values according to sshd -T, not according to the console? Which files in sshd_config.d and sudoers.d were written by something other than your management tool? And should an SSH server even be running on this machine? That last one is worth checking: our SSH User Policy installs OpenSSH server if it's missing, so assigning it to a laptop group turns on a listener. That's usually fine for servers and often not what you want on laptops.
Curious how others handle this. Do you diff authorized_keys against a source of truth, use SSH certificates so there are no long-lived keys to clean up, or accept the drift and rely on network controls?
Sources:
Swif Help Center, Linux SSH User Policy: help.swif.ai
Swif Help Center, Linux SSHD Policy: help.swif.ai
Swif Help Center, Linux Sudoer Policy: help.swif.ai
Swif Help Center, Run commands by script, osquery, file or Git repo: help.swif.ai
NIST IR 7966, Security of Interactive and Automated Access Management Using Secure Shell (SSH): csrc.nist.gov
MITRE ATT&CK T1098.004, Account Manipulation: SSH Authorized Keys: attack.mitre.org
OpenSSH sshd_config(5) manual (first obtained value, Include ordering): man.openbsd.org
OpenSSH sshd(8) manual (authorized_keys format, expiry-time): man.openbsd.org
osquery schema, authorized_keys table: osquery.io
cloud-init source, sshd_config.d/50-cloud-init.conf handling: github.com/canonical/cloud-init/blob/main/cloudin…
1 point
Mentions
Your MDM can own the SSH policy on a Linux machine and still not decide who gets in. Usually two files it didn't write do. Disclosure: I work on Swif, which does device management for Linux alongside macOS, Windows and Android. This post is about SSH on managed Linux machines, and about a limit that applies to any Linux management tool, including ours: a policy is only authoritative over the lines it wrote. On a Linux box, who can SSH in is decided by a handful of plain text files. Your management tool writes some of them. People, installers, cloud-init, old Ansible runs and occasionally attackers write the rest. The dashboard shows you the first group. There are two places where that gap matters most. Here's how our tooling handles each, including the parts it doesn't cover. First file: authorized_keys. The policy removes what it added, and nothing else Our Linux SSH User Policy does what you'd expect. You list usernames and public keys, and the agent creates the accounts if needed and installs the keys on every assigned device. If you remove a user from the list or unassign the policy, the agent removes the keys it gave that user. If Swif created the account, it gets locked, not deleted, so the files stay on the device. The important part is the next line in our own doc: accounts and keys that Swif didn't add are never changed. That's on purpose. If a management agent deleted every authorized_keys line it didn't recognize, the first rollout would lock out the on-call engineer, break a backup job's service account, and stop a deploy pipeline. "Only manage what you own" is the only safe default for a tool you're going to assign to machines that already exist. Now think about what that means for revocation. An engineer gets access through the policy. Six months later, during an incident, a teammate pastes that engineer's laptop key into a service account's authorized_keys so they can debug as that user. Or the engineer adds their desktop key by hand because it was faster than filing a ticket. When that person leaves and you remove them from the policy, the dashboard says done. The hand-added lines still work, because the policy never knew they existed. NIST covered this ten years ago in IR 7966, its report on SSH access management. Section 4.4, "Backdoor Keys," says creating one is as easy as generating a key pair and adding a line to an authorized keys file, and that because those files are often not audited, "an added key may remain unnoticed for years." The same report notes that many organizations don't know how many SSH keys grant access to their systems or who has copies of them, and that admins often set up new keys without any approval. MITRE ATT&CK tracks the attacker version as T1098.004, SSH Authorized Keys: a persistence technique where the adversary simply adds their own public key. One more detail makes this worse. A plain authorized_keys line never expires. OpenSSH does support an expiry-time="..." option on the line, but someone has to add it, and almost nobody does. To be honest, our policy doesn't help with that yet. It doesn't accept key options such as from= or command= at the start of a line, so you can't set expiry or source restrictions through it today. So the policy is how you provision access. It isn't your inventory. We tell customers to treat those as two separate controls. Push access through the policy, so there's one list that's supposed to be the truth. Then regularly read what's actually on disk and diff it against that list. osquery has an authorized_keys table (uid, key, options, comment, key_file), and you can run it across the fleet with Swif's osquery command type. Any key on a device that isn't in a policy is either an undocumented exception that needs an owner, or a problem. NIST's recommended process is basically the same. Periodically discover every configured authorized key, check each one against an approval, and alert on anything without a match. It also says to identify and remove orphan keys with no known owner, because they may be backdoors. One caveat about the scan: osquery reads authorized_keys files in user home directories. If someone has changed AuthorizedKeysFile or configured an AuthorizedKeysCommand, keys can come from somewhere else entirely. That leads to the second file. Second file: sshd_config.d. Whichever file sorts first wins Our Linux SSHD Policy writes snippets into /etc/ssh/sshd_config.d/. Each rule has a priority from 0 to 99 (default 10), which becomes the filename prefix. A rule named baseline at priority 10 becomes 10-baseline.conf. Here's the trap. The sshd_config man page says that, unless noted otherwise, for each keyword the first obtained value is used. Include expands wildcards in lexical order. So in sshd config, earlier beats later. If something else has dropped a file that sorts before yours and it sets PasswordAuthentication yes, your PasswordAuthentication no is parsed, valid, and ignored. Our doc says this explicitly: a higher priority number doesn't mean a stronger override. It means a later file, and in sshd a later file is the weaker one. We spell it out because "priority" means the opposite almost everywhere else. Who drops those other files? Cloud images are the classic case. cloud-init applies its ssh_pwauth setting by writing /etc/ssh/sshd_config.d/50-cloud-init.conf. Other sources include distro packages, a hardening script from three years ago, or a vendor agent. None of them show up in your MDM's policy view, because your MDM didn't write them. A few more points the doc spells out, because they're commonly misread. Disabling PasswordAuthentication alone doesn't disable KbdInteractiveAuthentication, and with PAM handling passwords, keyboard-interactive can still prompt for one. MaxAuthTries limits attempts per connection, so it isn't an account lockout. ClientAliveInterval detects a dead client, but a responsive client that's just sitting idle stays connected, so it isn't an idle-session timeout either. And our policy doesn't expose PubkeyAuthentication or AuthenticationMethods, so if you need strict key-only or key-plus-MFA, you have to configure and verify those separately. The only check that tells you the truth is the effective config. sudo sshd -T prints what sshd will actually use after every file is merged, and sshd -T -C user=...,addr=... evaluates Match blocks for a specific user and source address. If your compliance evidence for "SSH password auth is disabled" is "the policy says so," that's a statement about intent. sshd -T output from the device is evidence. The same pattern shows up in sudo Briefly, because it's the same lesson. Our Linux Sudoer Policy has rule priorities, and the doc says plainly that priority only resolves conflicts between Swif-managed rules. Anything already in /etc/sudoers or /etc/sudoers.d/, or delivered by another tool, can still grant access. A clean least-privilege policy sitting next to an old "%devs ALL=(ALL) NOPASSWD: ALL" line in sudoers.d is not least privilege. What I'd take away from this On macOS and Windows, the OS has a management channel that's designed to be the authority, and it mostly behaves that way. On Linux, the management tool is just one more process writing files, alongside many others. That's not a knock on Linux. It's a big part of why people like it. But it means an honest Linux MDM has two jobs: write the files it owns, and keep reading the files it doesn't. For SSH, these are the questions I'd want a team to be able to answer for each device. Which authorized_keys lines exist that no policy put there, and who owns each one? What are the effective PasswordAuthentication, KbdInteractiveAuthentication and PermitRootLogin values according to sshd -T, not according to the console? Which files in sshd_config.d and sudoers.d were written by something other than your management tool? And should an SSH server even be running on this machine? That last one is worth checking: our SSH User Policy installs OpenSSH server if it's missing, so assigning it to a laptop group turns on a listener. That's usually fine for servers and often not what you want on laptops. Curious how others handle this. Do you diff authorized_keys against a source of truth, use SSH certificates so there are no long-lived keys to clean up, or accept the drift and rely on network controls? Sources: Swif Help Center, Linux SSH User Policy: help.swif.ai Swif Help Center, Linux SSHD Policy: help.swif.ai Swif Help Center, Linux Sudoer Policy: help.swif.ai Swif Help Center, Run commands by script, osquery, file or Git repo: help.swif.ai NIST IR 7966, Security of Interactive and Automated Access Management Using Secure Shell (SSH): csrc.nist.gov MITRE ATT&CK T1098.004, Account Manipulation: SSH Authorized Keys: attack.mitre.org OpenSSH sshd_config(5) manual (first obtained value, Include ordering): man.openbsd.org OpenSSH sshd(8) manual (authorized_keys format, expiry-time): man.openbsd.org osquery schema, authorized_keys table: osquery.io cloud-init source, sshd_config.d/50-cloud-init.conf handling: github.com/canonical/cloud-init/blob/main/cloudin…