Jeder der schon einmal einen Server nach Security-Richtlinien gehärtet hat, weiß wie schwierig es sein kann, die richtigen Maßnahmen zu finden. Suchen Sie nach einer einfachen und schnellen Möglichkeit Server in Azure zu deployen, welche bereits nach aktuellen Sicherheitsstandards gehärtet sind? Dann sind Sie hier richtig. Im folgenden Beitrag erhalten Sie einen Überblick über den Deployment Prozess eines gehärteten Ubuntu Linux Servers in Azure mit Hilfe von Azure DevOps.

Warum Azure DevOps?

Für Infrastructure as a Service (IaaS) Deployments in Microsoft Azure eignet sich besonders Ubuntu Linux als freie Variante neben RHEL und SUSE. Es ist Bestandteil der Endorsed Linux distributions on Azure, die Images werden direkt von Canonical gepflegt und Microsoft liefert passende Agenten für Platformdienste wie Azure ARC, Azure Security Center, Azure Monitor und Azure Update Management.

Für die Härtung halten wir uns als aConTech grundsätzlich an den CIS Benchmark für Ubuntu Linux als Vorlage. Das „Center for Internet Security“ stellt diese als PDF zur Verfügung und das über 500-seitige Dokument erfüllt auch ambitionierte Anforderungen für Systemsicherheit. Diese Anforderungen jedoch manuell umzusetzen ist sehr zeitaufwendig.

Im folgenden Anwendungsfall stellen wir Ihnen deshalb die Variante mit Azure DevOps vor, um Ubuntu Linux zu deployen. Dabei wird ein „Infrastructure as Code“ (IaC) Ansatz verwendet. Hier wird die Verwaltung und Bereitstellung von Infrastruktur durch maschinenlesbare Definitionsdateien vorgenommen. Darüber hinaus wird ein ARM Template für die Definition der Azure Ressourcen und Shell-Scripte für die Konfiguration von Ubuntu Linux verwendet. In Bezug auf DevOps werden dafür nicht alle Features benötigt, sondern lediglich ein Azure Repository zur Codepflege und Dokumentation, und eine Azure Pipeline zum automatisierten Deployment des in dem Repository gespeicherten Codes für einen gehärteten Ubuntu Linux Server.

Was sind die Voraussetzungen?

Die Voraussetzungen für den Deployment sind folgende:

Der Microsoft Account benötigt Zugriff auf den Microsoft Tenant und die Azure Subscription in dem die Ressourcen, wie z.B. das KeyVault oder die Server liegen. Das KeyVault enthält dann die Logindaten, die man über eine gültige Ubuntu Advantage Subscription erhält. Dadurch müssen keine wichtigen Daten, wie das Passwort für den Launchpad Zugang, im Klartext in das Script einbaut werden.

1.Schritt: Vorbereitung

Das Ziel ist eine Pipeline, die eine VM in ein bestehendes VNET deployed und diese VM mit Hilfe von wenigen Scripten konfiguriert. Zunächst wird somit ein Projekt über die Azure DevOps Homepage erstellt:

Azure DevOps - Neues Projekt erstellen
Azure DevOps – Neues Projekt erstellen

Um DevOps den Zugriff auf unsere Azure Subscription zu ermöglichen, muss nun einen Service Principal mit entsprechenden Berechtigungen erstellt werden. Einen generischen SPN mit Contributor Rechten kann über „Project Settings -> Service Connections -> Create Service Connection“ erzeugt und verknüpft werden. Dabei wurde in diesem Fall „Azure Ressource Manager“ und „Service principal (automatic)“ gewählt. Außerdem kann der Scope angegeben werden. In diesem Beispiel wurde der Einfachheit halber „Subscription” gewählt.

Azure DevOps - Service Connection erstellen
Azure DevOps – Service Connection erstellen

Anschließend kümmern wir uns um die notwendigen Dateien, die wir in unserer Bereitstellung verwenden werden.

2. Schritt: Azure Repository befüllen

ARM Template mit Custom Script Extension

Das ist zum einen das ARM Template zum Erstellen eines Linux Servers. Eine gute Basis dafür finden man bei den Quickstart Templates: Create an Ubuntu Linux virtual machine using an ARM template. Dazu wird im Projekt-Repository eine neue Datei erstellt und „azuredeploy.json“ genannt. Anschließend wird der JSON-Code eingefügt:

Azure Repository - Neue Datei erstellen
Azure Repository – Neue Datei erstellen

Nun wird das ARM Template um die Custom Script Extension (CSE) erweitert. Dafür wird innerhalb der Definition der VM am Ende nach dem „dependsOn“ das leicht modifizierte Code-Snippet der CSE als „nested resource“ eingefügt:

        "dependsOn": [
          "[resourceId('Microsoft.Network/networkInterfaces', variables('networkInterfaceName'))]"
        ],
        "resources": [
          {
            "name": "config-app",
            "type": "extensions",
            "location": "[resourceGroup().location]",
            "apiVersion": "2019-03-01",
            "dependsOn": [
              "[resourceId('Microsoft.Compute/virtualMachines/', parameters('vmName'))]"
            ],
            "tags": {
              "displayName": "config-app"
            },
            "properties": {
              "publisher": "Microsoft.Azure.Extensions",
              "type": "CustomScript",
              "typeHandlerVersion": "2.1",
              "autoUpgradeMinorVersion": true,
              "settings": {
                "skipDos2Unix":false
              },
              "protectedSettings": {
                "commandToExecute": "[concat('sh ', 'install.sh ', parameters('cseUbuntuLaunchpadId'), ' ', parameters('cseUbuntuPpaPassword'))]",
                 "fileUris": [
                    "[uri(parameters('_artifactsLocation'), concat('CustomScripts/install.sh', parameters('_artifactsLocationSasToken')))]",
                    "[uri(parameters('_artifactsLocation'), concat('CustomScripts/custom-ruleset-params.txt', parameters('_artifactsLocationSasToken')))]"
                  ]
              }
            }
          }
        ]

Es wurde hier das „dependsOn“ im oberen Teil angepasst, um die VM korrekt zu referenzieren. Im unteren Teil wurden die Einstellungen angepasst. Dort sind Pfade und Scripte hardcoded und zusätzliche Parameter enthalten. Damit alles funktioniert definieren wir im oberen Teil des ARM Templates noch die fehlenden Parameter und ersetzen die Ubuntu Version mit 20.04 LTS:

      "ubuntuOSVersion": {
        "type": "string",
        "defaultValue": "20_04-lts-gen2",
        "allowedValues": [
          "20_04-lts-gen2"
        ],
        "metadata": {
          "description": "The Ubuntu version for the VM. This will pick a fully patched image of this given Ubuntu version."
        }
      },
      "cseUbuntuLaunchpadId": {
        "type": "string"
      },
      "cseUbuntuPpaPassword": {
        "type": "securestring"
      },
      "_artifactsLocation": {
        "type": "string",
        "defaultValue": "[deployment().properties.templateLink.uri]",
        "metadata": {
          "description": "Auto-generated container in staging storage account to receive post-build staging folder upload"
        }
      },
      "_artifactsLocationSasToken": {
        "type": "securestring",
        "metadata": {
          "description": "Auto-generated token to access _artifactsLocation"
        }
      }

Anschließend wird ein Commit ausgeführt, um die Datei im Repository zu speichern.

Serverkonfiguration nach CIS

Bevor die im ARM Template referenzierten Dateien „install.sh“ und „ruleset-params.txt“ erstellt werden, ein kurzer Exkurs, wie wir die Härtung des Servers durchführen. Hierfür gibt es von Canonical die CIS Compliance for Ubuntu, welche Abonnenten von Ubuntu Advantage zur Verfügung steht. Es handelt sich um ein OpenSCAP-basiertes Tool zur Automatisierung von Härtung und deren Auditing und basiert auf dem erwähnten CIS Benchmark.

In der CIS Dokumentation sind jedoch einige Regeln aufgeführt, die das Tool nicht automatisiert anpassen kann. Dazu gehören z.B. die besonderen Anforderungen an Dateisysteme und Partitionierung. Hier unterliegt man durch den Einsatz des Azure Ubuntu Standard Image zusätzlichen Einschränkungen, es lässt sich jedoch fast alles über die „install.sh“ anpassen.

Die „install.sh“

