[SELinux] Créer un module pour une application simple

Page content

Avant de commencer, j'aimerais remercier IoT.bzh de m'avoir donné l'opportunité d'écrire cet article.


----------

Prérequis

Vous pouvez lire l’article sur la mise en place d’un environnement SELinux avant cet article.

Dans cet article, toutes les commandes sont lancées sur un serveur fedora 31.

Vous avez besoin d’installer le paquet suivant pour la suite :

$ dnf install make

Modules

Architecture

Un “policy package” (.pp) définit les règles d’une application.

Il peut être installé comme un module dans la politique SELinux.

# En "targeted" mode :

/etc/selinux/targeted/policy/policy.31

Trois fichiers composent ce paquet :

  • Type Enforcement (.te)

  • File Context (.fc)

  • Interfaces (.if)

Fichiers Type Enforcement

Les fichiers “type enforcement” contiennent les règles pour confiner l’application.

Fichiers File Context

Les fichiers “file context” définissent quel label attribuer à chaque fichier.

Fichiers Interface

Les fichiers “interface” contiennent les interfaces pour indiquer comment intéragir avec le module.

Ajouter ou supprimer un paquet

Imaginons un module my_module.pp

Si on veut l’ajouter dans notre politique, on utilise la commande semodule :

$ semodule -i my_module.pp

On peut vérifier qu’il est bien installé avec la commande :

$ semodule -l | grep my_module
...
my_module	1.0

Et on peut le supprimer de la politique avec la commande :

$ semodule -r my_module

Créer une politique

Application

La première chose à faire avant de créer une politique et de bien comprendre le fonctionnement de l’application que l’on veut isoler.

Pour comprendre cela, vous pouvez commencer par lire la documentation et le code.

Dans un deuxième temps, vous pouvez lancer SELinux en mode permissif et regarder les appels systèmes efféctués

Attention, une application peut demander des droits alors qu’elle n’en a pas besoin.

Exemple

Pour cette exemple, on va créer une application qui ouvre un serveur et communique sur le port 1234. Tous les message reçus sont stockés temporairement dans le dossier /tmp.

Le nom de l’application est server et est stockée dans le dossier /opt/server/server.py

# vim /opt/server/server.py

#!/usr/bin/python3

__author__ = "Arthur Guyader"
__copyright__ = "Copyright (C) 2020 'IoT.bzh'"
__license__ = "Apache 2.0"
__version__ = "1.0"
__maintainer__ = "Arthur Guyader"
__email__ = "arthur.guyader@iot.bzh"

import socket
import sys

def create_server():
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.bind(('',1234))
        print("Start server on localhost:1234")
        s.listen(5)
        return s

def wait_connection(s):
        while True:
                print("Wait connection...")
                return s.accept()

def recv_msg(connection, address):
        data = connection.recv(1024)
        print("Receive data from {} = {}".format(address, data))
        if data:
                save_data(data)

def save_data(data):
        file = open("/tmp/server_data.txt","ab")
        file.write(data)
        file.close()

def close_server(connection, s):
        print("Close server")
        if connection:
                connection.close()
        if s:
                s.close()

def main():
        try:
                s = create_server()
                (connection, address) = wait_connection(s)
                print("Connection from : {}".format(address))
                recv_msg(connection,address)
        finally:
                close_server(connection,s)

main()

Création des fichiers

Comme on l’a dit plus tôt, un module est composé de trois fichiers :

Type Enforcement

Commençons par créer un fichier myserver.te dans notre dossier de développement (local) :

# vim ~/selinux/local/myserver.te

###########################################################################
# Copyright 2020 IoT.bzh
#
# author: Arthur Guyader <arthur.guyader@iot.bzh>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
###########################################################################


policy_module(myserver,1.0)
############################

# ------ DECLARATIONS ------ #

#### TYPES ####
type server_t;
type server_exec_t;
type server_log_t;
type server_tmp_t;

#### REQUIRE ####

