Store passwords like a boss #2

Als Programmierer von Webanwendungen, die Ihre Benutzer selbst verwalten, kommt man um das Thema Sicherheit nicht herum – so auch ich als Mitentwickler von Icinga Web 2.

Aus diesem Anlass habe ich recherchiert, wie Passwörter von Benutzern sinnvoll gespeichert werden können und mein neu gewonnenes Wissen geteilt.

Darüber hinaus habe ich vorgeschlagen, Icinga Web 2 dementsprechend zu erweitern. Eric hat diesen Vorschlag begutachtet und mich auf eine Alternative aufmerksam gemacht, nämlich…

password_hash()

PHP bietet seit Version 5.5 u. a. diese Funktion, um Passwörter zu verschlüsseln. Diese ersetzt meine selbst geschriebene Funktion aus dem Vorgänger-Blogpost vollständig:

$encryptedNewPassword = password_hash($newPassword, PASSWORD_DEFAULT);

Der Rückgabe-Wert kann direkt in einer Datenbank gespeichert werden – so einfach ist das.

Der zweite Parameter der Funktion bestimmt den Verschlüsselungs-Algorithmus. Damit kann man diesen zwar frei wählen, aber ich empfehle, den Standard vorzuziehen, denn dieser ändert sich (laut Doku) mit neueren PHP-Versionen zu Gunsten der Sicherheit. Damit bleiben zumindest die neuen/geänderten Passwörter aktuell was die Sicherheit angeht.

password_verify()

Wie der Name schon andeutet, überprüft diese Funktion ein vom Benutzer eingegebenes Passwort auf Übereinstimmung mit einem verschlüsselten Passwort:

$ok = password_verify($enteredPassword, $encryptedPassword);
if ($ok) {
    ...

password_needs_rehash()

Wie bereits erläutert: mit einem neuen Standard-Verschlüsselungs-Algorithmus werden automatisch alle neu verschlüsselten Passwörter sicherer. Aber was ist mit den bereits bestehenden?

Da bestehende Passwörter (aus gutem Grund!) nur verschlüsselt vorliegen, können sie nicht alle “in einem Abwasch” migriert werden. Die einzige Gelegenheit ist die Anmeldung eines Benutzers, die das Klartext-Passwort erfordert. Und als wäre die Funktion für diese Gelegenheit geschrieben…

if ($ok) {
    $outdated = password_needs_rehash($encryptedPassword, PASSWORD_DEFAULT);
    if ($outdated) {
        $encryptedPassword = password_hash($enteredPassword, PASSWORD_DEFAULT);
    }
    ...

Fazit

Der IT-Branche wird nicht umsonst nachgesagt, dass sie eine “permanente Innovation” vollbringe. Umso wichtiger ist es, aus Gründen der Datensicherheit, mit der dunklen Seite der Macht mitzuhalten.

Denn die sind böse, sehr böse…

Alexander Klimov

Autor: Alexander Klimov

Alexander hat 2017 seine Ausbildung zum Developer bei NETWAYS erfolgreich abgeschlossen. Als leidenschaftlicher Programmierer und begeisterter Anhänger der Idee freier Software, hat er sich dabei innerhalb kürzester Zeit in die Herzen seiner Kollegen im Development geschlichen. Wäre nicht ausgerechnet Gandhi sein Vorbild, würde er von dort aus daran arbeiten, seinen geheimen Plan, erst die Abteilung und dann die Weltherrschaft an sich zu reißen, zu realisieren - tut er aber nicht. Stattdessen beschreitet er mit der Arbeit an Icinga Web 2 bei uns friedliche Wege.

PHP Error – php_network_getaddresses

Ein Problem zu dem es viele Lösungsansätze gibt, ist folgender PHP Fehler. Er tritt beispielsweise auf, beim Verbinden auf externe Anwendungen oder APIs:
php_network_getaddresses: getaddrinfo failed: No address associated with hostname

Im Netz kursieren Teilweise die wildesten Lösungsansätze. Da ich vor kurzem mit eben diesem Problem konfrontiert wurde, möchte ich meine Erfahrung mit euch teilen. In der “php.ini” gibt es einen Parameter, der diesen Fehler verursachen kann.

Dieser kann unterschiedlich gefüllt sein und ist per default leer.

disable_functions = pcntl_alarm,pcntl_fork,,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wexitstatus,pcntl_wtermsig,,pcntl_signal,,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,,pcntl_exec,pcntl_getpriority,pcntl_setpriority

Um obigen Fehler zu beheben, kann es also reichen, sich diesen Parameter anzusehen und notfalls sogar komplett zu leeren.

Marius Gebert

Autor:

Marius ist seit September 2013 bei uns beschäftigt. Er hat im Sommer 2016 seine Ausbildung zum Fachinformatiker für Systemintegration absolviert und kümmert sich nun um den Support unserer Hostingkunden. Seine besonderen Themengebiete erstrecken sich vom Elastic-Stack bis hin zu Puppet. Auch an unserem Lunchshop ist er ständig zu Gange und versorgt die ganze Firma mit kulinarischen Köstlichkeiten. Seine Freizeit verbringt Marius gern an der frischen Luft und wandert dabei durch die fränkische Schweiz

Ruby Applikationen testen mit RSpec und WebMock

Das Testen von Software begleitet mich von Projekt zu Projekt. Bei bisherigen Entwicklungen habe ich immer wieder auf RSpec zurück gegriffen, ein tool zum testen von Ruby Applikationen. Es zeichnet sich durch eine relativ einfache Handhabung aus und eine Ausdrucksweise die an die menschliche Sprache angelehnt ist. Auch wenn nicht jeder mögliche einzutretende Fall getestet werden kann, tests geben Entwicklern Sicherheit wenn Änderungen am Code vorgenommen werden.

WebMock

WebMock ist eine Library die es einem ermöglich HTTP requests auszudrücken und Erwartungen an diese zu setzen. In Verbindung mit RSpec lässt sich WebMock verwenden um eine Ruby Anwendung zu testen die HTTP Requests ausführt, beispielsweise an eine API. Oft sind diese Requests nicht starr sondern werden dynamisch anhand von Parametern zusammen gesetzt. WebMock/RSpec hilft dabei unterschiedliche Fälle zu simulieren, Erwartungen zu definieren und diese mit zu prüfen.

WebMock ist als Gem verfügbar, die Installation ist daher relativ einfach:

user@localhost ~ $ gem install webmock

Als Beispiel verwende ich eine sehr simple Ruby Klasse. Deren einzige Methode ‘request’ sendet einen GET request an die URL ‘https://wtfismyip.com’. Abhängig vom Parameter ‘option’ wird die IP entweder in Textform oder als JSON abgefragt:

require 'net/http'
require 'uri'

class ApiCaller
  def self.request(option)
    uri = URI.parse("https://wtfismyip.com/#{option}")
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true
    request = Net::HTTP::Get.new(uri.request_uri)
    http.request(request)
  end
end

Mein Test beinhaltet den Aufruf der ‘request’ Methode. Der HTTP request wird simuliert und die Erwartung das ein Request an eine bestimmte URL stattfinden soll wird auch definiert.

require 'api_caller'
require 'webmock/rspec'

describe ApiCaller do
  describe '.request' do
    context 'given json parameter' do
      it 'requests json url' do
        stub_request(:get, 'https://wtfismyip.com/json')
        expect(ApiCaller.request('json')).
          to have_requested(:get, 'https://wtfismyip.com/json').once
      end
    end

    context 'given text parameter' do
      it 'requests text url' do
        stub_request(:get, 'https://wtfismyip.com/text')
        expect(ApiCaller.request('text')).
          to have_requested(:get, 'https://wtfismyip.com/text').once
      end
    end
  end
end

Beim testen sieht das nun folgendermaßen aus:

user@localhost ~ $ rspec --format documentation

ApiCaller
  .request
    given json parameter
      requests json url
    given text parameter
      requests text url

Finished in 0.00557 seconds (files took 0.37426 seconds to load)
2 examples, 0 failures
Blerim Sheqa

Autor: Blerim Sheqa

Blerim ist seit 2013 bei NETWAYS und seitdem schon viel in der Firma rum gekommen. Neben dem Support und diversen internen Projekten hat er auch im Team Infrastruktur tatkräftig mitgewirkt. Hin und wieder lässt er sich auch den ein oder anderen Consulting Termin nicht entgehen. Mittlerweile kümmert sich Blerim hauptsächlich im Icinga Umfeld um die technischen Partner und deren Integrationen in Verbindung mit Icinga 2.

Alt, aber bewährt

Immer wieder geht es in der IT um Vergleiche,  z.B. bei der Web-Entwicklung (NodeJS vs. PHP), der Datenhaltung (MongoDB vs. SQL-Datenbanken) oder auch scheinbaren Kleinigkeiten wie das Einlesen einer Datei in den Arbeitsspeicher.

Letztgenannter Vorgang würde bspw. in der Programmiersprache C unter Linux und anderen POSIX-Systemen wie folgt aussehen (Prüfungen auf Fehler habe ich der Übersichtlichkeit halber weggelassen):

void* read_file(char const *path, size_t *length) {
    int fd;
    struct stat stats;
    void *buf, *pos;
    ssize_t current_read;
    size_t remain;
    
    fd = open(path, O_RDONLY);
    fstat(fd, &stats);
    buf = malloc(stats.st_size);
 
    pos = buf;
    remain = stats.st_size;
    for (;;) {
        current_read = read(fd, pos, remain);        
        if (current_read == 0) {
            break;
        }
 
        *(char**)&pos += current_read;
        remain -= (size_t)current_read;
    }
    
    close(fd);
    *length = stats.st_size;
    return buf;
}

Die Alternative dazu wäre die Verwendung von mmap():

void* read_file_via_mmap(char const *path, size_t *length) {
    int fd;
    struct stat stats;
    void *addr, *buf;
    
    fd = open(path, O_RDONLY);
    fstat(fd, &stats);
    addr = mmap(NULL, stats.st_size, PROT_READ, MAP_SHARED, fd, 0);
    buf = malloc(stats.st_size);
 
    memcpy(buf, addr, stats.st_size);
 
    munmap(addr, stats.st_size);
    close(fd);
    *length = stats.st_size;
    return buf;
}

Deren Verfechter werben damit, dass die Anzahl der Systemaufrufe pro Datei konstant ist. Dem kann ich nicht widersprechen – die Datei wird mit open() geöffnet und mit mmap() in den Adressbereich integriert. Damit kann auf den Inhalt zugegriffen werden als befände er sich im Arbeitsspeicher. Das ermöglicht das anschließende Kopieren mit Hilfe von memcpy().

Aber das hilft einem nur weiter, wenn das auch tatsächlich messbar Zeit spart – dies ist definitiv nicht der Fall:

aklimov@ws-aklimov:~$ time ./read garbage

real    0m8.108s
user    0m0.000s
sys     0m3.664s
aklimov@ws-aklimov:~$ time ./mmap garbage

real    0m8.099s
user    0m0.652s
sys     0m3.000s
aklimov@ws-aklimov:~$

Das Lesen einer Datei (3 GB) hat im konkreten Fall nicht viel weniger Zeit in Anspruch genommen. Die Verwendung von mmap() hat lediglich ca. 660 Millisekunden von der linken in die rechte Tasche transferiert. Um genau zu sein – die Systemaufrufe der konventionellen Methode (strace):

open("garbage", O_RDONLY)               = 3                                                                                                                                                  
read(3, "\377\0\377\0\377\0\377\0\377\0\377\0\377\0\377\0\377\0\377\0\377\0\377\0\377\0\377\0\377\0\377\0"..., 3221225472) = 2147479552                                                      
read(3, "\377\0\377\0\377\0\377\0\377\0\377\0\377\0\377\0\377\0\377\0\377\0\377\0\377\0\377\0\377\0\377\0"..., 1073745920) = 1073745920                                                      
read(3, "", 0)                          = 0
close(3)                                = 0

… vs. mmap():

open("garbage", O_RDONLY)               = 3
mmap(NULL, 3221225472, PROT_READ, MAP_SHARED, 3, 0) = 0x7ff5423c6000
munmap(0x7ff5423c6000, 3221225472)      = 0
close(3)                                = 0

Zwar benötigt die konventionelle Methode im konkreten Fall einen Systemaufruf mehr, aber angesichts der Dateigröße ist das meiner Meinung nach trotzdem vergleichsweise effizient.

Fazit

Nicht alles was glänzt ist auch Gold. Und mit Kaffeesatzlesen ist niemandem geholfen.

Alexander Klimov

Autor: Alexander Klimov

Alexander hat 2017 seine Ausbildung zum Developer bei NETWAYS erfolgreich abgeschlossen. Als leidenschaftlicher Programmierer und begeisterter Anhänger der Idee freier Software, hat er sich dabei innerhalb kürzester Zeit in die Herzen seiner Kollegen im Development geschlichen. Wäre nicht ausgerechnet Gandhi sein Vorbild, würde er von dort aus daran arbeiten, seinen geheimen Plan, erst die Abteilung und dann die Weltherrschaft an sich zu reißen, zu realisieren - tut er aber nicht. Stattdessen beschreitet er mit der Arbeit an Icinga Web 2 bei uns friedliche Wege.

Monitoring Plugins in Go

Auf Twitter hat Jan Schaumann vor Kurzem begonnen eine Liste aufzustellen mit Dingen, die jeder Sysadmin in seinem Leben schon mindestens ein mal getan hat. Darunter zählen Sachen wie einen Parser für den ifconfig Befehl zu schreiben oder unvollständige Regexes für IP Adressen zu basteln. Beim Durchgehen dieser Liste habe ich mich immer wieder selbst ertappt. Es ist erschreckend, wie viele von diesen Dingen auf einen selbst zutreffen. Dennoch ist es sehr amüsant zu lesen.

Bei Netways arbeiten wir sehr viel im Bereich Monitoring. So ist es kein Wunder, das ich bei den Dingen die jeder Sysadmin schon mal getan hat, sofort auch an dieses Thema denken musste. Lange muss man da auch nicht überlegen: Jeder Sysadmin hat mindestens schon ein mal in seiner Karriere ein monitoring Plugin geschrieben. Das gehört einfach dazu und selbst DevOps wird uns vermutlich nicht davor bewahren.

Das Schreiben von monitoring Plugins ist auf den ersten Blick ein ziemlich einfaches Thema. So ein Plugin muss einen Rückgabewert von 0, 1 oder 2 liefert und im besten Fall einen Text ausgeben, der den Zustand beschreibt. Damit wären schon mal alle Grundvoraussetzungen gegeben. Weil es eben so einfach ist, gibt es auch so viele von diesen Plugins. Sie werden in nahezu jeder Programmiersprache geschrieben. Manche Plugins sind umfangreich mit vielen Optionen und noch mehr Performance Daten in der Ausgabe. Andere wiederum bestehen nur aus wenigen Zeilen und erfüllen einen einzigen speziellen oder simplen Zweck.

Was mich bei monitoring Plugins schon immer gestört hat, ist das Deployment von jenen. Je nachdem in welcher Sprache das Plugin entwickelt ist, müssen mit cpan, gem, pip, npm, pear, composer oder andern Package Managern Abhängigkeiten nachinstalliert werden. So richtig lästig wird das bei ein paar Hundert Plugins für ein paar Tausend Server mit unterschiedlichen Distributionen. Sind Windows Systeme dabei, … ach davon fang ich garnicht erst an.

Welche Lösung gibt es also dafür? Noch keine fertige, zumindest meiner Meinung nach. Selbst Configuration Management löst das Problem nicht ganz. Um das vernünftig zu machen, müsste man ja seine 3-Zeiler Plugins packetieren und vernünftige Module oder Cookbooks schreiben, die das dann auf die Systeme ausrollen. Bestenfalls natürlich auf jedes System nur die Plugins die dort auch wirklich hin gehören. Seitdem ich mich bezüglich eines Projekts aber mit der Programmiersprache Go auseinander setze, beschäftigt mich ein anderer Ansatz um das Problem zu lösen: was wäre, wenn so ein Plugin gar keine Abhängigkeiten hat?

Go ist eine Programmiersprache, deren Wurzeln bei C liegen. Sie ist dementsprechend auch keine Scriptsprache wie Ruby oder Python, fühlt sich manchmal aber trotzdem so an. Das führt dazu das Leute wie ich, die aus der Scripting Welt kommen, sich sehr schnell wohl fühlen mit Go. Ob Go eine objektorientierte Sprache ist, da scheiden sich die Geister. Objekte im klassischen Sinne gibt es nicht, zumindest werden sie nicht so genannt. Betrachtet man es im Detail, kann man aber nachvollziehen warum es Stimmen gibt, die Go als objektorientiert bezeichnen.

Wie bei anderen Sprachen auch gibt es bei Go viele Libraries die verwendet werden können, sie werden aber Packages genannt. Der in Go geschriebene Code muss kompiliert werden. Ein großer Vorteil dabei ist, dass die entstandenen Binaries standardmäßig statisch gelinkt sind. Das heißt im Umkehrschluss es muss nichts nachinstalliert werden mit gem, pip, cpan usw. Alles steckt schon in dieser einen Binary. Sicherlich lässt sich dasselbe auch mit C, C++ oder anderen Sprachen erreichen. Keine ist aber so einfach und einsteigerfreundlich wie Go. Das zählt für mich als sehr großer Vorteil, schließlich werden diese Plugins oft von Sysadmins entwickelt, deren Hauptbeschäftigung nicht das Programmieren ist.

Statisch gelinkte Binaries könnten also das Problem der Abhängigkeiten lösen. Spinnt man die Idee weiter, könnte man mit einem “monitoring plugin” Package für Go ein Framework schaffen mit dem einheitliche Plugins entwickelt werden. Eine tolle Idee, wenn ihr mich fragt. Das Problem des packetierens lässt sich übrigens wunderbar mit fpm lösen, dazu aber vielleicht ein anderes Mal mehr.

Mein Aufruf an dieser Stelle also: Schreibt eure monitoring Plugins in Go, denn ich möchte mich nicht stundenlang damit Beschäftigen sie zum Laufen zu kriegen!

Blerim Sheqa

Autor: Blerim Sheqa

Blerim ist seit 2013 bei NETWAYS und seitdem schon viel in der Firma rum gekommen. Neben dem Support und diversen internen Projekten hat er auch im Team Infrastruktur tatkräftig mitgewirkt. Hin und wieder lässt er sich auch den ein oder anderen Consulting Termin nicht entgehen. Mittlerweile kümmert sich Blerim hauptsächlich im Icinga Umfeld um die technischen Partner und deren Integrationen in Verbindung mit Icinga 2.

Schwarze Magie für GNU/Linux-Nerds

“Linux ist der beste Virenschutz”, heißt es in “Fachkreisen”. “Installiere Linux und Du wirst fortan ruhiger schlafen können!” Ähm… falsch.

Ja, Linux ist ein guter Anfang was die Herstellung der Sicherheit auf dem eigenen Rechner betrifft. Aber es hilft einem nichts ohne brain.exe. Vor allem wenn man es mit einem Magier zu tun bekommt…

Mögen die Spiele beginnen!

ps -fu nagios

Wer auf das Monitoring-System Zugriff hat, hat viel Macht.

Wo Bernd recht hat, hat er recht – nicht umsonst nimmt z. B. Gunnars Jabber-Notification-Skript Nutzername und Passwort via Umgebungsvariablen entgegen. Check- und Notification-Skripte, die diesem Beispiel nicht folgen, reißen eine Sicherheitslücke auf, die jeder Systemnutzer ganz einfach ausnutzen kann:

aklimov@icinga2:~$ ps -fu nagios
UID        PID  PPID  C STIME TTY          TIME CMD
nagios    8128     1  0 11:10 ?        00:00:00 /usr/lib/x86_64-linux-gnu/icinga2/sbin/icinga2 --no-stack-rlimit daemon -e /var/log/icinga2/error.log
nagios    8148  8128  0 11:10 ?        00:00:00 /usr/lib/x86_64-linux-gnu/icinga2/sbin/icinga2 --no-stack-rlimit daemon -e /var/log/icinga2/error.log
nagios    8433  8148  0 11:17 ?        00:00:00 /usr/lib/nagios/plugins/check_ping -6 -H ::1 -c 200,15% -w 100,5%
nagios    8434  8433  0 11:17 ?        00:00:00 /bin/ping6 -n -U -w 10 -c 5 ::1
nagios    8435  8148  0 11:17 ?        00:00:00 /usr/lib/nagios/plugins/check_ping -4 -H 127.0.0.1 -c 200,15% -w 100,5%
nagios    8436  8148  0 11:17 ?        00:00:00 /usr/lib/nagios/plugins/check_ping -H 127.0.0.1 -c 5000,100% -w 3000,80%
nagios    8437  8435  0 11:17 ?        00:00:00 /bin/ping -n -U -w 10 -c 5 127.0.0.1
nagios    8438  8436  0 11:17 ?        00:00:00 /bin/ping -n -U -w 30 -c 5 127.0.0.1

Schwarze Magie ist hier noch nicht im Spiel. (Aber dieses Beispiel ist auch nur zum Aufwärmen.)

strace -p

Was tut ein Programm so alles wenn ich gerade mal nicht hinschaue? Um diese Frage zu beantworten, wird nicht zwangsläufig der Quellcode benötigt. In vielen Fällen reicht bereits strace:

aklimov@WS-AKlimov:~$ echo $$  # Prozess-ID der Shell
20901
aklimov@WS-AKlimov:~$ strace -p20901
strace: Process 20901 attached
wait4(-1,

Der Ausgabe können wir entnehmen, dass die Shell gerade darauf wartet, dass ein beliebiger Kindprozess (z. B. strace) sich beendet.

Diese Art der Hexenkunst hat sich bereits rumgesprochen und die Kernel-Entwickler haben ihre Wirkung eingedämmt. Daher konnte ich ohne weiteres nur den Elternprozess von strace als Beispiel nehmen.

gdb -p

Ich erinnere mich noch genau an diese eine AWP-Unterrichtsstunde in der Berufsschule:
Es ging darum, dass ein C/C++-Programm auf mehrere Module aufgeteilt werden kann. Mit dem static-Schlüsselwort sei es möglich, “modulglobale” Variablen zu erstellen – d. h. Variablen, die zwar global, aber nur für das eigene Modul sichtbar sind. Ein Argument des Lehrers für solche Variablen war allen Ernstes die Sicherheit. Und die Übung zu diesem Thema bestand darin, in einem Modul ein Passwort vor den anderen Modulen zu verbergen.
Wenn der wüsste…

Zunächst starte ich in einem neuen Terminal cat:
aklimov@WS-AKlimov:~$ cat

Damit haben wir auch schon unseren “Opfer”-Prozess, der im konkreten Fall auf eingehende Daten wartet.

Darauf hin öffne ich ein zweites Terminal (mit den gleichen Benutzerrechten!) und starte gdb:
aklimov@WS-AKlimov:~$ ps -ef |grep cat
aklimov  10217 10149  0 12:25 pts/11   00:00:00 cat
aklimov  11088 10298  0 12:26 pts/12   00:00:00 grep cat
aklimov@WS-AKlimov:~$ gdb -p 10217
GNU gdb (Debian 7.11.1-2+b1) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.
(...)
Attaching to process 10217
(...)
(gdb)

Die schwarze Magie daran…

… besteht darin, dass ich nun innerhalb dieses Prozesses schalten und walten kann wie ich will. (Und sämtlichen Speicher aller Module auslesen.)

Beispielsweise kann ich den Standard-Eingabe-Datenstrom (Stdin) des Prozesses on-the-fly durch /dev/null ersetzen (ohne ihn zu verlieren):

Aktion (Beschreibung) Aktion (GDB) Datei-Deskriptoren
/dev/null öffnen (gdb) p open("/dev/null", 0)
$1 = 3
0 /dev/pts/1
3 /dev/null
Stdin sichern (gdb) p dup(0)
$2 = 4
0, 4 /dev/pts/1
3 /dev/null
Datenstrom umleiten (gdb) p dup2(3, 0)
$3 = 0
4 /dev/pts/1
0, 3 /dev/null
Redundanten Deskriptor schließen (gdb) p close(3)
$4 = 0
4 /dev/pts/1
0 /dev/null
Programm-Ausführung fortsetzen (gdb) c
Continuing.
[Inferior 1 (process 10217) exited normally]
(gdb)
N/A

Fazit

Linux nimmt einem keinerlei Sicherheitsfragen vollständig ab. Es gibt einem lediglich die Möglichkeit, sich selbst um diese zu kümmern (und sie nicht der NSA zu überlassen).

Gehet hin und sichert euch!

Alexander Klimov

Autor: Alexander Klimov

Alexander hat 2017 seine Ausbildung zum Developer bei NETWAYS erfolgreich abgeschlossen. Als leidenschaftlicher Programmierer und begeisterter Anhänger der Idee freier Software, hat er sich dabei innerhalb kürzester Zeit in die Herzen seiner Kollegen im Development geschlichen. Wäre nicht ausgerechnet Gandhi sein Vorbild, würde er von dort aus daran arbeiten, seinen geheimen Plan, erst die Abteilung und dann die Weltherrschaft an sich zu reißen, zu realisieren - tut er aber nicht. Stattdessen beschreitet er mit der Arbeit an Icinga Web 2 bei uns friedliche Wege.