Intranet mit WordPress absichern: die totale Abschottung einrichten

Dies ist ein Beitrag aus der Serie Intranet mit WordPress, in der ich beschreibe, wie ein Intranet-System mit WordPress aufgebaut werden kann, das mehr als ein Blog ist. Hier findet ihr den Ankündigungs-Beitrag. Hier findet ihr die Übersicht aller Beiträge der Serie.


Ein Intranet ist schon dem Begriff nach für interne Zwecke konzipiert. Dementsprechend werden Intranet-Systeme in der Regel zugriffsbeschränkt aufgesetzt. Dies kann über verschiedene Techniken geschehen, die Vor- und Nachteile mit sich bringen:

  • Umsetzung als lokales System, das nur im Unternehmens-Netzwerk erreichbar ist. In diesem Fall können Mitarbeiter/Benutzer eben nur vom Arbeitsplatz aus darauf zugreifen. Ein Funnel-System wäre möglich, um von außerhalb zuzugreifen, würde aber den lokalen Gedanken ad absurdum führen.
  • Umsetzung mit Passwort-Schutz auf Webserver-Ebene mit htpasswd. Dies fordert eine Benutzerauthentifizierung bei Aufrufen der Seite. Wer keine Zugangsdaten hat, kriegt vom Webserver grundlegend keine Antwort. Eine einfache und sichere Möglichkeit. Bei vielen Benutzern müssten entweder Zugangsdaten synchronisiert werden, oder ein allgemeines (weniger unsicher) genutzt werden.
  • Umsetzung mit Passwort-Schutz auf Website-Ebene. Dies ist mit WordPress recht simpel, da das Benutzer- und Rechte-Management schon vorhanden und gut nutzbar ist. Nachteil: in diesem Fall müssen z.B. Dateien im Intranet noch gegen Zugriff von außerhalb abgesichert werden, z.B. mit htaccess.

Da ich letzteren Fall umgesetzt habe, möchte ich hier das Vorgehen und die Maßnahmen aufzeigen, mit denen das WordPress-Intranet abgesichert wurde.

Schritt 1: Freie Registrierung deaktivieren

Die erste Maßnahme ist recht offensichtlich. In den WordPress-Einstellungen unter Allgemein deaktivieren wir die Option „Mitgliedschaft: Jeder kann sich registrieren“. Wir werden später alle Benutzer manuell anlegen, eine eigenständige Registrierung ist nicht gewollt. Somit kann schon mal nur ein Benutzerprofil haben, wen wir aktiv in das Intranet eingeladen haben.

Schritt 2: Sichtbarkeit für Suchmaschinen deaktivieren

Ebenfalls offensichtlich, aber gern vergessen: In den WordPress-Einstellungen unter Einstellungen > Lesen sollte die „Sichtbarkeit für Suchmaschinen“ deaktiviert werden. So stellen wir sicher, dass das gesamte System nicht öffentlich in Suchmaschinen auftaucht.

Schritt 3: Nicht angemeldete Benutzer zur Anmelde-Seite umleiten

Hier kommt schon ein Mini-Code-Schnipsel in’s Spiel. Da wir nicht wollen, dass nicht angemeldete Benutzer irgendetwas vom Intranet sehen können, leiten wir Benutzer immer zur Anmelde-Seite um, wenn sie nicht angemeldet sind.

if ( !is_user_logged_in() ) {
	auth_redirect();
}

Genau diesen Zweck erfüllt auth_redirect, wie auch in der WordPress Code Reference beschrieben.

Dieses simple If-Statement sollte beim Aufbau einer Seite sehr früh platziert werden. Es bietet sich z.B. an, es an erste Stelle einer header.php (insofern vorhanden) zu platzieren, so dass es vor allem anderen steht und auf allen Seiten eingebunden ist.

Schritt 3.1: Nach der Anmeldung zur Startseite umleiten

Dies ist ebenfalls optional und/oder subjektiv. In meinem konkreten Fall war gewünscht, das Backend (WordPress-Administrationsbereich) so weit wie möglich für normale Benutzer zu vermeiden. Daher leiten wir hier immer auf die Frontend-Startseite um, wenn sich ein Benutzer angemeldet hat.

