~runiq/selinux-cil-policy-module

fff2e822 — Patrice Peterson 10 days ago
Initial commit

refs

main
browse  log 

clone

read-only
https://git.sr.ht/~runiq/selinux-cil-policy-module
read/write
git@git.sr.ht:~runiq/selinux-cil-policy-module

You can also use your local clone with git send-email.

+++ title = "Einführung in SELinux" date = 2022-05-08 +++

#Notizen

#Einführung in SELinux (Stand: 2022-05-08)

Anfangen mit Dan Walshs Mantra, davon ausgehend ausholen.

Außerdem: In https://danwalsh.livejournal.com/2018.html wird beschrieben, wie der Kernel von kernel_t nach httpd_t übergeht. Außerdem erklärt Dan dort, wieso httpd manchmal als httpd_t und manchmal als unconfined_t läuft. Wenn die Erklärung dort keinen Sinn macht, bitte melden! Ich erachte das als Bug in meiner Anleitung, wenn ihr nicht ausreichend lernt, um die Domänenübergänge in Dans Blogpost zu verstehen.

Da mit der Standard-Policy targeted Nutzer und Rollen eigentlich keine Rolle spielen, kann man Nutzer und Rollen eigentlich ignorieren und sich auf Typen und Ressourcen konzentrieren:

  Linux-Prozess -> Linux-Datei
    |                |
----+---------SELinux+Raum------
    v                v
 SELinux-Typ ---> SELinux-Typ
    |                |
----+-Konzeptioneller+Raum------
    |                |
    v                v
  Quelltyp         Zieltyp

Wenn man die Nutzer und Rollen mit einbeziehen will, fügt dies eine zusätzliche Spalte am Anfang hinzu:

  Linux-Nutzer --?> Linux-Prozess -?> Linux-Datei
    |                 |                 |
----+-----------------+--------- SELinux+Raum------
    |                 |                 |
    v                 |                 |
  SELinux-Nutzer      |                 |
    |                 |                 |
    v                 v                 v
  SELinux-Rolle -?> SELinux-Typ ---?> SELinux-Typ
                      |                 |
----------------------+--Konzeptioneller+Raum------
                      |                 |
                      v                 v
                    Quelltyp          Zieltyp

Aber wie spielen Klassen in dieses Konzept?

  • SELinux ist top-down, benötigt restorcon
    • Benutze chcon/runcon nicht, wenn du nicht weißt, ob du sie wirklich brauchst, da sie dem Top-Down-Konzept von SELinux zuwiderlaufen
  • targeted vs strict vs mls: Sind nicht fundamental unterschiedlich, sie nutzen nur die in SELinux existierenden Konzepte in einer Art und Weise, die ihnen entgegenkommt
  • Auf Dans Video verweisen (im Besonderen die "Gebetsmühle" am Anfang)
  • RBAC in SELinux ist 'unintuitiv': Pro Nutzer nur eine Rolle zur gleichen Zeit, aber er darf wechseln (mit einem Paket, das nicht standardmäßig installiert ist)
    • Ist der Grund dafür, warum wir Rollen und Nutzer haben
  • Lustige Sache: Ab einer bestimmten Stelle wird SELinux einfacher, je mehr man darüber lernt
  • Conventions: _t, _r, _u, nix (für Attribute)
  • Gentoo SELinux Tutorials sind heißer Scheiß, aber sie benutzen ein anderes Sicherheitskonzept (RBAC statt targeted-Policy)
  • "SELinux space" (w/types) <--> "Real space" (w/Linux users, files, ports, etc.)
  • Auch in der targeted-Policy laufen bestimmte (kernelnahe) Prozesse noch alssystem_u:system_r statt als unconfined_u:unconfined_r: ps -eZ | grep -v unconfined_u
  • Innerhalb der Typen keine vernünftige Trennung zwischen Typen für Subjekte (die als Quelle dienen können) und solche für Objekte (die als Ziel dienen) (merkt man, wenn man mal sesearch -As <irgendein-datei-kontext> versucht – da kommt kein Typ als Ergebnis)
  • Orthogonale Konzepte: User, Role, Type sind alles "Typen" und können als solche Attribute haben
  • Typen müssen bestimmte Attribute haben, da sie sonst (in der "targeted"-Policy) nicht wie erwartet im unconfined_t-Kontext funktionieren
  • Statt eine Regel gegen einen Typ zu schreiben, kann man sie gegen ein Attribut schreiben. Diese Regel gilt dann für alle Typen, die das Attribut haben

