[RFC PATCH v3 01/19] landlock: Support socket access-control
Mikhail Ivanov
ivanov.mikhail1 at huawei-partners.com
Mon Dec 2 11:32:45 UTC 2024
On 11/28/2024 11:52 PM, Mickaël Salaün wrote:
> On Thu, Nov 28, 2024 at 03:01:52PM +0300, Mikhail Ivanov wrote:
>> On 11/27/2024 9:43 PM, Mickaël Salaün wrote:
>>> On Mon, Nov 25, 2024 at 02:04:09PM +0300, Mikhail Ivanov wrote:
>>>> On 11/22/2024 8:45 PM, Günther Noack wrote:
>>>>> Hello Mikhail,
>>>>>
>>>>> sorry for the delayed response;
>>>>> I am very happy to see activity on this patch set! :)
>>>>
>>>> Hello Günther,
>>>> No problem, thanks a lot for your feedback!
>>>>
>>>>>
>>>>> On Mon, Nov 11, 2024 at 07:29:49PM +0300, Mikhail Ivanov wrote:
>>>>>> On 9/4/2024 1:48 PM, Mikhail Ivanov wrote:
>>>>>>> Landlock implements the `LANDLOCK_RULE_NET_PORT` rule type, which provides
>>>>>>> fine-grained control of actions for a specific protocol. Any action or
>>>>>>> protocol that is not supported by this rule can not be controlled. As a
>>>>>>> result, protocols for which fine-grained control is not supported can be
>>>>>>> used in a sandboxed system and lead to vulnerabilities or unexpected
>>>>>>> behavior.
>>>>>>>
>>>>>>> Controlling the protocols used will allow to use only those that are
>>>>>>> necessary for the system and/or which have fine-grained Landlock control
>>>>>>> through others types of rules (e.g. TCP bind/connect control with
>>>>>>> `LANDLOCK_RULE_NET_PORT`, UNIX bind control with
>>>>>>> `LANDLOCK_RULE_PATH_BENEATH`). Consider following examples:
>>>>>>>
>>>>>>> * Server may want to use only TCP sockets for which there is fine-grained
>>>>>>> control of bind(2) and connect(2) actions [1].
>>>>>>> * System that does not need a network or that may want to disable network
>>>>>>> for security reasons (e.g. [2]) can achieve this by restricting the use
>>>>>>> of all possible protocols.
>>>>>>>
>>>>>>> This patch implements such control by restricting socket creation in a
>>>>>>> sandboxed process.
>>>>>>>
>>>>>>> Add `LANDLOCK_RULE_SOCKET` rule type that restricts actions on sockets.
>>>>>>> This rule uses values of address family and socket type (Cf. socket(2))
>>>>>>> to determine sockets that should be restricted. This is represented in a
>>>>>>> landlock_socket_attr struct:
>>>>>>>
>>>>>>> struct landlock_socket_attr {
>>>>>>> __u64 allowed_access;
>>>>>>> int family; /* same as domain in socket(2) */
>>>>>>> int type; /* see socket(2) */
>>>>>>> };
>>>>>>
>>>>>> Hello! I'd like to consider another approach to define this structure
>>>>>> before sending the next version of this patchset.
>>>>>>
>>>>>> Currently, it has following possible issues:
>>>>>>
>>>>>> First of all, there is a lack of protocol granularity. It's impossible
>>>>>> to (for example) deny creation of ICMP and SCTP sockets and allow TCP
>>>>>> and UDP. Since the values of address family and socket type do not
>>>>>> completely define the protocol for the restriction, we may gain
>>>>>> incomplete control of the network actions. AFAICS, this is limited to
>>>>>> only a couple of IP protocol cases (e.g. it's impossible to deny SCTP
>>>>>> and SMC sockets to only allow TCP, deny ICMP and allow UDP).
>>>>>>
>>>>>> But one of the main advantages of socket access rights is the ability to
>>>>>> allow only those protocols for which there is a fine-grained control
>>>>>> over their actions (TCP bind/connect). It can be inconvenient
>>>>>> (and unsafe) for SCTP to be unrestricted, while sandboxed process only
>>>>>> needs TCP sockets.
>>>>>
>>>>> That is a good observation which I had missed.
>>>>>
>>>>> I agree with your analysis, I also see the main use case of socket()
>>>>> restrictions in:
>>>>>
>>>>> (a) restricting socket creating altogether
>>>>> (b) only permitting socket types for which there is fine grained control
>>>>>
>>>>> and I also agree that it would be very surprising when the same socket types
>>>>> that provide fine grained control would also open the door for unrestricted
>>>>> access to SMC, SCTP or other protocols. We should instead strive for a
>>>>> socket() access control with which these additional protocols weren't
>>>>> accessible.
>>>>>
>>>>>
>>>>>> Adding protocol (Cf. socket(2)) field was considered a bit during the
>>>>>> initial discussion:
>>>>>> https://lore.kernel.org/all/CABi2SkVWU=Wxb2y3fP702twyHBD3kVoySPGSz2X22VckvcHeXw@mail.gmail.com/
>>>>>
>>>>> So adding "protocol" to the rule attributes would suffice to restrict the use of
>>>>> SMC and SCTP then? (Sorry, I lost context on these protocols a bit in the
>>>>> meantime, I was so far under the impression that these were using different
>>>>> values for family and type than TCP and UDP do.)
>>>>
>>>> Yeap. Following rule will be enough to allow TCP sockets only:
>>>>
>>>> const struct landlock_socket_attr create_socket_attr = {
>>>> .allowed_access = LANDLOCK_ACCESS_SOCKET_CREATE,
>>>> .family = AF_INET{,6},
>>>> .type = SOCK_STREAM,
>>>> .protocol = 0
>>>> };
>>>
>>> We should indeed include the protocol type in the rule definition.
>>>
>>>>
>>>> Btw, creation of SMC sockets via IP stack was added quite recently.
>>>> So far, creation has been possible only with AF_SMC family.
>>>>
>>>> https://lore.kernel.org/all/1718301630-63692-1-git-send-email-alibuda@linux.alibaba.com/
>>>>
>>>>>
>>>>>
>>>>>> Secondly, I'm not really sure if socket type granularity is required
>>>>>> for most of the protocols. It may be more convenient for the end user
>>>>>> to be able to completely restrict the address family without specifying
>>>>>> whether restriction is dedicated to stream or dgram sockets (e.g. for
>>>>>> BLUETOOTH, VSOCK sockets). However, this is not a big issue for the
>>>>>> current design, since address family can be restricted by specifying
>>>>>> type = SOCK_TYPE_MASK.
>>>
>>> It looks like SOCK_TYPE_MASK is not part of UAPI, which means it could
>>> change with kernel versions (even while being in UAPI in fact). This
>>> new socket creation control should allow to deny any socket creation
>>> known or unknow at the time of the user space program build, and
>>> whatever the available C headers.
>>
>> Agreed
>>
>>>
>>> This also means that Landlock should accept any domain, type, and
>>> protocols defined in rules. Indeed, we don't want to reject rules for
>>> which some protocols are not allowed.
>>
>> Do you mean that Landlock should not make any assumptions about this
>> values during a build time? Currently, patchset provides boundary checks
>> for domain (< AF_MAX) and type (< SOCK_MAX) in landlock_add_rule().
>
> The *running kernel* may not support some socket's domains or types,
> which may be confusing for users if the rule was tested on a kernel
> supporting such domains/types. >
> For the bitmask of domains or types, the issues to keep boundary checks
> would be when a subset of them is not supported. Landlock would reject
> such rule and it would be difficult for users to identify the cause.
Ok, I'll remove these checks.
>
> I'm still wondering if the landlock_append_net_rule()'s -EAFNOSUPPORT
> return value for kernels without CONFIG_INET was a good idea. We should
> probably return 0 in this case, which would be similar to not checking
> socket's domains nor types.
It seems that returning -EAFNOSUPPORT only complicates error checking
for landlock_append_net_rule() from the user's perspective. Probably the
only reason to check the correctness of restricted objects in Landlock
is to provide errors consistency in hooks.
>
>>
>>>
>>> What about using bitmasks for the domain and type fields (renamed to
>>> "domains" and "types")? The last protocol is currently 45/MCTP so a
>>> 64-bit field is enough, and 10/SOCK_PACKET also fits for the last socket
>>> type.
>>>
>>> We cannot do the same with the protocol because the higher one is
>>> 262/MPTCP though. But it looks like a value of 0 (default protocol)
>>> should be enough for most use cases, and users could specify a protocol
>>> (but this time as a number, not a bitmask).
>>>
>>> To sum up, we could have something like this:
>>>
>>> const struct landlock_socket_attr create_socket_attr = {
>>> .allowed_access = LANDLOCK_ACCESS_SOCKET_CREATE,
>>> .families = 1 << AF_INET | 1 << AF_INET6,
>>> .types = 1 << SOCK_STREAM,
>>> .protocol = IPPROTO_SCTP
>>> };
>>
>> Looks good! I think it's a nice approach which will provide a sufficient
>> level of flexibility to define a single rule for a specific protocol (or
>> for related protocols).
>>
>> But, this adds possibility to define a single rule for the set of
>> unrelated protocols:
>>
>> /* Allows TCP, UDP and UNIX sockets. */
>> const struct landlock_socket_attr create_socket_attr = {
>> .allowed_access = LANDLOCK_ACCESS_SOCKET_CREATE,
>> .families = 1 << AF_INET | 1 << AF_INET6 | 1 << AF_UNIX,
>> .types = 1 << SOCK_STREAM | 1 << SOCK_DGRAM,
>> .protocol = 0
>> };
>>
>> Perhaps limiting the addition of one rule to only one address family
>> would be more clear in terms of rule semantics?:
>>
>> /* Allows TCP, UDP, UNIX STREAM, UNIX DGRAM sockets. */
>> const struct landlock_socket_attr create_socket_attrs[] = {
>> {
>> /* Allows IPv4 TCP and UDP sockets. */
>> .allowed_access = LANDLOCK_ACCESS_SOCKET_CREATE,
>> .family = AF_INET,
>> .types = 1 << SOCK_STREAM | 1 << SOCK_DGRAM,
>> .protocol = 0
>> },
>> {
>> /* Allows IPv6 TCP and UDP sockets. */
>> .allowed_access = LANDLOCK_ACCESS_SOCKET_CREATE,
>> .family = AF_INET6,
>> .types = 1 << SOCK_STREAM | 1 << SOCK_DGRAM,
>> .protocol = 0
>> },
>> {
>> /* Allows UNIX sockets. */
>> .allowed_access = LANDLOCK_ACCESS_SOCKET_CREATE,
>> .family = AF_UNIX,
>> .types = 1 << SOCK_STREAM | 1 << SOCK_DGRAM,
>> .protocol = 0
>> },
>> };
>
> Because we are already mixing bitmasks and (protocol) value, I'm not
> sure it will help much. I think in most cases the "families" bitmask
> would handle IPv4 and IPv6 the same (e.g. to only allow TCP with one
> rule). I think this is also required to be able to have a 1:1 mapping
> with SELinux's socket_type_to_security_class().
Ok, agreed
>
>>
>>>
>>>
>>>>>
>>>>> Whether the user is adding one rule to permit AF_INET+*, or whether the user is
>>>>> adding two rules to permit (1) AF_INET+SOCK_STREAM and (2) AF_INET+SOCK_DGRAM,
>>>>> that does not seem like a big deal to me as long as the list of such
>>>>> combinations is so low?
>>>>
>>>> Agreed
>>>
>>> I also agree, but this might change if users have to set a combination
>>> of families, types, and protocols. This should be OK with the bitmask
>>> approach though.
>>>
>>>>
>>>>>
>>>>>
>>>>>> I suggest implementing something close to selinux socket classes for the
>>>>>> struct landlock_socket_attr (Cf. socket_type_to_security_class()). This
>>>>>> will provide protocol granularity and may be simpler and more convenient
>>>>>> in the terms of determining access rights. WDYT?
>>>>>
>>>>> I see that this is a longer switch statement that maps to this enum, it would be
>>>>> an additional data table that would have to be documented separately for users.
>>>>
>>>> This table is the general drawback, since it makes API a bit more
>>>> complex.
>>>>
>>>>>
>>>>> Do you have an example for how such a "security class enum" would map to the
>>>>> combinations of family, type and socket for the protocols discussed above?
>>>>
>>>> I think the socket_type_to_security_class() has a pretty good mapping
>>>> for UNIX and IP families.
>>>
>>> The mapping looks good indeed, and it has been tested for a long time
>>> with many applications. However, this would make the kernel
>>> implementation more complex, and I think this mapping could easily be
>>> implemented in user space libraries with the bitmask approach, if really
>>> needed, which I'm not sure.
>>
>> I agree, implementing this in a library is a better approach. Thanks for
>> the catch!
>>
>>>
>>>>
>>>>>
>>>>> If this is just a matter of actually mapping (family, type, protocol)
>>>>> combinations in a more flexible way, could we get away by allowing a special
>>>>> "wildcard" value for the "protocol" field, when it is used within a ruleset?
>>>>> Then the LSM would have to look up whether there is a rule for (family, type,
>>>>> protocol) and the only change would be that it now needs to also check whether
>>>>> there is a rule for (family, type, *)?
>>>>
>>>> Something like this?
>>>>
>>>> const struct landlock_socket_attr create_socket_attr = {
>>>> .allowed_access = LANDLOCK_ACCESS_SOCKET_CREATE,
>>>> .family = AF_INET6,
>>>> .type = SOCK_DGRAM,
>>>> .protocol = LANDLOCK_SOCKET_PROTO_ALL
>>>> };
>>>>
>>>>>
>>>>> —Günther
>>>>
>>
More information about the Linux-security-module-archive
mailing list