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