openssh-ldap-authkeys
is an implementation of AuthorizedKeysCommand
for OpenSSH 6.9 and newer that allows SSH public
keys to be retrieved from an LDAP source. It's provided for situations where a more robust solution than 1:1 mapping is
needed for users. For example, a large corporation with a significant deployment of Linux servers might use
openssh-ldap-authkeys
to allow specific users or groups to log into the root
account safely and securely. With SSH
keys stored centrally in LDAP, revocation of a compromised key is a quick and painless exercise for the user or IT
department. openssh-ldap-authkeys
allows shared accounts to be fully auditable as to who used them.
This program is a clean-room implementation of a tool I developed internally at a former place of employment. It retains backwards compatibility with the configuration files used by that internal tool, but is unencumbered by any copyrights other than my own.
This program is compatible with OpenSSH 6.2 and up, and works best on OpenSSH 6.9 and later which support the
substitutions %u
, %t
and %k
in an AuthorizedKeysCommand
.
openssh-ldap-authkeys
depends on Python 3 and the following Python libraries:
Dependency installation one-liners for popular distros:
- Fedora (dnf):
dnf -y install python3-dns python3-ldap python3-yaml
- Arch Linux (pacman):
pacman -S python-dns python-ldap python-yaml
- Debian/Ubuntu (apt):
apt-get install -y python3-dns python3-ldap python3-yaml
Run python3 setup.py build
, then as root/sudo python3 setup.py install
.
A puppet module is included if you wish to use that. Otherwise, follow the manual instructions below:
- Create a user for
openssh-ldap-authkeys
:useradd -s /bin/false -d /dev/null -r olak
- Rename the files in
/etc/openssh-ldap-authkeys
, removing the.example
suffix. Make these files owned byroot:olak
, group-readable and with no world permissions (mode0640
). - Edit these files to your needs. See the sections for
olak.yml
andauthmap
below. - To get caching working, copy the file
openssh-ldap-authkeys.tmpfiles
into/usr/lib/tmpfiles.d
, then run (as root)/usr/bin/systemd-tmpfiles --create
. This will create the directory/var/run/ldap/ssh-cache
, owned by userolak
and groupolak
with mode0700
. If you aren't usingsystemd-tmpfiles
or a similar temporary file manager, you'll need to write a script for your init system to create this directory on system startup. - Test OLAK by invoking it as
sshd
would:sudo -u olak /usr/bin/openssh-ldap-authkeys <user>
(where<user>
is a local user account that will be accessible through OLAK) - Add the following lines to
/etc/ssh/sshd_config
:
PermitUserEnvironment yes
AuthorizedKeysCommand /usr/bin/openssh-ldap-authkeys %u %t %k
AuthorizedKeysCommandUser olak
olak.yml
is the main configuration file for openssh-ldap-authkeys
. It tells openssh-ldap-authkeys
how to connect
and bind to the LDAP server, describes the LDAP schema in use in your current environment, configures caching, logging,
and output. The following keys are supported:
Required.
Base distinguished name of your LDAP tree.
Default: none
Default realm (domain) name for LDAP users. Defaults to the domain specified by ldap.basedn
. This should be written
all-uppercase, a convention that was chosen to allow it to resemble Kerberos principals.
Default: generated from your basedn
Whether to attempt to discover the LDAP server using DNS SRV records. If this is set to true
, a domain name will be
generated using the domain components of ldap.basedn
and converted to a DNS domain name. (This can be overridden
using the ldap.srv_discovery_domain
option; see below.) From there, openssh-ldap-authkeys
will attempt to discover
the LDAP server first using the service name _ldaps._tcp
(for LDAP with TLS) and then _ldap._tcp
(for unsecured
LDAP).
An example of LDAP SRV records would be:
_ldaps._tcp.example.com. IN SRV ldap.example.com. 636 0 0
_ldap._tcp.example.com. IN SRV ldap.example.com. 389 0 0
If LDAP server discovery fails, ldap.server_uri
will be used instead. However, if discovery succeeds but the script is
unable to connect to any discovered server, it will not fall back to ldap.server_uri
.
Default: false
Domain name under which to perform service discovery, if different from the basedn.
Defaults to a dynamic value generated from the basedn: a basedn of dc=example,dc=com
will default to the use of
example.com
for this option. If overridden, the default discovery domain is not used.
If this is true
and multiple LDAP servers are discovered, the list of servers is randomized using random.shuffle
before any connections are attempted. If your DNS service returns multiple LDAP servers in a consistent order, changing this to false
risks uneven load distribution between individual LDAP servers. Most deployments should leave this set to true
(the default).
Default: true
Required if ldap.use_dns_srv
is false
.
URI to the LDAP server in the format of protocol://host:port
. Supported protocols include anything supported by the
OpenLDAP C library, including ldap://
, ldaps://
and ldapi://
.
The default TCP ports are 389 for unsecured traffic and 636 for TLS-encrypted LDAP traffic.
Default: none
Distinguished name to use for a simple bind. If omitted, the script will use anonymous bind.
Default: none
Password to be used when performing a simple bind. If omitted, the script will use anonymous bind.
Default: none
SASL method to use to bind to LDAP. If omitted (the default behavior), SASL binding is not attempted.
If this setting and authdn
are both set, a warning is omitted and simple bind settings are ignored.
The following methods are supported (case sensitive):
EXTERNAL
To specify paths to a client certificate and private key, see the tls
-related options below.
Default: none
Timeout for establishing of the LDAP connection.
If this timeout is exceeded and there is a stale cache file, the cache file will be used.
Default: 15
LDAP filter string to use for identifying users.
Default: (objectClass=posixAccount)
LDAP filter string to use for identifying groups.
Default: (objectClass=posixGroup)
Indicates the format of group membership entries. Valid values are:
uid
: Groups specify the member's username (e.g.memberUid: john
). Common in POSIX environments.dn
: Groups specify the member's DN (e.g.member: cn=John Doe,ou=People,o=ACME Corp,dc=acme,dc=com
). Common in Windows/AD environments.
Default: uid
LDAP attribute used for usernames.
In POSIX environments this is typically uid
, and in Windows environments this is typically sAMAccountName
.
Default: uid
LDAP attribute used for SSH keys.
There is no standardized LDAP attribute for storing SSH keys. One common implementation by
Jakub Jirutka uses the attribute
sshPublicKey
.
JumpCloud, a Directory-as-a-Service vendor, uses sshKey
.
Default: sshPublicKey
LDAP attribute for group names. Most schemas use common name (cn
).
Default: cn
LDAP attribute for group members. Typically memberUid
in POSIX environments and member
in Active Directory.
Default: memberUid
LDAP attribute to check whether a user account is in a disabled state. This will be used to exclude disabled accounts from login.
Default: None
Comparison operation to use for checking the disabled state of an account.
eq
,=
or==
: checks thatattribute
is exactly equal tovalue
ne
,neq
, or!=
: checks thatattribute
is not exactly equal tovalue
bitmask
,and
or&
: checks that all the bits set in the fieldvalue
are set in the user's LDAP attribute.
Default: None
Comparison value for the user disable check. Should be a string for eq
or ne
, or an integer for bitmask
comparison.
Default: None
ldap:
attributes:
user_disabled:
attribute: loginShell
op: eq
value: /sbin/nologin
ldap:
attributes:
user_disabled:
attribute: userAccountControl
op: and
value: 2
Control TLS behavior when connecting to the LDAP server. The following values are understood:
prohibit
requires plain-text connections. Only unencrypted connections will be attempted. Specifying this value in combination with anldaps://
server URI will generate an error.noverify
negotiates TLS, but performs no verification whatsoever on the server certificate. Theca_file
andca_dir
options are ignored in this case.allow
attempts to connect using TLS first, and will attemptSTARTTLS
when connecting to a server using the unsecured LDAP port (389) or when using anldap://
URI. IfSTARTTLS
fails, the client will fall back to an unencrypted connection.require
causes the client to reject connections that are not secured by TLS.
Note: The behavior of the allow
option is presently untested and not fully supported.
Default: allow
Path to a file containing root certificates the client will consider valid. If both this setting and tls.ca_dir
are omitted, the client will use the default LDAP library behavior, which typically means using the default certificate storage provided by the OS.
Default: None
Path to a directory containing root certificates the client will consider valid. Files in this directory may need to be named using OpenSSL's hash format xxxxxxxx.n
, where x
is a hexadecimal digit and n
is a decimal number.
If both this setting and tls.ca_file
are omitted, the client will use the default LDAP library behavior, which typically means using the default certificate storage provided by the OS.
Default: None
If these settings are specified, the client will search these paths for a PEM-encoded X.509 certificate and private key to present during the TLS handshake with the LDAP server.
Both options must be specified; otherwise, an error will be generated.
In order to use the client certificate as the authentication vector against the LDAP server, set ldap.sasl_method
to EXTERNAL
.
Default: None
Number of seconds for which a local cache file is valid. During this window, the local cache of SSH keys will be used without the script attempting to reach back out to the LDAP server. This should typically be a shorter window if you wish for key revocation to be effective in emergencies.
Keep in mind that if the LDAP server is unreachable and there is a previous local cache, that cache file will be used even if it's expired. This measure is in place to protect your ability to log into servers in the event that LDAP is down.
Default: 900 seconds (15 minutes)
Directory where SSH keys will be cached. Must be writable only by the user running OLAK (set by the
AuthorizedKeysCommandUser
directive in sshd_config
).
Default: /var/run/ldap/ssh-cache
Allow a stale cache file to be used if any exception occurs when trying to get authorized keys from LDAP. This does pose a small security risk, but is recommended to be left on in the event that your LDAP server is unreachable.
Default: true
Environment variable that will be populated with the user's LDAP username. If they are coming from a realm other than
the default, @REALM
will be appended.
Default: OLAK_USER
Set to true
to write openssh-ldap-authkeys
log messages to standard error in addition to syslog.
The authmap
file is a text file mapping local user accounts and groups to LDAP entities.
The basic format of authmap
is as follows:
local_user:ldapuser1,ldapuser2,ldapuser3
The following conventions are observed:
- Prefixing a name with an ampersand (
&
) causes it to be treated as a group. This means you can authorize everyone in a certain LDAP group to log into a single local, shared account. @all
is a special keyword that translates to any local user account. Use it in the left-hand column to allow an LDAP entity to log in as ANY local user.~self
is a special keyword that translates to the name of the local user being logged into. Use it in the right-hand column to allow a user whose LDAP username matches the local username to log into that local account.- An LDAP username may be suffixed with
@REALM
to search an alternate base DN.
root:&admin
You would use this in combination with an NSS extension like sssd
to populate a local group for users with shell
access.
&shellusers:~self
This could also be called "SSSD emulation mode."
@all:~self
openssh-ldap-authkeys
logs output to syslog with the facility LOG_AUTH
and prefixes all messages with the string
openssh-ldap-authkeys.
. Enabling the logging.to_stderr
option documented above can help isolate problems.
This program is provided under the MIT license.
Copyright (c) 2019 Dan Fuhry
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Written by Dan Fuhry