#Offene Fragen

  • Wie trenne ich die komplexen, anfangs unnötigen Sachen (newrole etc.) von den einfachen, die am Anfang nötig sind?
  • Was ist ein Kontext?
    • Warum hat ein Dateikontext auch einen Nutzertypen? (Sind im Rahmen der targeted-Policy nicht relevant)
  • Wie spielen Booleans in alles rein? Wie kann ich sie visualisieren?
  • Silverblue/Kinoite: Wie komme ich in einer Toolbox an die im Host installierte Policy, damit ich sie mit seinfo/semanage/sesearch inspizieren kann? Indem ich /etc/selinux reinmounte. Das ist offensichtlich sicherheitsrelevant, daher ist es nicht standardmäßig dabei.
  • Soll ich mein Anki-Deck übersetzen, online stellen und darauf verweisen?

#Glossar

s.u.

#SELinux: Grundlegende Regeln

#Tools

Nur die relevanten möglichst orthogonalen behandeln; den Rest vllt. in <details> oder Fußnoten oder einem Anhang abfrühstücken?

#Erstellen eines eigenen SELinux-Policy-Moduls in CIL

SELinux war für mich bisher ein Buch mit sieben Siegeln. Der Grund dafür war nicht zuletzt die Tatsache, dass es zuviele Tools und Befehle gab, mit denen man vertraut werden musste, um das System auch nur halbwegs zu durchblicken.

In den letzten paar Wochen habe ich mit Hilfe einiger weniger Werkzeuge endlich einen Weg durch den SELinux-Dschungel gefunden und glaube jetzt, das Ganze soweit erklären zu können, dass es auch anderen etwas nützen kann. Spoiler: Man muss gar nicht soviel wissen, wie ich am Anfang dachte! Seitdem CIL so gut in das System integriert ist, ist es sehr viel einfacher geworden, mit dem ganzen umzugehen.

#Glossar

In alphabetischer Reihenfolge:

  • CIL: Common Intermediate Language, die Sprache, die wir benutzen werden, um unsere Policy-Module zu definieren.
  • HLL: High Level Language: Die Sprache, in der die Standard-Policy geschrieben ist. Wir werden sie nicht benutzen.
  • Klasse: Die "Sorte" einer Ressource: Datei, Verzeichnis, Port, Socket, Device, etc. Diese dienen SELinux dazu
  • Nutzer: Kann eine oder mehrere Rollen haben, die entscheiden, wie sie:er mit einem System, auf dem SELinux aktiv ist, interagieren darf. Ein Nutzer kann zu einem bestimmten Zeitpunkt immer nur eine Rolle ausüben, aber er kann zwischen den Rollen wechseln, die ihm zugewiesen wurden.
  • Policy: Die Gesamtheit einer auf einem System installierten Regeln
  • Policy-Modul: Ein Teil einer Policy, der gesondert aktiviert/deaktiviert werden kann.
  • Regel: Mit diesen entscheidet SELinux, welche Rechte ein Typ hat.
  • Ressource: Alles, was einen Typ haben kann: Dateien, Ports, (Linux-)Nutzer, Netzwerk-Interfaces, etc.
  • Typ: Eigenschaft einer Ressource, auf Grund der SELinux Zugriffsentscheidungen trifft.
  • Rolle: Definiert die Typen, mit denen ein Nutzer interagieren darf.

Werkzeuge:

  • **seinfo:** Stellt Infos zu Typen, Attributen etc. bereit, die in der momentan aktiven Policy existieren. Standardmäßig nicht in Fedora dabei, da es nur von Leuten benötigt wird, die die Policy und ihre Module administrieren.
  • **semanage:** Ein Werkzeug zum Interagieren mit Ressourcen. Es legt fest, welchen Typen sie zugeordnet werden. Da die Informationen, die es bereitstellt, potenziell sicherheitsrelevant sind, benötigt man für seine Benutzung Superuser-Rechte.
  • **semodule:** Interagiert mit den Modulen, aus der eine Policy besteht. Man kann damit Module auflisten, neue Module laden und entfernen, Module aus der Policy exportieren (als HLL oder CIL) und Weiteres. Auch dieses Werkzeug benötigt (aus offensichtlichen Gründen) Superuser-Rechte.
  • **sesearch:** Dient dem Durchsuchen der Regeln, die von einer Policy definiert werden. Standardmäßig ebenfalls nicht in Fedora dabei, aus dem gleichen Grund wie bei *seinfo*.

