Weirdnesses with the sshd_config Include directive

Brian Candler b.candler at pobox.com
Wed Sep 9 23:31:39 AEST 2026


On 09/09/2026 07:39, M Rubon wrote:
> Am I misunderstanding how Match or -C user= works?

Interesting question. Aside: here I'm on OpenSSH_10.5p1, and the format 
of -T output has changed to be in CamelCase, but otherwise agrees with you:

% sshd -T -f main.conf -h /tmp/test_rsa_hostkey -C user=bigblue | grep 
-i deny
DenyUsers main.before.include.always
DenyUsers include.should.be.always
DenyUsers main.after.include.maybe
% sshd -T -f main.conf -h /tmp/test_rsa_hostkey -C user=xyz | grep -i deny
DenyUsers include.match.user.xyz
DenyUsers main.match.user.xyz

Looking at what's documented in sshd_config(5), firstly in general:

"Unless noted otherwise, for each keyword, the first obtained value will 
be used"

... but DenyUsers is one of those exceptions:

"This keyword may appear multiple times in sshd_config with each 
instance appending to the list."

That is: Multiple DenyUsers lines [implied: in the same scope?] are 
treated as a single concatenated DenyUsers line.

Then for Match blocks:

"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, until either another 
Match line or the end of the file.  If a keyword appears in
multiple Match blocks that are satisfied, only the first instance of the 
keyword is applied."

Here, "override those set in the global section" means complete 
override. If there are one or more DenyUsers lines inside any active 
Match block, they replace any and all DenyUsers in the global config.

I see two potential issues for confusion. Firstly about Include blocks: 
it says "An Include directive may appear inside a Match block to perform 
conditional inclusion", but not what happens the other way round (i.e. a 
Match block inside Include). Clearly the scope of the Match must finish 
at the end of the Include, and I seem to remember there was a change in 
that area recently.

Secondly, what happens to multiple instances of DenyUsers in *different* 
match blocks? In your example there's a "Match user xyz" both in the 
IncludeA.conf and at the end of main.conf, and the DenyUsers are 
merged.  It's as if the two "Match xyz" blocks have been merged to make 
a single scope.  "Only the first instance of the keyword is applied" 
doesn't apply to multiple DenyUsers lines, but it's unclear what happens 
if a second match block also has DenyUsers.

I think it's instructive to change the last two lines of main.conf to:

Match user bob
    DenyUsers main.match.user.xyz         #tracing

Now I get:

% sshd -T -f main.conf -h /tmp/test_rsa_hostkey -C user=bigblue | grep 
-i deny
DenyUsers main.before.include.always
DenyUsers include.should.be.always
DenyUsers main.after.include.maybe
% sshd -T -f main.conf -h /tmp/test_rsa_hostkey -C user=xyz | grep -i deny
DenyUsers include.match.user.xyz
% sshd -T -f main.conf -h /tmp/test_rsa_hostkey -C user=bob | grep -i deny
DenyUsers main.match.user.xyz

This is what I expect. What about if we have overlapping matches? Change 
the last two lines of main.conf to

Match host 1.2.3.4
    DenyUsers main.match.user.xyz         #tracing

Now I get:

% sshd -T -f main.conf -h /tmp/test_rsa_hostkey -C user=bigblue | grep 
-i deny
DenyUsers main.before.include.always
DenyUsers include.should.be.always
DenyUsers main.after.include.maybe
% sshd -T -f main.conf -h /tmp/test_rsa_hostkey -C 
user=bigblue,host=1.2.3.4 | grep -i deny
DenyUsers main.match.user.xyz
% sshd -T -f main.conf -h /tmp/test_rsa_hostkey -C user=xyz | grep -i deny
DenyUsers include.match.user.xyz
% sshd -T -f main.conf -h /tmp/test_rsa_hostkey -C user=xyz,host=1.2.3.4 
| grep -i deny
DenyUsers include.match.user.xyz
DenyUsers main.match.user.xyz

Aha. The DenyUsers from the two Match blocks have been merged, and are 
overriding all the DenyUsers from the global scope.

So in summary, it seems to me that:

1. If there is *any* DenyUser line trigged in a Match block, then that 
wipes out any global DenyUser line(s) (i.e. outside any Match blocks)

2. If there are multiple DenyUsers lines in one or more matching Match 
blocks, they are combined

Cheers,

Brian.

Aside: another possible point of confusion is that DenyUsers is 
"followed by a list of user name patterns, separated by space"; but this 
is *not* a "pattern list" (which would be comma-separated and allow 
negation, such as "DenyUsers !bob"; this tripped me up while I was 
testing this)



More information about the openssh-unix-dev mailing list