2

I am trying to understand an sshd configuration that I believe should not work but does. The premise comes from a production system I’m working on; however, I simplified it for my own testing. Since I am unable to explain why this simple example works, I am also unable to explain why the more complex iteration works.

I have two servers, both with users Auser, Buser, and Cuser.

My client machine has an IP address of 192.168.10.1

My server has an sshd configuration that looks like this:

AllowGroups Cuser
Match Address 192.168.10.1
    AllowGroups Cuser Buser
Match Address 192.168.10.1
    AllowGroups Cuser Auser

According to the man pages for sshd_config(5)

Match Introduces a conditional block. If all of the criteria on the Match line are satisfied, the keywords on the following lines override those set in the global section of the config file, un- til either another Match line or the end of the file. If a key- word appears in multiple Match blocks that are satisfied, only the first instance of the keyword is applied.

My interpretation is that from the client (192.168.10.1), only Cuser and Buser should be allowed to ssh to the server.

However when I test this, all three users: Auser, Buser, and Cuser have access. If I look in the sshd logs for the server, I see where each match block is applied:

Jul 25 10:48:59 server1 sshd[3525]: debug3: Trying to reverse map address 192.168.10.1.
Jul 25 10:48:59 server1 sshd[3525]: debug2: parse_server_config: config reprocess config len 854
Jul 25 10:48:59 server1 sshd[3525]: debug3: checking match for 'Address 192.168.10.1' user buser host client addr 192.168.10.1 laddr 192.168.10.4 lport 22
Jul 25 10:48:59 server1 sshd[3525]: debug1: connection from 192.168.10.1 matched 'Address 192.168.10.1' at line 138
Jul 25 10:48:59 server1 sshd[3525]: debug3: match found
Jul 25 10:48:59 server1 sshd[3525]: debug3: reprocess config:139 setting AllowGroups cuser buser
Jul 25 10:48:59 server1 sshd[3525]: debug3: checking match for 'Address 192.168.10.1' user buser host fedora addr 192.168.10.1 laddr 192.168.10.4 lport 22
Jul 25 10:48:59 server1 sshd[3525]: debug1: connection from 192.168.10.1 matched 'Address 192.168.10.1' at line 140
Jul 25 10:48:59 server1 sshd[3525]: debug3: match found
Jul 25 10:48:59 server1 sshd[3525]: debug3: reprocess config:141 setting AllowGroups cuser auser

So, interestingly, from my interpretation of the man pages, I would have expected only the first “reprocess config:139” line to be applied as it is the first instance of the AllowGroups keyword. However, looking at the logs, since I see “reprocess config:141 setting AllowGroups cuser auser”, I might only expect the second instance to be applied.

However, neither of these interpretations seem correct since all three users are able to connect.

So, with some additional testing I changed my sshd_config to look like this:

AllowGroups Cuser
Match Address 192.168.10.1
    AllowGroups Cuser Buser
Match Address 192.168.10.1
    AllowGroups Auser

and

AllowGroups Cuser
Match Address 192.168.10.1
    AllowGroups Auser
Match Address 192.168.10.1
    AllowGroups Cuser Buser

All three users were still able to login.

And one final test

AllowGroups Cuser
Match Address 192.168.10.1
    AllowGroups Buser
Match Address 192.168.10.1
    AllowGroups Auser

Finally, only Auser and Buser have access.

It's almost as if the first Match block will override any default settings, but subsequent match blocks append to any previous match blocks.

2
  • 1
    my understanding is that all match group are tried, and setting done, one by one until success. you could for instance specify an alternative place for authorizedkeys in first block (Cuser, Buser) and generic place (that is no specification) for second.
    – Archemar
    Commented Jul 27, 2022 at 14:03
  • 1
    I worked with AllowGroups, DenyGroups, AlllowUser, and DenyUser six years ago, and found their behavior pretty counter-intuitive. I had to download the source package and grovel through the code to figure it out. I wasn't working with the Allow/Deny parameters in Match clauses, though, so I didn't see this particular behavior that you're observing. I'm curious about this and will try to grab some time and look at the source again.
    – Sotto Voce
    Commented Jul 27, 2022 at 14:24

1 Answer 1

5

I wasn't able to trace through the code as closely as I hoped, but I think I have a sense of what's happening. A very significant clue is in the source code file for the routines that parse the command-line arguments, the sshd_config file, and its included files:

/*
 * The strategy for the Match blocks is that the config file is parsed twice.
 *
 * The first time is at startup.  activep is initialized to 1 and the
 * directives in the global context are processed and acted on.  Hitting a
 * Match directive unsets activep and the directives inside the block are
 * checked for syntax only.
 *
 * The second time is after a connection has been established but before
 * authentication.  activep is initialized to 2 and global config directives
 * are ignored since they have already been processed.  If the criteria in a
 * Match block is met, activep is set and the subsequent directives
 * processed and actioned until EOF or another Match block unsets it.  Any
 * options set are copied into the main server config.
 */

There is essentially one routine that reads the config keywords and arguments (like Compression no and AllowGroups foo bar baz) through the three phases of parsing: command-line arguments, the first pass that skips the Match blocks, and the second pass that reads the Match blocks. There are a couple of flag variables that track which phase the parsing is in - one for the command-line parsing and another for the first and second file passes.

AllowGroups, DenyGroups, AllowUsers, and DenyUsers are parameters that take multiple values rather than a single value. So the routine parses out the list of arguments on the line and appends each argument to an array of strings. While there's a flag for the first and second file passes, there's no flag for "this is a new Match block".

This means when the first Match block is parsed (the start of the second pass) and it contains an AllowGroups parameter, the existing list of values (before the Match blocks) will be wiped and replaced with the new arguments in the Match block. However, nothing signals the routine to wipe the list again when a second Match block also matches and is parsed. If AllowGroups were a single-valued parameter, each new instance would be ignored in favor of the first one. However, since it's a multi-value parameter, the new arguments are instead appended to the list. This is the behavior that puzzled you when you uncovered it. (Me too)

My guess is the authors of the parsing routine didn't envision a config like your test server - having multiple valid Match blocks that set the same multi-value parameter. The more typical case is that only one of the Match blocks will succeed and be parsed because they match different things. Or if multiple blocks succeed, the reason for multiple blocks is because each one sets different parameters.


In my comment a couple of days ago I mentioned that these four parameters work in a way that's not intuitive. Even though it's not directly an answer to this question, I'll add my findings here (for posterity).

After a connection has come in and the second pass config parsing is finished, the code then checks the Allow/Deny groups to see which ones apply. They're checked in this order: DenyUsers, AllowUsers, DenyGroups, and AllowGroups. The test and decision to allow/deny works like this pseudo-code:

if DenyUsers has > 0 items and user is one of them
    deny
if AllowUsers has > 0 items and user is not one of them
    deny
if DenyGroups has > 0 items and user is member of one of them
    deny
if AllowGroups has > 0 items and user is not a member of any of them
    deny

allow

As soon as a 'deny' decision is reached, the routine stops checking further. So the 'AllowUsers' list doesn't work in the intuitive way. If you put any names in the 'AllowUsers' list, all the other users (not in the list) are denied. It doesn't matter if the other users are in the 'AllowGroups' list, because the code stops checking before it consults 'AllowGroups'. And any groups in the 'AllowGroups' list will cause users who don't belong to those groups to be denied, even if they are listed in 'AllowUsers'.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .