Weirdnesses with the sshd_config Include directive

M Rubon rubonmtz at gmail.com
Thu Sep 10 01:36:36 AEST 2026


I missed it, but as you point out sshd_config man about Match talks
about this: "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"

This clearly answers my original question (Match ends at "end of file"
of the include file), and solves the case of the disappearing
DenyUsers (which are overridden by the Match lines).

Thank you very much! That was a very thorough explanation.

M

On Wed, 9 Sept 2026 at 09:31, Brian Candler <b.candler at pobox.com> wrote:
>
> 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