#Einführung in SELinux (Stand: 2022-05-08)

Eins vorneweg: Auch wenn dieser Weg relativ einfach ist, besteht er immer noch aus den folgenden Komponenten (in ~absteigender Reihenfolge ihrer Wichtigkeit beim Entwickeln eines eigenen Policy-Moduls):

Benutzt CIL, es hat eine Referenz und ist mMn weniger kompliziert zu benutzen als HLL. SELinux kann seit Version 2.4 problemlos mit CIL-Modulen umgehen: Sie installieren, aus der Policy extrahieren, etc.

Was ich enorm wichtig finde, ist, dass man SELinux-Module, die bereits in der Policy sind, auch wieder als CIL exportieren kann. Das ermöglicht einem, die vordefinierten Module nach Code-Beispielen zu durchsuchen, die man für eigene Policy-Module adaptieren kann. Ein kleiner Bonus: Wenn man die selbst erstellten Module exportiert, enthalten diese auch alle Kommentare.

Im folgenden möchte ich an Hand eines fiktiven Anwendungsfalls beschreiben, wie die Erstellung eines CIL-Policy-Moduls aussieht.

#Erstellen eines eigenen SELinux-Policy-Moduls in CIL

#Ausgangssituation

Ich habe das (fiktive) Programm test geschrieben:

#!/bin/sh
echo 'Inhalt von /foo:'
cat /foo
nc -lvp "${1:-12354}"

Es hat folgende Eigenschaften:

  1. Es liest die Datei /foo.
  2. Es lauscht auf dem Port 12354.

#Ziel

Ich möchte das Programm in einem Podman-Container laufen lassen:

$ podman run --init --rm -it \
    --name testcontainer \
    -v ./test:/usr/local/bin/test:Z,ro \
    -v /tmp/testcontainer/foo:/foo:ro \
    -p 12354:12354 \
    alpine test

Dabei möchte ich das Programm so gut wie möglich abschotten:

  1. Das Programm soll nur die Datei /tmp/testcontainer/foo lesen dürfen, sonst nichts (es sei denn, etwas wurde mit :Z oder :z extra für den Container relabelt, wie in diesem Fall das Programm test selbst).
    1. Das Programm soll die Datei /foo nicht schreiben dürfen, nur lesen.
    2. Die Datei soll beim Anlegen automatisch den Typ testcontainer.foo_file bekommen, ohne dass der Administrator sie mit restorecon oder chcon bearbeiten muss.
    3. Andere Prozesse dürfen die Datei nicht benutzen.
  2. Der Prozess soll nur auf Port 12354 lauschen dürfen.
    1. Andere Prozesse dürfen Port 12354 nicht benutzen.

Hinweis: Wenn ich schreibe "andere Prozesse dürfen die Datei/den Port nicht benutzen", dann sind davon natürlich diejenigen Prozesse auf dem Host-System ausgenommen, die kein dediziertes SELinux-Policy-Modul haben und daher als uncontained_t, also ohne SELinux-Abschottung, laufen.