function cr_login_redirect_home( $url, $request, $user ) {
	if( $user && is_object( $user ) && is_a( $user, 'WP_User' ) ) {
		if( $user->has_cap( 'administrator' ) ) {
			$url = admin_url();
		} else {
			$url = home_url();
		}
	}
	return $url;
}
add_filter( 'login_redirect', 'cr_login_redirect_home', 10, 3 );

Was passiert hier? Der filter login_redirect wird durchlaufen, wenn sich ein Benutzer anmeldet. Übergeben wird die Herkunfts-URL, die Anfrage und der Benutzer. Wir prüfen also, ob der Benutzer ein gültiger Benutzer des Systems ist, und leiten ihn auf die home_url() um, wenn dem so ist. That’s basically it.

Kleiner Twist: In meinem konkreten Fall waren alle Benutzer als Redakteure angelegt. Administratoren haben wir beim Login-Redirect daher ausgenommen, da dies meist System-relevante Zugriffe sind. Immer in’s Frontend umgeleitet zu werden, wenn man eigentlich in’s Backend will, war auf Dauer etwas nervig. Daher dieser kleine if-else-Eingriff.

Schritt 3.2: Gültigkeitsdauer des Anmelde-Cookies anpassen

Zugegeben, das ist auch optional, aber meines Erachtens macht es Sinn die Gültigkeitsdauer des Anmelde-Cookies festzulegen, damit eine regelmäßige erneute Anmeldung notwendig ist. So wird sichergestellt, dass Sessions regelmäßig enden und nicht ggfs. Tage später ein Unbefugter über einen Browser zugreift, bei dem die Abmeldung vergessen wurde. In meinem Fall wurde sich für eine Gültigkeitsdauer von 15 Stunden entschieden, was ungefähr eine tägliche Anmeldung bedeutet: Wird sich morgens um 8 Uhr angemeldet, ist die Session bis 23 Uhr gültig. Wer sich später am Tag anmeldet, muss sich trotzdem meist am nächsten Morgen neu anmelden.

function cr_auth_cookie_expiration( $expiration, $user_id, $remember ) {
	if( $remember ) {
		return 60*60*15; // 15 hours
	}
	return $expiration;
}
add_filter( 'auth_cookie_expiration', 'cr_auth_cookie_expiration', 10, 3 );

Schritt 4: Login-Versuche einschränken, z.B. mit Limit Login Attempts

Hier greifen ich ehrlich gesagt gern auf ein Plugin zurück, namentlich Limit Login Attempts Reloaded. Hierüber lassen sich sehr komfortabel Zugriffsversuche beschränken, IPs blocken und auch IPs whitelisten (falls z.B. das Unternehmen einen eigenen IP-Block hat). Da das Plugin seinen Job sehr gut erledigt, verliere ich hier keine großen Worte mehr. Wer diese Funktionalität benötigt, kann guten Gewissens auf das Plugin zurückgreifen.

Schritt 5: Feeds, Embeds, XML-RPC deaktivieren

Um nach außen weiter „abzuschließen“ wurde sich entschieden die Feeds zu deaktivieren, Embeds zu deaktivieren sowie die XML-RPC-Schnittstelle abzuschalten.

Feeds lassen sich über folgendes Snippet deaktivieren:

function sp_disable_feeds() {
	wp_die( 'Kein Feed verfügbar.' );
}
add_action( 'do_feed', 'sp_disable_feeds', 1 );
add_action( 'do_feed_rdf', 'sp_disable_feeds', 1 );
add_action( 'do_feed_rss', 'sp_disable_feeds', 1 );
add_action( 'do_feed_rss2', 'sp_disable_feeds', 1 );
add_action( 'do_feed_atom', 'sp_disable_feeds', 1 );
add_action( 'do_feed_rss2_comments', 'sp_disable_feeds', 1 );
add_action( 'do_feed_atom_comments', 'sp_disable_feeds', 1 );
remove_action( 'wp_head', 'feed_links_extra', 3 );
remove_action( 'wp_head', 'feed_links', 2 );