gen_require(`
	type monopd_port_t;
	type node_t;
')

#### DOMAIN ####
domain_type(server_t)
domain_entry_file(server_t, server_exec_t)

#### LOG ####
logging_log_file(server_log_t)

#### TMP ####
files_tmp_file(server_tmp_t)

# ------ POLICY ------ #

allow server_t server_log_t:file append_file_perms;
allow server_t server_tmp_t:file manage_file_perms;

# Allow create socket
allow server_t server_t:tcp_socket create_stream_socket_perms;

# Allow bind on port 1234
allow server_t monopd_port_t:tcp_socket name_bind;
allow server_t node_t:tcp_socket node_bind;

# Allows server_t to create file in tmp we label server_tmp_t
files_tmp_filetrans(server_t,server_tmp_t,file)

# Allow access tty and devpts
userdom_use_user_terminals(server_t)

# Allow map on bin_t
corecmd_mmap_bin_files(server_t)

Dans ce fichier, on définit les permissions de notre application. On commence par créer quatre nouveaux types :

  • server_t
  • server_exec_t
  • server_log_t
  • server_tmp_t

Le fichier de notre application sera labelisé avec le type server_exec_t et quand on l’exécutera, il prendra le type server_t. On définit ce comportement avec les interfaces domain_type et domain_entry_file.

Les journaux de notre application seront stockés dans un fichier avec le type server_log_t (logging_log_file).

Et les données temporaires seront labélisées avec le type server_tmp_t (files_tmp_file).

Les autres interfaces permettent le fonctionnement de l’application. Nous verrons ensuite comment elles ont été définies.

File Context

Maintenant que les autorisations sont correctement définies, on vient attribuer les bons labels aux différents fichiers.

# vim ~/selinux/local/myserver.fc

###########################################################################
# Copyright 2020 IoT.bzh
#
# author: Arthur Guyader <arthur.guyader@iot.bzh>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
###########################################################################


# server.py will have :
# label : system_u:object_r:server_exec_t
/opt/server/server.py  -- gen_context(system_u:object_r:server_exec_t,s0)

Interface file

Pour finir, on crée un fichier qui définira la manière de s’interfacer avec notre application.

# vim ~/selinux/local/myserver.if

###########################################################################
# Copyright 2020 IoT.bzh
#
# author: Arthur Guyader <arthur.guyader@iot.bzh>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
###########################################################################

interface(`server_domtrans',`
        gen_require(`
                type server_t, server_exec_t;
        ')
        domtrans_pattern($1,server_exec_t,server_t)
')

Installation du module

On se rend dans le dossier ~/selinux/local où ont été créé les fichiers et on vient créer notre module :

$ cd ~/selinux/local
$ make

On vient ensuite l’installer dans notre politique de sécurité :

$ semodule -i myserver.pp

Utilisation

Maintenant que nous avons installé correctement notre module, on veut s’interfacer avec lui.

On va autoriser un utilisateur SELinux staff_u à exécuter l’application.

On va donc créer un fichier “type enforcement” :

# vim ~/selinux/local/mystaff.te

###########################################################################
# Copyright 2020 IoT.bzh
#
# author: Arthur Guyader <arthur.guyader@iot.bzh>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
###########################################################################

policy_module(mystaff, 0.1)

gen_require(`
        type staff_t;
        role staff_r;
        type server_t;
')

# Allow domain transition for staff_t to server_t
server_domtrans(staff_t)

# Allow staff_r role to have the type server_t
role staff_r types server_t;

On va aussi compiler ce module et l’installer :

$ cd ~/selinux/local/
$ make
$ semodule -i mystaff.pp

Si vous aviez créé le fichier server.py avant d’installer le module, il faut relabéliser ces fichiers :

$ restorecon -F /opt/server/server.py
$ ls -Z /opt/server/
system_u:object_r:server_exec_t:s0 server.py

Maintenant notre utilisateur va pouvoir lancer l’application :

$ id -Z
staff_u:staff_r:staff_t:s0
$ /opt/server/server.py
Start server on localhost:1234
Wait connection...
Connection from : ('127.0.0.1', 54726)

❗ WARNING

Si vous lancez l’application comme cela :

$ python3 /opt/server/server.py

Ça ne fonctionnera pas parce que l’application exécutée sera python3. Il n’y a pas de transition de domaine définie pour ce binaire.

Comment définir ces règles

Je vous ai donné des gros fichiers à copier sans vraiment vous montrer comme je les ai fait.

Regardons un peu la méthodologie à avoir pour écrire un module.

On peut diviser le travail en deux étapes :

  • La première partie consiste à déterminer les types et les éléments faciles à trouver (type, domain type, logging_log_file …). Pour créer ces parties, je me suis inspiré de templates existants.

  • La seconde partie consite à y aller étape par étape et de voir les différentes erreurs générées.

Prenons un exemple en supprimant une règle de notre module :

# allow server_t server_t:tcp_socket create_stream_socket_perms;

On le recompile et on le réinstalle :

$ make
$ semodule -i myserver.pp

Quand on va lancer notre application, on obtient une erreur : “permission denied”.

$ /opt/server/server.py 
Traceback (most recent call last):
  File "/opt/server/server.py", line 45, in main
    s = create_server()
  File "/opt/server/server.py", line 14, in create_server
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  File "/usr/lib64/python3.7/socket.py", line 151, in __init__
    _socket.socket.__init__(self, family, type, proto, fileno)
PermissionError: [Errno 13] Permission denied

Pour plus d’informations, on va inspecter le fichier de log /var/log/audit/audit.log :

$ tail -f /var/log/audit/audit.log
type=AVC msg=audit(1587133920.304:426): avc:  denied  { create } for  pid=5910 comm="server.py" scontext=staff_u:staff_r:server_t:s0 tcontext=staff_u:staff_r:server_t:s0 tclass=tcp_socket permissive=0

On peut voir que notre application n’a pas les permisisons create sur une tcp_socket avec un type server_t.

On va donc chercher une définition qui nous donnerait l’autorisation de créer une socket :

$ sefinddef "create.*socket"
define(`create_socket_perms', `{ create rw_socket_perms }')
define(`create_stream_socket_perms', `{ create_socket_perms listen accept }')
define(`create_netlink_socket_perms', `{ create_socket_perms nlmsg_read nlmsg_write }')
define(`rw_netlink_socket_perms', `{ create_socket_perms nlmsg_read nlmsg_write }')
define(`r_netlink_socket_perms', `{ create_socket_perms nlmsg_read }')

Deux définitions resortent plus que les autres :

define(`create_socket_perms', `{ create rw_socket_perms }')
define(`create_stream_socket_perms', `{ create_socket_perms listen accept }')

On se demande maintenant laquelle correspond le plus à notre besoin. Dans notre code, on a un appel système listen et on va donc plutôt choisir create_stream_socket_perms.

On obtient la ligne :

allow server_t server_t:tcp_socket create_stream_socket_perms;

Sources

Vermeulen, S. (2014). SELinux Cookbook : over 70 hands-on recipes to develop fully functional policies to confine yours applications and users using SElinux. Packt Publishing.

Miroslav Grepl. (2013, April 22). Writing SELinux Policy [Slides]. Retrieved from https://mgrepl.fedorapeople.org/PolicyCourse/writingSELinuxpolicy_MUNI.pdf

RefpolicyWriteModule - SELinux Wiki. (n.d.). Retrieved April 17, 2020, from https://selinuxproject.org/page/RefpolicyWriteModule