News
Das Neuste aus der Welt von Adfinis SyGroup
Wer viel mit diversen Linux Systemen arbeitet und diese noch manuell aufsetzt, kennt das Problem, dass immer das entsprechende Installationsmedium fehlt. Weiter wäre immer ein Rescue System zu haben auch hilfreich, viele nutzen dafür Live Systeme wie grml, Kali oder Damn Small Linux. Jedes dieser Systeme hat aber Einschränkungen, z.B. ist teilweise eine Ausgabe auf der seriellen Konsole zwingend, jedoch vom entsprechenden System nicht unterstützt.
In diesem Artikel wird kurz ein Systemimage präsentiert, welches im Frust von zu vielen USB Sticks für Installations- und Live-Systemen entstand. Die Anforderungen waren:
- “Live”-System für Rescue
- Installations-Medium für mehrere gängige Distributionen
- Einfach die neusten Images einspielen
- Keine eigenen Anpassungen an den diversen Systemen
Ansatz
Auf einem USB Stick wird ein Arch Linux installiert, welches mithilfe von Grub bootet. Grub bietet neben dem Arch Linux auch die Möglichkeit andere Systeme zu booten oder zu installieren.
Grub und das “Live” Arch Linux werden auf der seriellen und graphischen Konsole dargestellt. Wo möglich trifft dies auch auf die anderen Systeme zu (was nicht immer möglich ist, da z.B. ein Kali Linux nur graphisch startet). Das “Live” Arch Linux wird nicht als echtes Live-System aufgebaut, sondern ist weiterhin Read-Write, da somit auch zusätzliche Pakete und Updates installiert werden können.
Eine aktuelle Liste der Systeme, welche mit diesem Image gebootet werden können, ist folgende:
- “Live” Arch Linux (Rescue System; Read-Write)
- Arch Linux Installer
- Debian Jessie Netinstall
- Debian Jessie Preseed Install (mit lokalem Preseed-File und Netinstall für die Pakete)
- CentOS 7 Installer
- Kali Linux
- Tails
Probleme
Die verschiedenen Systeme erwarten unterschiedliche Kernel-Parameter, dies bereitete des öfteren Probleme, da Systeme nicht korrekt oder gar nicht booteten. Eine grosse Hilfe war hierbei das Arch Linux Wiki, welches einen Artikel zu diesem Thema hat.
Die diversen “Live” Systeme werden in der /etc/grub.d/40_custom
eingetragen. Dies passiert manuell, je nach System wird direkt der Kernel und das InitramFS gestartet oder es wird zuerst ein ISO Image Loopback gemounted. Zwei Beispiele, welche genau diese zwei Fälle abbilden, sind hier gezeigt:
- CentOS 7 Installer Menuentry
menuentry 'CentOS 7 Installer' {
set kernel='/centos-7-vmlinuz'
set initrdfile='/centos-7-initrd.gz'
set append='console=ttyS0,115200n8 devfs=nomount'
set ip='ip=192.168.1.10::192.168.1.1:255.255.255.0:host.example.com:enp1s0:off nameserver=8.8.8.8'
set repo='repo=http://pkg.adfinis.com/centos/7/os/x86_64'
set vnc='vnc vncpassword=password'
linux $kernel $append $ip $repo $vnc --
initrd $initrdfile
}
- Kali Linux Installer Menuentry
menuentry 'Kali Linux 2016.2 amd64' {
set isofile='/kali-linux-2016.2-amd64.iso'
loopback loop $isofile
linux (loop)/live/vmlinuz boot=live findiso=$isofile noconfig=sudo username=root hostname=kali
initrd (loop)/live/initrd.img
}
Die entsprechenden Config Optionen sind in der Grub Dokumentation beschrieben. Welche Parameter einem Kernel mitgegeben werden können, hängt zum einen von der Kernel-Version ab, zum anderen von der Distribution. Eine Liste von möglichen generischen Parametern ist auf kernel.org abrufbar.
Image
Das Image wird öffentlich zur Verfügung gestellt. Es wird keine Haftung zur Aktuallität oder allfälligen Problemen übernommen:
https://files.adfinis.com/rescue-multiboot/rescue-multiboot-latest.img.gz
Nach dem Herunterladen kann das Image auf einen USB Stick transferiert werden. Der USB Stick muss mindestens 8 GB Speicherplatz bieten. Dabei werden alle Daten auf dem USB Stick gelöscht. Ist das Image auf den USB Stick transferiert, wird die Partition noch auf das Maximum vergrössert. Der String /dev/sd<X>
muss dabei unbedingt durch die Disk des USB Sticks ersetzt werden.
$ DISK="/dev/sd<X>"
$ gzip -cd rescue-multiboot-latest.img.gz | dd of="$DISK" bs=8M
$ printf "d\nn\np\n1\n2048\n\nw\n" | fdisk "$DISK"
$ e2fsck -f "${DISK}1"
$ resize2fs "${DISK}1"
Updates
Der USB Stick sollte gelegentlich aktualisiert werden. Vorbereitend sollte dafür das “Live” Arch Linux gestartet werden.
Nach dem Start wird jeweils eine Netzwerk Verbindung hergestellt. Die vorhandenen Interfaces können mit ip addr show
dargestellt werden. Das Interface muss teilweise noch mit ip link set $INTERFACE up
gestartet werden. Danach wird eine IP-Adresse am einfachsten über Ethernet per DHCP geholt, dafür einfach dhcpcd $INTERFACE
starten. Anschliessend sollte das System eine IP-Adresse haben und kann mit pacman -Syu
aktualisiert werden.
Komplexere Netzwerk Verbindungen können mithilfe von netctl
hergestellt werden. Entsprechende Konfigurationsbeispiele sind im Verzeichnis /etc/netctl/examples
zu finden und müssen nach /etc/netctl
kopiert und danach angepasst werden. Starten kann man diese mit netctl start $CONFIGNAME
. Die verschiedenen Images zum Installieren oder Live-Booten sind im Root Verzeichnis. Sie können simpel aktualisiert werden, indem die jeweils aktuellsten Versionen der ISOs, Kernel und initrd Images heruntergeladen werden. Es gibt keine Anpassungen an den Images oder initrd Images.
Um Debian-Pakete automatisiert zu bauen, haben wir uns entschieden, GitLab CI einzusetzen. GitLab CI bietet die Möglichkeit Tasks aufgrund von definierbaren Ereignissen, wie zum Beispiel Git-Tags, auszuführen.
Wir haben einen generischen Docker-Container erstellt, der die Basis-Tools für den Paketbau enthält und von GitLab verwendet wird, um das Paket zu bauen. Der Docker-Container erlaubt es, Updates in der Build-Umgebung einfach einzuspielen, da der Container einfach durch einen neuen ersetzt werden kann.
Nachfolgend wird die automatisierte Paketierung des Log Analyse Tools GoAccess gezeigt. Viele Tools sind jeweils nicht in der neusten Version paketiert, womit selbst Hand angelegt werden muss.
Debian-Paket vorbereiten
Als erstes werden die Dateien erstellt, welche den Bau des Debian-Pakets steuern. Im Fall von GoAccess sind das die folgenden:
debian/changelog # Änderungen am Paket und der Software
debian/compat # Kompatibilitätslevel für debhelper
debian/control # Paketspezifische Angaben wie Abhängigkeiten und Beschreibung
debian/rules # Anweisungen für debhelper
Debian selbst bietet bereits eine ausführliche Dokumentation an, welche den Einstieg in die Paketierung vereinfacht.
Docker-Container vorbereiten
Auf einem Hostsystem muss ein Container vorbereitet werden, in dem das Paket danach gebaut werden kann. Dazu wird zuerst das Dockerfile
erstellt:
FROM debian:wheezy
ADD setup.sh /opt/
RUN /bin/bash /opt/setup.sh
Im Dockerfile
(offizielle Dokumentation) wird angegeben, welches Basis-Image verwendet werden soll. In diesem Fall ist das Debian Wheezy. Danach wird das Script setup.sh
in das Verzeichnis /opt/
des Containers kopiert.
In setup.sh
wird der zu verwendende Mirror konfiguriert sowie die grundlegendsten Abhängigkeiten installiert, die bei jedem Build verwendet werden können:
#!/bin/sh
# change to our own mirror
echo "deb http://pkg.adfinis.com/debian/ wheezy main non-free contrib" > /etc/apt/sources.list
echo "deb http://security.debian.org/ wheezy/updates main" >> /etc/apt/sources.list
echo "deb http://pkg.adfinis.com/debian/ wheezy-updates main contrib non-free" >> /etc/apt/sources.list
# requirements
apt-get update
apt-get -y install git dh-make build-essential autoconf autotools-dev
Sobald diese Dateien vorbereitet sind, kann man den Docker-Container bauen:
$ docker build -t generic-package-build-runner:v1 .
Der Docker-Container ist nun erstellt und kann verwendet werden.
GitLab CI konfigurieren
Jetzt muss der vorbereitete Docker-Container für das aktuelle Projekt registriert werden, in dem ein Paket gebaut werden soll:
$ gitlab-ci-multi-runner register \
--non-interactive \
--url "$(GITLAB_URL)" \
--registration-token "$(CI_TOKEN)" \
--description "Generic debian wheezy package build runner" \
--executor "docker" \
--docker-image "generic-package-build-runner:v1"
Die GitLab URL und das CI Token können im GitLab-Projekt auf der Seite “Settings” > “Runners” eingesehen werden. Jedes Projekt hat sein eigenes CI-Token.
Damit GitLab CI weiss, welche Befehle im Container ausgeführt werden müssen, wird die Datei .gitlab-ci.yml
innerhalb des Repositories erstellt.
# Wird vor den Scripts in den Stages ausgeführt
before_script:
- source /etc/profile
# Definiert Stages, die ausgeführt werden sollen
stages:
- build
# Stage "build"
run-build:
stage: build
script:
- apt-get install -y libncurses5-dev libglib2.0-dev libgeoip-dev libtokyocabinet-dev zlib1g-dev libncursesw5-dev libbz2-dev
- autoreconf -fvi
- cp COPYING debian/copyright
- dpkg-buildpackage -us -uc
- mkdir build
- mv ../goaccess*.deb build/
# Diese Stage wird nur bei neuen Tags ausgeführt
only:
- tags
# Die Dateien, welche in GitLab zur Verfügung gestellt werden sollen
artifacts:
paths:
- build/*
Der wichtigste Teil dieser Datei ist die Stage run-build
. In diesem Teil wird definiert, welche Aktionen durchgeführt werden, wann diese durchgeführt werden und wo die Dateien liegen, welche durch den Build erstellt werden.
Da ein generischer Docker-Container erstellt wurde, müssen die benötigten Abhängigkeiten im ersten Schritt zuerst noch installiert werden.
Anschliessend wird der Bauvorgang mit autoreconf
vorbereitet. Dadurch wird unter anderem das Makefile erstellt, welches für den Build unerlässlich ist. Da wir das Copyright aus dem Paket übernehmen, kopieren wir dieses nach debian/
.
Mit dem Befehl dpkg-buildpackage
wird der Bauvorgang anschliessend gestartet. Dadurch wird das Paket kompiliert und das Debian-Paket erstellt. Diese Pakete werden danach in den erstellten Ordner build
verschoben und auf GitLab hochgeladen.
Workflow
Sobald wir einen neuen Release haben, wird ein Git-Tag erstellt. Durch den erstellten Git-Tag wird in GitLab ein neuer Build gestartet, welcher das Paket in der neuen Version baut.
Das erstellte Paket steht dann im Webinterface von GitLab zur Verfügung, wo es heruntergeladen werden kann.
Ausblick
Die gebauten Pakete bzw. Artifakte sollten idealerweise entsprechend automatisiert weiterverarbeitet werden, indem man diese beispielsweise auf einen Mirror hochlädt. In unserem Fall verwenden wir einen Bot, der auf Anweisung eines GitLab-Webhooks die Artifakte auf den Zielserver herunterlädt, dort einem Aptly Repository hinzufügt und das Repository entsprechend publiziert, so dass der Prozess vom Bau des Pakets bis zur Publizierung vollständig automatisiert werden kann. Das Resultat kann schlussendlich auf unserem öffentlichen Aptly Mirror eingesehen werden.
Ich hatte neulich einen Kunden am Telefon, der sich über ein langsam ablaufendes Script beschwerte. Es handelte sich dabei um einen Import, welcher in der Regel mehrere Stunden läuft.
Der Quellcode stammt nicht von mir und das Script greift auf mehrere Datenbanken auf verschiedenen Servern zu.
Die üblichen Tools für eine Performance-Analyse konnten hier nicht zum Einsatz kommen: Für mindestens eine Datenbank haben wir nicht genügend Rechte, um z.B. das MySQL Slow Query Log einzuschalten und zu überwachen.
Nach einem ersten Einblick in den Quellcode sehen wir, dass das Script sehr viele Queries verschickt sowie auch viele Dateien kopiert und verarbeitet. Wir müssen also zuerst herausfinden, was wirklich langsam ist. Ist es wirklich die Datenbank? Oder am Ende der Datentransfer?
Irrwege
Der erste Instinkt ist in solchen Fällen häufig, die SQL-Queries auf ineffiziente Patterns zu checken und die Datenbanken auf fehlende Indizes zu prüfen.
Einige der Queries hatten in der Tat solche Patterns – zum Beispiel fällt die folgende Bedingung schon früh als verdächtig auf:
WHERE `p`.`product_ean` LIKE '%123456789'
Wie man relativ früh in der Schule lernt, können solche Abfragen nicht mittels Index abgedeckt werden und sind daher immer mit einem Table Scan verbunden – was bei wenigen Daten vielleicht noch geht, aber mit zunehmender Datenmenge garantiert zu Problemen führt.
Aber: Welches dieser Queries im Code ist wirklich problematisch? Und: Wie dürfte man die Queries korrigieren? Der obere Fall deutet auf eine EAN hin und wir können die Vermutung anstellen, dass hier wohl führende Nullen abgedeckt werden sollen. Aber Vermutungen können zu Fehleinschätzungen führen, daher fahren wir weiter mit dem Fakten-Check…
Ziele setzen
Wichtig ist, dass wir das Ziel nicht aus den Augen verlieren. Welche dieser ineffizienten Queries sind wirklich problematisch? Und welche davon können wir ignorieren? Und wie häufig werden all diese Queries ausgeführt?
Ich bin seit langer Zeit Fan des Tools strace
. strace
zeichnet alle System Calls eines Programms auf. Man sieht also damit, welche Auswirkungen ein Programm wirklich auf seine Umgebung hat. Da unser Script vermutlich primär aufgrund der Interaktionen mit seinen Umsystemen langsam wird, denke ich, dass wir hiermit das richtige Tool haben.
Debugging
Ich werfe also Strace an und hänge es mit dem Parameter -p $PID
an den bereits laufenden Prozess.
# strace -p 933950
Process 933950 attached
write(1, "Produkt wurde erfolgreich aktual"..., 133) = 133
sendto(5, "\334\4\0\0\3\n\t\t\tSELECT\n\t\t\t\tentryID AS I"..., 1248, MSG_DONTWAIT, NULL, 0) = 1248
poll([{fd=5, events=POLLIN|POLLERR|POLLHUP}], 1, 1471228928) = 1 ([{fd=5, revents=POLLIN}])
recvfrom(5, "\1\0\0\1\30+\0\0\2\3def\6mmdb15\1t\5Entry\2ID\7"..., 1139, MSG_DONTWAIT, NULL, NULL) = 1139
poll([{fd=5, events=POLLIN|POLLERR|POLLHUP}], 1, 1471228928) = 1 ([{fd=5, revents=POLLIN}])
recvfrom(5, "\31\3def\6mmdb15\1t\5Entry\10statusID\10st"..., 1519, MSG_DONTWAIT, NULL, NULL) = 69
sendto(5, "\304\0\0\0\3\n\t\t\t\tSELECT\n\t\t\t\t\tEAN,\n\t\t\t\t\t"..., 200, MSG_DONTWAIT, NULL, 0) = 200
poll([{fd=5, events=POLLIN|POLLERR|POLLHUP}], 1, 1471228928) = 1 ([{fd=5, revents=POLLIN}])
recvfrom(5, "\1\0\0\1\10,\0\0\2\3def\6mmdb15\5Track\5Track"..., 1450, MSG_DONTWAIT, NULL, NULL) = 443
Okay, ich sehe hier viele Interaktionen, aber gefühlt bleibt der Output immer mal wieder für den Bruchteil einer Sekunde stehen. Was genau passiert da? Ich vermute ja schon, dass das Script auf die Antwort einer Datenbank wartet. Können wir prüfen, wo das Script genau hängt?
Stellt sich heraus – ja, man kann: strace
kennt eine Option -c
, welche nicht “live” einen Output generiert, sondern am Ende (nachdem man strace abbricht, oder der Zielprozess fertig ist) eine Statistik ausgibt. Klingt gut, testen wir das mal:
# strace -p 933950 -c
Process 933950 attached
^CProcess 933950 detached
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
46.43 0.000970 10 99 poll
45.14 0.000943 236 4 stat
3.49 0.000073 5 16 lstat
3.45 0.000072 1 71 sendto
0.91 0.000019 0 100 recvfrom
0.19 0.000004 0 48 write
0.19 0.000004 0 16 open
0.05 0.000001 0 22 lseek
0.05 0.000001 0 6 mmap
Und hier sehen wir auch schon den ersten Verdächtigen: poll
wird sehr oft aufgerufen und verbraucht damit den Grossteil der Zeit! stat hingegen können wir wohl vorerst vernachlässigen, da er nur 4 mal aufgerufen wird. poll
wird verwendet um auf Antworten vom Netzwerk zu warten. Ein Programm braucht diesen System Call, um zu schlafen, bis von einer Gegenstelle wieder Daten geliefert werden. Aber welche Daten? Und wie kommen wir anhand dieser Informationen wieder zu möglichen Lösungen für unser eigentliches Problem?
Wenn wir den Output von oben nochmals anschauen, sehen wir ein Pattern von System Calls: Es wird immer ein sendto
gemacht, gefolgt von poll
und schlussendlich recvfrom
. Wie die Namen andeuten, sind diese System Calls für das Senden und Empfangen von Daten via Netzwerk zuständig.
Wir könnten jetzt also schauen, wie lange es zwischen einem sendto
– und recvfrom
-Paar genau dauert. Vielleicht gibt uns das ja Aufschluss! Aber wie geht das am einfachsten?
Manpage to the rescue!
Nach etwas lesen in der Manpage von strace
finde ich eine Option, die mir sehr nützlich erscheint:
-r Print a relative timestamp upon entry to each system call.
This records the time difference between the beginning of
successive system calls.
Mit dieser Option sehe ich also, wieviel Zeit seit dem letzten System Call verging. Etwas Nachdenken, dann der Geistesblitz: Was passiert wohl, wenn ich mit strace
nur die beiden System Calls sendto
und recvfrom
anzeige und den Rest ausfiltere? Mal schauen:
strace -p 933950 -r -e recvfrom,sendto
Process 933950 attached
0.000169 sendto(4, "\v\1\0\0\3\n\t\t\tSELECT\n\t\t\t\t`image_name`"..., 271, MSG_DONTWAIT, NULL, 0) = 271
1.036864 recvfrom(4, "\1\0\0\1\1^\0\0\2\3def\21sometable\1"..., 131, MSG_DONTWAIT, NULL, NULL) = 131
0.000088 recvfrom(4, "0.jpg\5\0\0\5\376\0\0\"\0", 173, MSG_DONTWAIT, NULL, NULL) = 14
Perfekt! Und bereits einen Verdächtigen erwischt! Da haben wir ein Query (man sieht ein SELECT
) und irgendwelche Tabellennamen im Output (recvfrom
-Zeile). Interessant. Aber vielleicht war es ja nur ein statistischer Ausreisser. Denn die anderen Calls brauchen viel weniger und ich sehe auch nicht das ganze Query.
Du bist umzingelt!
Wir wissen also wonach wir suchen müssen. Jetzt muss die Falle nur noch richtig gestellt werden und dann schauen was passiert. Mein Plan ist folgender:
Wir lassen strace
in eine Datei schreiben (Option -o
), sortieren dann die Aufrufe nach Zeit und sollten so, statistisch gesehen, die langsamsten Queries finden.
Und wenn wir schon dabei sind, möchten wir dann auch sehen, welche Queries daran schuld sind. Der obige Output zeigt uns, dass strace
lange String-Argumente stark zusammenkürzt. In der Manpage finden wir den Parameter -s
, mit welchem man angeben kann, wie stark die Kürzung ist. Wir wollen möglicht die ganzen Daten; ich gebe also einen extrem langen Parameter von 10000 Zeichen an. Also:
# strace -p 933950 -r -e sendto,recvfrom -s 10000 -o import_db_stats.txt
Process 933950 attached
Wir lassen den Trace etwas laufen und brechen dann mit Ctrl-C
ab. Anschliessend sortieren wir die Aufzeichnung numerisch und schauen uns die letzten paar Zeilen an:
# sort -n import_db_stats.txt | tail -n5
0.749176 recvfrom(4, "\1\0\0\1\1^\0\0\2\3def\21sometable\1i\"product_images\nimage_name\nimage_name\f!\0\375\2\0\0\375\1P\0\0\0\5\0\0\3\376\0\0\"\0\24\0\0\4\0231234567890_1.jpg\5\0\0\5\376\0\0\"\0", 159, MSG_DONTWAIT, NULL, NULL) = 145
0.769669 recvfrom(4, "\1\0\0\1\1^\0\0\2\3def\21sometable\1i\"product_images\nimage_name\nimage_name\f!\0\375\2\0\0\375\1P\0\0\0\5\0\0\3\376\0\0\"\0\24\0\0\4\0231234567890_0.jpg\5\0\0\5\376\0\0\"\0", 172, MSG_DONTWAIT, NULL, NULL) = 145
0.803661 recvfrom(4, "\1\0\0\1\1^\0\0\2\3def\21sometable\1i\"product_images\n", 69, MSG_DONTWAIT, NULL, NULL) = 69
0.814297 recvfrom(4, "\1\0\0\1\1^\0\0\2\3def\21me", 16, MSG_DONTWAIT, NULL, NULL) = 16
0.947148 recvfrom(4, "\1\0\0\1\1^\0\0\2\3def\21sometable\1i\"product_images\nimage_name\nimage_name\f!\0\375\2\0\0\375\1P\0\0", 102, MSG_DONTWAIT, NULL, NULL) = 102
Oh, wunderbar! Wir haben also die fünf langsamsten Calls (respektive diejenigen mit der längsten Wartezeit vorher), alle sind recvfrom
und zudem von der gleichen Datenbank (der erste Parameter entspricht dem File Handle respektive dem Socket). Bonus: Vier dieser Calls enthalten Daten der gleichen Tabelle! Jackpot!
Moment – wie sehe ich jetzt aber die Queries, welche langsam sind? Die recvfrom
betreffen ja nur das Auslesen der Resultate. Wir erinnern uns – das Query wird in der Regel direkt vorher mittels sendto
abgeschickt. Wir müssen also nur im (unsortierten!) Output nach den entsprechenden Zeilen suchen und den Vorgänger finden.
Ich denke, dass die relativen Timestamps über die kurze Zeit wohl eindeutig genug sind um als Suchparameter zu dienen. Also holen wir von den obigen Resultaten mal nur diese raus:
# sort -n import_db_stats.txt | tail -n5 | awk '{print $1}'
0.749176
0.769669
0.803661
0.814297
0.947148
Perfekt! Und mit grep
können wir jetzt die Vorgänger-Zeilen dazu finden. Dabei hilft uns der Parameter -B
. Weil wir die Resultate nicht wollen, filtern wir diese gleich wieder raus:
# sort -n import_db_stats.txt | tail -n5 | awk '{print $1}' > slowest_times.txt
> for time in $(cat slowest_times.txt); do
> grep -B1 $time import_db_stats.txt | grep -v $time
> done
0.000091 sendto(4, "\v\1\0\0\3\n\t\t\tSELECT\n\t\t\t\t`image_name`\n\t\t\tFROM\n\t\t\t\t`product_images` AS `i`\n\t\t\tJOIN\n\t\t\t\t`products` AS `p`\n\t\t\t\t\tUSING (`product_id`)\n\t\t\tWHERE\n\t\t\t\t`image_name` LIKE '12345678896_1.jpg'\n\t\t\t\tAND\n\t\t\t\t`p`.`product_ean` LIKE '%12345678896'\n\t\t", 271, MSG_DONTWAIT, NULL, 0) = 271
0.000222 sendto(4, "\v\1\0\0\3\n\t\t\tSELECT\n\t\t\t\t`image_name`\n\t\t\tFROM\n\t\t\t\t`product_images` AS `i`\n\t\t\tJOIN\n\t\t\t\t`products` AS `p`\n\t\t\t\t\tUSING (`product_id`)\n\t\t\tWHERE\n\t\t\t\t`image_name` LIKE '1234567890134_0.jpg'\n\t\t\t\tAND\n\t\t\t\t`p`.`product_ean` LIKE '%1234567890134'\n\t\t", 271, MSG_DONTWAIT, NULL, 0) = 271
0.000196 sendto(4, "\v\1\0\0\3\n\t\t\tSELECT\n\t\t\t\t`image_name`\n\t\t\tFROM\n\t\t\t\t`product_images` AS `i`\n\t\t\tJOIN\n\t\t\t\t`products` AS `p`\n\t\t\t\t\tUSING (`product_id`)\n\t\t\tWHERE\n\t\t\t\t`image_name` LIKE '1234567890189_0.jpg'\n\t\t\t\tAND\n\t\t\t\t`p`.`product_ean` LIKE '%1234567890189'\n\t\t", 271, MSG_DONTWAIT, NULL, 0) = 271
0.000089 sendto(4, "\v\1\0\0\3\n\t\t\tSELECT\n\t\t\t\t`image_name`\n\t\t\tFROM\n\t\t\t\t`product_images` AS `i`\n\t\t\tJOIN\n\t\t\t\t`products` AS `p`\n\t\t\t\t\tUSING (`product_id`)\n\t\t\tWHERE\n\t\t\t\t`image_name` LIKE '1234567890134_1.jpg'\n\t\t\t\tAND\n\t\t\t\t`p`.`product_ean` LIKE '%1234567890134'\n\t\t", 271, MSG_DONTWAIT, NULL, 0) = 271
0.000167 sendto(4, "\v\1\0\0\3\n\t\t\tSELECT\n\t\t\t\t`image_name`\n\t\t\tFROM\n\t\t\t\t`product_images` AS `i`\n\t\t\tJOIN\n\t\t\t\t`products` AS `p`\n\t\t\t\t\tUSING (`product_id`)\n\t\t\tWHERE\n\t\t\t\t`image_name` LIKE '1234567890103_0.jpg'\n\t\t\t\tAND\n\t\t\t\t`p`.`product_ean` LIKE '%1234567890103'\n\t\t", 271, MSG_DONTWAIT, NULL, 0) = 271
Wunderbar. Wie bereits ganz am Anfang vermutet, ist es eines der Queries, welches eine LIKE
-Suche mit einem Wildcard-Anfang macht.
Jetzt müssen wir also nur noch nach dem entsprechenden Query im Code suchen und uns überlegen, wie wir dies optimieren können. Aber das muss ich jetzt nicht mehr erklären; heute ging es darum, ein Problem einmal auf eine andere Art zu suchen.
Fazit
Wir lernen also, dass Linux mit strace
ein wunderbares Werkzeug zur Verfügung stellt, um nicht nur allgemeine Probleme zu finden, sondern auch für Performance-Analysen. Das Schöne daran ist, dass man hier bereits viel über ein Programm erfahren kann, insbesondere auch wenn man auf den Quellcode keinen Zugriff hat.
Ich hatte strace
bisher primär für die Fehlersuche bei hängenden Programmen verwendet oder zur Analyse was ein Programm genau tut.
Die wichtigsten Parameter von strace
nochmals zusammengefasst:
-o
schreibt den Output in eine Datei-r
zeigt die relative Zeit seit dem letzten aufgezeichneten (!) System
Call-s
kann verwendet werden, um die String-Parameter-Kürzung einzustellen-c
erstellt ein Call Profiling für die aufgerufenen Calls
strace
kann noch viel mehr, es lohnt sich also, die Manpage einfach mal zu
überfliegen.
Mit Stolz verkünden wir, dass wir am diesjährigen SUSE D-A-CH Partner Summit in München mit dem Innovationspreis in der Hauptkategorie “Technologie” geehrt wurden. Der Preis baut auf einer langjährigen und professionellen Zusammenarbeit auf und zeichnet Adfinis SyGroup unter den SUSE Technologiepartnern aus Deutschland, Österreich und der Schweiz als Innovation Leader aus. Dies betrifft vor allem den Bereich DevOps und den Umgang mit Bleeding-Edge-Technologien wie beispielsweise Container und Platform as a Service (PaaS).
Mehr dazu in der Pressemitteilung
Security ist nach wie vor ein heisses Thema in der IT. Das zeigt auch das Aufkommen diverser Security bezogenen Tools wie z.B. Square’s Keywhiz oder HashiCorp’s Vault. Wir haben uns Vault etwas genauer angeschaut und uns überlegt wie wir Vault in der Adfinis SyGroupeinsetzen können.
Was ist Vault?
In seiner simpelsten Form ist Vault ein hierarchischer Key/Value Store zum Speichern von Secrets. Die Idee ist, dass man Vault an eine persistente Datenbank (z.B. MySQL, PostgreSQL, Consul, …) seiner Wahl anschliesst oder alternativ auf das built-in File-Backend zurückgreift. Die Interaktion mit Vault geschieht über eine HTTP REST API, zu welcher es Client Libraries in den verschiedensten Programmiersprachen gibt.
Bevor wir aber genauer darauf eingehen wie Vault funktioniert, sollte geklärt sein, welche Probleme Vault lösen zu versucht. Betrachtet man den Lifecycle eines generischen Secrets, z.B. eines Passwortes für einen Datenbank-Benutzer, sieht dieser meist wie folgt aus:
1. Der Admin legt ein Secret mit Berücksichtigung der firmenweiten Passwort-Richtlinien fest.
2. Das Passwort wird in einem Passwortmanager seiner Wahl festgehalten.
3. Beim Rückbau des Benutzers wird die Passwortliste entsprechend angepasst.
Leider geht oftmals ein wichtiger Punkt vergessen, nämlich die regelmässige Rotation der Secrets. Mit der Annahme, dass man mit genug Zeit und Ressourcen jede Verschlüsselung brechen kann, wäre es verheerend wenn ein Angreifer an eine Kopie der verschlüsselten Passwort-Datenbank gelangt.
Die Vision von HashiCorp sind sogenannte “dynamische Secrets”. Anstatt, dass der System Administrator beim Einrichten einer Webapplikation das Passwort setzt, holt sich die Webapplikation das Secret selber in regelmässigen Zeitabständen bei Vault ab. Dabei wird jedem Secret eine TTL mitgegeben, welche die Lebensdauer des Secrets auf wenige Stunden oder Tage limitiert. Sollte in einem solchen Szenario der Angreifer an die verschlüsselten Passwörter gelangen, wäre dies “harmlos” da in einem Tag bereits alle rotiert sind.
Durch die Zentralisierung entsteht ausserdem ein weiterer Nebeneffekt, denn sie ermöglicht sogenannte Break Glass Procedures. Stellt man in der eigenen Infrastruktur eine Schwachstelle fest, kann der Zugriff auf das betroffene Subset an Secrets gesperrt werden. Hat man die Schwachstelle gefunden und behoben, werden die Secrets rotiert und wieder freigegeben.
Zu diesem Zeitpunkt unterstützen die wenigsten Applikationen Vault nativ. Verwendet man allerdings das HashiCorp eigene Consul als Datenbackend, können Konfigurationsdateien durch das Tool consul-template bei Key/Value Änderung automatisch neu generiert werden.
Um Änderungen am Datenbestand protokollieren zu können, stellt Vault verschiedene Audit Backends zur Verfügung (Structured Logs und Syslog). So können Mutation auch später eingesehen werden. Der Zugriff auf Secrets lässt sich ausserdem durch ein ausgeklügeltes ACL System einschränken. ACLs lassen sich wiederum mit verschiedenen Authentication Backends verknüpfen. So kann man den Zugriff auf einen Pfad für eine bestimmte LDAP Gruppe bspw. auf “Read-Only” einschränken.
Die Konfiguration von Vault geschieht wahlweise über eine HCL oder JSON Datei. In folgendem Beispeil sieht man eine Konfiguration mit einem Consul Backend:
backend "consul" {
address = "127.0.0.1:8500"
path = "vault"
}
listener "tcp" {
address = "0.0.0.0:8200"
tls_disable = 0
tls_cert_file = "/etc/ssl/host.crt"
tls_key_file = "/etc/ssl/host.key"
tls_min_version = "tls12"
}
> Was die Erstellung des TLS Zertikates betrifft, verweise ich an dieser Stelle gerne auf unseren Post OpenSSL x509 Zertifikate erstellen.
Demo
Um ein Gefühl dafür zu bekommen wie sich Vault bedienen lässt, folgt nun eine kleine Demo.
Sobald der Vault Server läuft, muss er initialisiert werden.
$ vault init
Unseal Key 1: aRqypTLVENeZ9Tt1Kw2sy7XlnqHLT8XOUn3yqGTFMvsB
Unseal Key 2: o4Rd/1ZyzBwZaVEksIHsMpCJimv/pOt1+pSfQFiOEUkC
Unseal Key 3: S1x0hTZ5SrqiQlH1Boh8c/teKtfypu/PS6c5aHGj31cD
Unseal Key 4: TNdq4mdDDUMXAlxbtH8GCRRn3KU4HR3VSiefbWDUjtAE
Unseal Key 5: pA9DmAdIi+WsKVyKAnaWSH+wfBk1Hxlv+xQ5RUn5QM4F
Initial Root Token: 53368d66-8d50-ba5a-f953-7ef6b2dc03de
...
$ for KEY in ${UNSEAL_KEYS}; do vault unseal ${KEY}; done
...
Key (will be hidden):
Sealed: false
Key Shares: 5
Key Threshold: 3
Unseal Progress: 0
...
$ vault auth $ROOT_TOKEN
Successfully authenticated! You are now logged in.
token: 1bb7c7c6-6d4e-be92-cb5c-2838ccde3b5c
token_duration: 0
token_policies: [root]
...
Dabei wird ein Master Key generiert und in mehrere Subkeys aufgeteilt (siehe Shamir’s Secret Sharing). Von den generierten Subkeys werden in der Standardkonfiguration ein Quorum von 3 Keys benötigt um die Vault Instanz zu “entsiegeln”, ein Prozedere das bei jedem Start der Vault Instanz vorgenommen werden muss. Das initiale Root Token wird anschliessend benötigt um sich gegenüber Vault zu authentisieren.
Ab jetzt können Secrets in das standardmässig vorhandene secret/
Backend geschrieben werden.
$ vault mounts
Path Type Default TTL Max TTL Description
cubbyhole/ cubbyhole n/a n/a per-token private secret storage
secret/ generic system system generic secret storage
sys/ system n/a n/a system endpoints used for control, policy and debugging
$ vault write secret/test secret_key=secret_value
Success! Data written to: secret/test
$ vault list secret/
Keys
----
test
$ vault read secret/test
Key Value
--- -----
refresh_interval 720h0m0s
secret_key secret_value
Das war nur ein kleiner Auszug aus den verfügbaren Features welche Vault anbietet. Hier ein paar weiterführende Use Cases welche aber den Rahmen dieses Blogpostes sprengen würden:
- Generieren von Amazon Web Services IAM Policies
- Verwaltung von dynamischen Datenbank Benutzer/Rollen für MySQL, PostgreSQL, MongoDB…
- Vollständige PKI zum Erstellen und Signieren von TLS Zertifikaten
- SSH Zugriff auf Systeme durch OTP absichern
Ich kann jedem empfehlen einen Blick in die hervorragende Dokumentation von Vault zu werfen. In kürze folgt ausserdem ein zweiter Teil der aufzeigt, wie wir Vault adaptiert haben.