Embeds deaktiviere ich in der Regel mit dem Plugin Disable Embeds, da mir dies als einfachste und zukunftssicherste Variante erscheint. Im Grunde könnte man den Code auch in die functions.php packen, aber wenn sich dann in Zukunft etwas ändern würde, müsste man es in jedem je ausgelieferten Theme anpassen. Daher setze ich hier gern auf ein Plugin aus dem Repository.

Die XML-RPC Schnittstelle kann – wenn sie nicht verwendet wird – über einen einfachen Filter deaktiviert werden:

add_filter( 'xmlrpc_enabled', '__return_false' );

Die Gefahren dieser Schnittstelle sind zwar sehr gering – man erhält in der Regel „nur“ versuchte Bot-Zugriffe – aber ich handle hier gern nach der Devise „Weg, was nicht gebraucht wird“.

Schritt 6: Admin-Bar verstecken, WP-Logo verstecken, Generator verstecken

Folgende Filter und Snippets setze ich ein, um …

… für Benutzer die Admin-Bar im Frontend zu verstecken:

add_filter( 'show_admin_bar', '__return_false' );

… das WordPress-Logo in der Admin-Bar (falls jemand im Backend landet) zu verstecken:

function cr_remove_toolbar_menu_node() {
	global $wp_admin_bar;
	$wp_admin_bar->remove_menu( 'wp-logo' );
}
add_action( 'wp_before_admin_bar_render', 'cr_remove_toolbar_menu_node', 999 );

… WordPress als Generator der Seite zu verstecken (auch wenn niemand ohne Anmeldung dies einsehen kann):

remove_action( 'wp_head', 'wp_generator' );

Schritt 7: Inhalte durch einfache Permalinks verschleiern

In einer normalen Web-Umgebung wäre es absolut nicht empfehlenswert, in einer geschlossenen Umgebung absolut korrekt: Stellt die Permalink-Struktur unter Einstellungen > Permalinks auf „Einfach“. Dadurch geben geteilte oder irgendwie publik gewordene Links wenigstens keine Hinweise auf den Inhalt preis. Ein https://intranet.com/?p=123 sagt viel weniger aus als ein https://intranet.com/geheimnis-trump-ist-doof/.

Schritt 8: Zugriffe über htaccess beschränken

<IfModule mod_rewrite.c>
	RewriteEngine On
	RewriteCond %{HTTP_REFERER} !^http(s)?://(www\.)?intranet.com [NC]
	RewriteRule \.(jpg|jpeg|png|gif|doc|docx|xls|xlsx|ppt|pptx|pdf)$ - [NC,F,L]
	Options -Indexes
</IfModule>

Hier kommt nun etwas Webserver-Technik in’s Spiel. Dies variiert je nach eingesetzter Technik (NGINX, Apache, etc.) und ist daher nicht universell.

Was passiert hier? Zunächst setzen wir das mod_rewrite Modul von Apache ein. Wir prüfen, ob der HTTP_REFERER das System selbst ist. Wenn das nicht der Fall ist, werden Zugriffe auf Datei-Typen wie jpg, doc und pdf gesperrt. Der Webserver gibt dann einen 403 „Forbidden – You don’t have permission to access this resource.“ Fehler zurück. Dateien können also nur aus dem Netzwerk selbst, nicht über direkte Links aufgerufen werden. Zu guter letzt deaktivieren wir über Options -Indexes die Verzeichnis-Listung, so dass nicht direkt auf Ordner wie die WordPress-eigenen Uploads (z.B. /wp-content/uploads/2019/) zugegriffen werden kann.


That’s it – das sind die Grundlagen, nach denen man WordPress nach außen hin abschotten kann. Natürlich gibt es noch weitere Wege und Facetten. Mach einer möchte auch komplett verschleiern, dass WordPress eingesetzt wird – was ich persönlich für unnötig halte.

Demnächst geht es hier weiter mit Intranet-Themen wie ACF, Frontend-Posting, Einstellungsseiten für Benutzer, Like-System Kalender-Modulen und mehr. Schaut einfach in die Liste aller Intranet-Beiträge.

Einen Kommentar hinterlassen