Dabei handelt es sich um ein gewöhnliches Shell-Script, jedoch wird im vorliegenden Beispiel bei der Entwicklung ein deklarativer Ansatz verfolgt. So kann das Script mehrfach ausgeführt werden, ohne doppelte oder falsche Einträge in Config-Dateien zu erzeugen. Diese Datei wird wie zuvor erstellt – allerdings durch Angabe des kompletten Pfades „CustomScripts/install.sh“ automatisch im Unterordner „CustomScripts“:

Azure Repository - Unterordner erstellen
Azure Repository – Unterordner erstellen

Ein für dieses Beispiel stark vereinfachtes aber funktionales Script stellen wir Ihnen hier zum Download bereit. In diesem Beispiel wird ein Commit ausgeführt.

Die „custom-ruleset-params.txt“

Die „custom-ruleset-params.txt“ wird ebenfalls in dem genannten Unterordner erstellt. Hier ist bei der Benennung die Datei-Endung relevant, ansonsten funktioniert die Funktion „Dos2Unix“ der CSE nicht korrekt. Diese Datei ist als „ruleset-params.conf“ am Paket „usg-cisbenchmark“ enthalten. Hier können wir Einstellungen für das CIS Hardening treffen. Die Parameter sind hier erklärt. Zusätzlich dazu wurden am Ende die folgenden Zeilen eingefügt:

# Rulesets for Custom profile
ruleset1="1.1.1.1 1.1.1.2"
ruleset2="2.2.1.4 2.2.3 2.2.5"
#ruleset3=""
#ruleset4=""
#ruleset5=""
#ruleset6=""

Hier können alle benötigten Richtlinien des CIS Benchmarks einzeln aktiviert werden. Das „install.sh“ startet das „Canonical_Ubuntu_20.04_CIS-harden.sh“ auf folgende Weise mit diesem Custom-Profil:

$PATH_CIS/$FILE_CIS -f $PATH_CIS/$CIS_CUSTOM_RULESET_PREFIX.conf custom

Wem das zu aufwendig ist, der kann auch eines der folgenden Standardprofile wählen:

CIS Benchmark Profile von Canonical
3.CIS Benchmark Profile von Canonical

Dafür ist die „install.sh“ wie folgt anzupassen (Zeile 87/88):

#$PATH_CIS/$FILE_CIS -f $PATH_CIS/$CIS_CUSTOM_RULESET_PREFIX.conf custom
$PATH_CIS/$FILE_CIS lvl1_server

Beide Dateien werden beim Ausführen der Pipeline in die „_artifactsLocation“ und von dort durch die CSE auf die VM kopiert. Die „install.sh“ wird dann als root mit den definierten Parametern ausgeführt. Bei der Fehlerbehebung hilft ein Blick in das Verzeichnis „/var/lib/waagent/custom-script/download/“. Dort wird für jedes Deployment ein Unterverzeichnis für Daten und Logs der CSE angelegt.

3. Schritt: Azure Pipeline erstellen

Bevor die eigentliche Pipeline erstellt wird, wird zunächst eine Variablengruppe für die Übergabe der benötigten Secrets aus dem Azure Key Vault (KV) angelegt. Dazu wird der Punkt Library gewählt und eine neue Variablengruppe „UbuntuLinux-KV“ erstellt, wie in den Microsoft Docs beschrieben. Anschließend wird „Link secrets from an Azure key vault as variables“ aktiviert und folgende Variablen aus dem KV angelegt (diese wurden bereits im KV angelegt):

Azure Pipeline - Variablengruppe erstellen
Azure Pipeline – Variablengruppe erstellen

Für das Deployment soll nun eine Pipeline erstellt werden. Um das anschaulicher darzustellen, wird über „Pipelines -> Create Pipeline“ den Textlink „Use the classic editor“ gewählt. Für das neuerstelltes Projekt ist die Quelle bereits korrekt gesetzt. Im nächsten Schritt wird „empty Job“ gewählt und alle Einstellungen werden dabei belassen.

Die zuvor erstellte Variablengruppe wird nun mit der Pipeline verknüpft:

Azure Pipeline - Variablengruppe verknüpfen
Azure Pipeline – Variablengruppe verknüpfen

Zusätzlich wird dazu wird über „Pipeline variables“ die benötigten Variablen für das ARM Template angelegt und der Haken „Settable at queue time“ gesetzt:

Azure Pipeline - Pipeline Variablen festlegen
Azure Pipeline – Pipeline Variablen festlegen