#Weg

  1. Container starten (so, dass er mit SELinux funktioniert). Man könnte das auch machen, indem man container_runtime_t – die SELinux-Domain, in der Podman läuft – auf permissive setzt:
    $ sudo semanage permissive -a container_runtime_t
    
    Nicht vergessen, die Domain am Ende wieder aus der Permissive-Liste zu entfernen:
    $ sudo semanage permissive -d container_runtime_t
    
    Dann starten wir den Container:
    $ touch /tmp/testcontainer/foo
    $ podman run --init --rm -it \
        --name testcontainer \
        -v ./test:/usr/local/bin/test:Z,ro \
        -v /tmp/testcontainer/foo:/foo \
        -p 12354:12354 \
        alpine test
    
  2. Um das initiale SELinux-Modul für unseren Container durch zu erzeugen, lassen wir den laufenden Container durch Udica analysieren:
    $ podman inspect testcontainer | sudo udica -j - testcontainer
     
    Policy testcontainer created!
     
    Please load these modules using:
    # semodule -i testcontainer.cil /usr/share/udica/templates/{base_container.cil,net_container.cil}
     
    Restart the container with: "--security-opt label=type:testcontainer.process" parameter
    
    Den Befehl, den Udica zum Laden mitteilt, sollte man in ein Makefile (oder Justfile) schreiben:
    install: testcontainer.cil
        sudo semodule -i testcontainer.cil /usr/share/udica/templates/{base_container.cil,net_container.cil}
    
  3. Die generierte Policy sieht folgendermaßen aus. Kommentare – die in CIL mit einem ; beginnen – wurden von mir eingefügt.
    ; Die Syntax ist vergleichbar mit LISP und besteht ausschließlich aus
    ; S-Ausdrücken. Kommentare werden mit einem Semikolon eingefügt.
    ; Die Reihenfolge der Statements in einem Block ist irrelevant.
     
    ; Eröffnet einen neuen Namensraum für unsere Policy. Sollte am besten so heißen
    ; wie der Dienst.
    ;
    ; Alle Namen, die innerhalb dieses Blocks deklariert haben, wird der Name des
    ; Blocks vorangestellt. Wenn wir beispielsweise innerhalb des Blocks einen Typ
    ; "process" anlegen, wird der für SELinux am Ende den Namen
    ; "testcontainer.process" haben.
    (block testcontainer
        ; Importiert die Regeln aus dem Block "container" für unseren
        ; "testcontainer"-Block. Der "container"-Block ist unter
        ; /usr/share/udica/templates/base_container.cil zu finden. Der
        ; SELinux-Compiler kann ihn nicht selbsttätig finden, was der Grund dafür
        ; ist, dass man später beim Installieren den Pfad für alle benutzten
        ; CIL-Dateien mit angeben muss.
        (blockinherit container)
        ; Ähnlich: Importiert die Regeln aus dem Block "restricted_net_container"
        ; für unseren "testcontainer"-Block. "restricted_net_container" liegt in
        ; /usr/share/udica/templates/net_container.cil, den wir beim Kompilieren
        ; ebenfalls mit angeben müssen.
        (blockinherit restricted_net_container)
     
        ; allow-Regeln setzen fest, was SELinux einem Prozess erlaubt.
        ; Alles, was nicht ausdrücklich erlaubt ist, ist verboten.
     
        ; Hier sagt SELinux, dass der Prozess "testcontainer.process" eine Menge
        ; Capabilities bekommen darf. Wenn man die Namen vollständig ausschreiben
        ; würde, sähe die Regel so aus:
        ;
        ; (allow testcontainer.process testcontainer.process ( capability ( chown dac_override fowner fsetid kill net_bind_service setfcap setgid setpcap setuid sys_chroot )))
        (allow process process ( capability ( chown dac_override fowner fsetid kill net_bind_service setfcap setgid setpcap setuid sys_chroot )))
     
        ; Hier steht, dass der Prozess "testcontainer.process" auf allen Ports, der
        ; nicht für andere Dinge reserviert sind, lauschen kann. Diese Regel hat
        ; Udica aus unserem Port-Mapping "-p 12354:12354" erstellt, da 12354 noch
        ; für keinen Dienst reserviert ist.
        ; Welche Ports nicht reserviert sind, kann man mit "sudo semanage port -l |
        ; grep unreserved_port_t" herausfinden.
        (allow process unreserved_port_t ( tcp_socket (  name_bind )))
     
        ; Diese Regel sagt, dass "testcontainer.process" die aufgelisteten Rechte
        ; auf Verzeichnisse hat, die den Typ "user_home_t" haben. Da wir Podman
        ; nicht gesagt haben, dass wir die Datei readonly mounten wollen, ist Udica
        ; davon ausgegangen, dass wir sie auch beschreiben wollen, und generiert
        ; eine Regel mit Schreibrechten.
        (allow process user_home_t ( dir ( add_name create getattr ioctl lock open read remove_name rmdir search setattr write )))
        ; Ähnlich für Dateien vom Typ "user_home_t"…
        (allow process user_home_t ( file ( append create getattr ioctl lock map open read rename setattr unlink write )))
        ; Und FIFOs (Pipes)…
        (allow process user_home_t ( fifo_file ( getattr read write append ioctl lock open )))
        ; Und Sockets.
        (allow process user_home_t ( sock_file ( append getattr open read write )))
    )
    
  4. Die Policy kann nun angepasst werden. Hierfür ist der CIL Reference Guide nützlich. Das (kommentierte) Ergebnis meiner Anpassungen, ohne die vorherigen Kommentare:
    (block testcontainer
        (blockinherit container)
        (blockinherit restricted_net_container)
     
        ; Unser Prozess benötigt keine Capabilities, daher können wir die
        ; Capabilities-Regel löschen.
     
        ; Den Port 12354 würden wir gern als "testcontainer.foo_port" kennzeichnen,
        ; damit wir festlegen können, dass der Container nur diesen einen
        ; spezifischen Port nutzen darf. Dazu müssen wir einen neuen Typ
        ; deklarieren:
        (type foo_port)
        ; Und SELinux erklären, dass das ein Typ für einen Port ist (und nicht für
        ; eine Datei, oder ein Verzeichnis, etc.), damit es die richtigen Rechte
        ; für die "normale" Benutzung als unbeschränkter Nutzer setzt:
        (typeattributeset port_type (foo_port))
        ; Zudem müssen wir den Typ unserer "Standardrolle" "unconfined_r" zuweisen,
        ; da wir den Typen als normaler Nutzer sonst überhaupt nicht anfassen
        ; dürfen:
        (roletype unconfined_r foo_port)
        ; Die folgende Regel sagt schließlich, dass der TCP-Port 12354 den Typ
        ; "foo_port" haben soll.
        (portcon tcp 12354 (unconfined_u unconfined_r foo_port ((s0) (s0))))
        ; Der Rest des sogenannten "Sicherheitskontextes" ("unconfined_u
        ; unconfined_r", "((s0) (s0))") ist immer gleich, solange man bei der
        ; Fedora-Standard-SELinux-Policy "targeted" bleibt und nicht mit Nutzern
        ; und Rollen herumspielt.
        ;
        ;Den Typen eines bestimmten Ports kann man mit "sepolicy network -p <port>"
        ;herausfinden.
     
        ; Dann erlauben wir dem Typ "testcontainer.process", den Port zu benutzen:
        (allow process foo_port (tcp_socket (name_bind)))
     
        ; Ähnlich dazu würden wir die Datei "/tmp/testcontainer/foo" gern als
        ; besonderen Typ "testcontainer.foo_file" markieren, damit wir festlegen
        ; können, dass wir nur darauf zugreifen können und auf sonst nichts. Dazu
        ; müssen wir zunächst wieder einen neuen Typ "testcontainer.foo_file"
        ; definieren:
        (type foo_file)
        ; Und SELinux erklären, dass das ein Typ für eine Datei ist, damit es die
        ; richtigen Rechte für die "normale" Benutzung setzt:
        (typeattributeset file_type (foo_file))
        ; Genau wie bei "foo_port" müssen wir festlegen, dass der Typ
        ; "testcontainer.foo_file" von einem normalen Nutzer mit der Rolle
        ; "unconfined_r" angefasst werden darf:
        (roletype unconfined_r foo_file)
        ; Und dann – auch ähnlich wie beim Port – festlegen, dass der Pfad
        ; "/tmp/testcontainer/foo" diesen Typ bekommen kann:
        (filecon "/tmp/testcontainer/foo" any (unconfined_u unconfined_r foo_file ((s0) (s0))))
        ; Auch hier ist der Rest des Sicherheitskontextes immer gleich, solange man
        ; bei der Fedora-Standard-SELinux-Policy "targeted" bleibt und nicht mit
        ; Nutzern und Rollen herumspielt.
        ;
        ; Die vollständigen Kontexte von Dateien kann man mit "ls -Z" finden.
     
        ; Wir adaptieren die allow-Regel, die Udica uns für Dateien des Typs
        ; "user_home_t" erzeugt hat, für unseren neuen Typ
        ; "testcontainer.foo_file".
        ;
        ; Zudem wollen wir die Datei nur lesen und nicht schreiben, also entfernen
        ; wir die meisten Rechte aus der Liste, die wir vorher für Dateien des Typs
        ; "user_home_t" hatten, und ändern den Zieltyp auf "foo_file".
        (allow process foo_file (file (getattr ioctl lock open read)))
     
        ; Prinzipiell ist damit alles erledigt: Mit "restorecon
        ; /tmp/testcontainer/foo" kann man der Datei "/tmp/testcontainer/foo" nun
        ; das von diesem Policy-Modul festgelegte Label "testcontainer.foo_file"
        ; verliehen werden.
        ;
        ; Dies lässt sich zum Glück auch automatisch bewerkstelligen, damit der
        ; Administrator nicht bei jeder Datei selbst Hand anlegen muss: Mit der
        ; folgenden Regel bekommt die Datei "/tmp/testcontainer/foo" automatisch
        ; den richtigen Typ "testcontainer.foo_file" zugewiesen, solange sie von
        ; einem "normalen" Nutzerprozess mit Typ "unconfined_t" angelegt wird.
        ; "user_tmp_t" ist hierbei der Typ des Elternverzeichnisses, der ebenfalls
        ; passen muss – wenn das Verzeichnis "/tmp/testcontainer" einen anderen Typ
        ; als "user_tmp_t" hätte, würde das automatische Labeln nicht greifen.
        (typetransition unconfined_t user_tmp_t file foo_file)
        ; Typenübergänge sollten für jeden passenden Dateitypen definiert werden.
     
        ; FIFOs und Sockets benötigen wir nicht, also entfernen wir deren
        ; allow-Regeln komplett.
    )
    
  5. Das Policy-Modul kann nun in die System-Policy installiert werden. Hierzu benutzt man den Befehl, den Udica vorhin mitgeteilt hat:
    $ sudo semodule -i testcontainer.cil /usr/share/udica/templates/{base_container.cil,net_container.cil}
    
  6. Wenn es keine Fehler in der Policy gab (der CIL-Compiler sagt einem hilfreicherweise, auf welcher Zeile der Fehler ist), kann man das geladene Modul daraufhin in der Policy finden:
    $ sudo semodule -l | grep testcontainer
    testcontainer
    
    Achtung: Der Name des Moduls richtet sich immer nach der Name der verwendeten CIL-Datei, nicht nach dem Namen des Blocks. Aus diesem Grund sollte man den Block und die Datei stets gleich benennen.
  7. Nun kann man die Testdatei neu anlegen, die automatisch mit dem richtigen Label versehen wird:
    $ rm /tmp/testcontainer/foo
    $ touch /tmp/testcontainer/foo
    $ ls -Z /tmp/testcontainer/foo
    unconfined_u:object_r:testcontainer.foo_file:s0 /tmp/testcontainer/foo
    
  8. Wenn man sein Modul aus einer Policy als CIL-Datei haben will:
    $ sudo semodule -cE testcontainer
    Extracting at highest existing priority '400'.
    
    Danach liegt das Modul als testcontainer.cil im momentanen Verzeichnis.
  9. Wenn man sein Modul später aus der Policy entfernen will:
    $ sudo semodule -r testcontainer
    

#Ergebnis

Kommentare rausgenommen, sonst das gleiche wie oben.

(block testcontainer
    (blockinherit container)
    (blockinherit restricted_net_container)
 
    (type foo_port)
    (typeattributeset port_type (foo_port))
    (roletype unconfined_r foo_port)
    (portcon tcp 12354 (unconfined_u unconfined_r foo_port ((s0) (s0))))
    (allow process foo_port (tcp_socket (name_bind)))
 
    (type foo_file)
    (typeattributeset file_type (foo_file))
    (roletype unconfined_r foo_file)
    (filecon "/tmp/testcontainer/foo" any (unconfined_u unconfined_r foo_file ((s0) (s0))))
    (allow process foo_file (file (getattr ioctl lock open read)))
    (typetransition unconfined_t user_tmp_t file foo_file)
)

#Referenzen