Über „Tasks“ und das „+“ neben „Agent job 1“ wird nun der Pipeline folgende Tasks hinzugefügt:

Azure Pipeline - Task anlegen
Azure Pipeline – Task anlegen

Das Azure File Copy wird anschließend per YAML wie folgt konfiguriert (dabei angezeigte Fehler zu Variablen einfach ignorieren!):

steps:
- task: AzureFileCopy@4
  displayName: 'Azure File Copy'
  inputs:
    SourcePath: 'CustomScripts/*'
    azureSubscription: '[My Service Connection]'
    Destination: AzureBlob
    storage: [My Storage Account]
    ContainerName: '$(RESOURCEGROUP)-stageartifacts'
    BlobPrefix: CustomScripts
    AdditionalArgumentsForBlobCopy: '--log-level=INFO'

Zusätzlich setzen wir noch über die „Output Variables“ den „Reference name“ auf „artifactsLocation“:

Azure Pipeline - Task Optionen anpassen
Azure Pipeline – Task Optionen anpassen

Damit stehen die notwendigen Daten bereit, die für den Zugriff auf den Storage Account (SA) für den nachfolgenden Task in einer Variablen gespeichert sind. Damit dieser Task erfolgreich ist, muss für diesen Schritt dem Service Principal noch die Rolle „Storage Blob Data Contributor“ mit Scope für den referenzierten SA „[My Storage Account]“ gegeben werden.

Das ARM Template deployment wird per YAML wie folgt konfiguriert (dabei angezeigte Fehler zu Variablen einfach ignorieren!):

steps:
- task: AzureResourceManagerTemplateDeployment@3
  displayName: 'ARM Template deployment: Resource Group scope'
  inputs:
    azureResourceManagerConnection: '[My Service Connection]'
    subscriptionId: ' [00000000-0000-0000-0000-000000000000] '
    resourceGroupName: '$(RESOURCEGROUP)'
    location: '$(LOCATION)'
    csmFile: azuredeploy.json
    overrideParameters: '-vmName $(VMNAME) -adminUsername $(ADMINNAME) -adminPasswordOrKey $(adminPassword) -cseUbuntuLaunchpadId $(lnx-ubuntu-your-launchpad-id) -cseUbuntuPpaPassword $(lnx-ubuntu-PPA-password) -_artifactsLocation "$(artifactsLocation.StorageContainerUri)" -_artifactsLocationSasToken "$(artifactsLocation.StorageContainerSasToken)"'

4. Schritt: Deployment starten

Es ist soweit! Über „Save & Queue“ wird nun die Pipeline gespeichert und gestartet:

Azure Pipeline - Pipeline starten
Azure Pipeline – Pipeline starten

Über „Variables“ im unteren Teil können noch die Parameter für die Erstellung der VM angepasst werden und dann die Pipeline gestartet werden. Das Ergebnis sieht in Azure dann wie folgt aus:

Liste der erstellten Azure Ressourcen
Liste der erstellten Azure Ressourcen

Wie viel Potenzial steckt hinter Ubuntu Linux über DevOps?

Dies ist ein sehr einfacher Weg, um mit einfachsten Mitteln eine maximale Härtung zu erzielen. Um diese nicht ins Absurdum zu führen, wird beim Einsatz des Quickstart Templates nach dem Deployment selbstverständlich als Erstes über die Public IP und die Konfiguration der Network Security Group nachgedacht. Und deren Konfiguration angepasst.

Und wie geht es weiter? Ubuntu Linux Deployment über Azure DevOps geht nicht nur schnell, durch Einsatz des Azure Security Center kann der Server kontinuierlich auditiert werden und Abweichungen von den Richtlinien festgestellt werden. Azure Monitor bildet eine zentrale Stelle zur Auswertung von Logs und Verfügbarkeit, und mit Azure Update Management werden relevante Systemupdates verteilt.

Und im eigenen Rechenzentrum? Hier kann Ubuntu Linux zwar nicht so einfach deployen, mit Hilfe von Azure ARC aber die Härtung verskriptet durchführen und auch die drei zuvor genannten Platformdienste einsetzen.

Ihr Interesse wurde geweckt und Sie wollen ebenfalls Ihren nächsten Server auch mit einem einfachen Click bereits gehärtet deployen? Dann melden Sie sich bei uns, unsere zertifizierten Experten unterstützen Sie gerne!