16. Mai 2024

Hashicorp Vault Secrets Server selbst hosten (Docker)

Gepostet am 16. Mai 2024  •  15 Minuten  • 3184 Wörter  • Andere Sprachen:  English

Ich habe vor kurzem den Vault-Server von Hashicorp evaluiert und ihn auf mehreren Maschinen in einem einfachem Setup aufgesetzt. Da die Anleitungen im Netz etwas verstreut sind, dokumentiere ich mein Vorgehen in der Hoffnung, dass es anderen helfen mag.

Umgebung

Ich verzichte auf Kubernetes oder Docker Swarm (in beiden kann man mit entsprechendem Placing der Container das ganz schick lösen, wenn man die Herausforderung des Storage meistert). Dass wir getrennte Docker-Instanzen verwenden, hat im Projekt historische Gründe. Der Vorteil ist, dass ich hier die einzelnen Schritte ganz gut dokumentieren kann 😇

Wir haben drei Nodes. Auf jeder läuft Docker und sie sind per VLAN miteinander verbunden:

Auf den Servern läuft Ubuntu 22.04. Wer kein Docker verwenden mag, kann Vault auch als Dienst installieren - das Vorgehen ist so schwer nicht, man muss die Befehle unten entsprechend anpassen. Die verwendete Vault-Version ist 1.16.

Zertifikate erstellen

Zunächst erstellen wir die Zertifikate für die verschlüsselte Kommunikation mit Vault (und zwischen den Instanzen). Wir erstellen dazu eine Certificate Authority (CA) und Zertifikate für jede Vault-Node. Auf einem beliebigen Server führen wir Folgendes aus:

sudo mkdir /etc/vault
cd /etc/vault

sudo tee extfile.cnf << EOF
[ req ]
distinguished_name = req_distinguished_name
x509_extensions = v3_ca
prompt = no

[ req_distinguished_name ]
C = DE
ST = MyState
L = MyLocation
O = MyPlace
CN = RootCA

[ v3_ca ]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical,CA:true
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
EOF

sudo tee extfile01.cnf << EOF
[ req ]
distinguished_name = req_distinguished_name
req_extensions = req_ext
prompt = no

[ req_distinguished_name ]
C = DE
ST = MyState
L = MyLocation
O = MyPlace
CN = app01

[req_ext]
subjectAltName = @alt_names

[alt_names]
IP.1 = 192.168.100.111
IP.2 = 127.0.0.1
DNS.1 = app01
DNS.2 = vault
EOF

sudo tee extfile02.cnf << EOF
[ req ]
distinguished_name = req_distinguished_name
req_extensions = req_ext
prompt = no

[ req_distinguished_name ]
C = DE
ST = MyState
L = MyLocation
O = MyPlace
CN = app02

[req_ext]
subjectAltName = @alt_names

[alt_names]
IP.1 = 192.168.100.112
IP.2 = 127.0.0.1
DNS.1 = app02
DNS.2 = vault
EOF

sudo tee extfile03.cnf << EOF
[ req ]
distinguished_name = req_distinguished_name
req_extensions = req_ext
prompt = no

[ req_distinguished_name ]
C = DE
ST = MyState
L = MyLocation
O = MyPlace
CN = app03

[req_ext]
subjectAltName = @alt_names

[alt_names]
IP.1 = 192.168.100.113
IP.2 = 127.0.0.1
DNS.1 = app03
DNS.2 = vault
EOF

Damit wurden die Extension Files für OpenSSL erstellt. Diese enthalten für Vault notwendige Erweiterungen der Zertifikate. Ohne diese Extensions weigert sich Vault, mit den anderen Instanzen zu sprechen. Wichtig ist hier, die möglichen eigenen Namen und IPs der Vault-Instanzen anzugeben. Falls man Vault über weitere IPs oder DNS-Namen aufrufen will, müssen die Listen entsprechend erweitert werden!

Jetzt erstellen wir die eigentlichen Zertifikate für die Server:

cd /etc/vault

# CA
sudo openssl genrsa -out ca.key 4096
sudo openssl req -new -x509 -days 3650 -key ca.key -out ca.crt -config extfile.cnf

# Host 01
sudo openssl genrsa -out vault01.key 4096
sudo openssl req -new -key rsa:4096 -key vault01.key -out vault01.csr -config extfile01.cnf
sudo openssl x509 -req -days 3650 -in vault01.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out vault01.crt -extensions req_ext -extfile extfile01.cnf

# Host 02
sudo openssl genrsa -out vault02.key 4096
sudo openssl req -new -key rsa:4096 -key vault02.key -out vault02.csr -config extfile02.cnf
sudo openssl x509 -req -days 3650 -in vault02.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out vault02.crt -extensions req_ext -extfile extfile02.cnf

# Host 03
sudo openssl genrsa -out vault03.key 4096
sudo openssl req -new -key rsa:4096 -key vault03.key -out vault03.csr -config extfile03.cnf
sudo openssl x509 -req -days 3650 -in vault03.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out vault03.crt -extensions req_ext -extfile extfile03.cnf

Wir erstellen also zunächst das Zertifikat der CA und dann jeweils Schlüssel und Zertifikate für die einzelnen Instanzen. Die Zertifikate sind etwa 10 Jahre gültig. Im Produktivbetrieb ist es sicher sinnvoll, Zertifikate mit kürzerer Laufzeit zu verwenden - oder man setzt gleich eine eigene Stelle für die Ausstellung solcher ein, beispielsweise step-ca .

Zum Abschluss testen wir noch, ob die Zertifikate auch die notwendigen Erweiterungen enthalten:

openssl x509 -noout -text -in vault01.crt | grep -A 1 "Subject Alternative Name"
openssl x509 -noout -text -in vault02.crt | grep -A 1 "Subject Alternative Name"
openssl x509 -noout -text -in vault03.crt | grep -A 1 "Subject Alternative Name"

Bei der Ausgabe sollten alle IPs und DNS-Namen aufgelistet sein, die man benötigt.

Wichtig: Zum Abschluss muss man alle erstellten Dateien auf die anderen Server kopieren, z.B. per rsync oder scp. Ich gehe davon aus, dass auf app01, app02, app03 am Ende dieses Schritts die Zertifikate auf allen Servern im Verzeichnis /etc/vault liegen.

Ebenfalls wichtig: Die Dienste müssen untereinander sprechen können. Dazu müssen die TCP-Ports 8200 und 8201 im VLAN offen sein. Hier beispielhaft die Befehle für UFW:

sudo ufw allow proto tcp from 192.168.100.0/24 to any port 8200
sudo ufw allow proto tcp from 192.168.100.0/24 to any port 8201

Auto-Unsealing mit AWS KMS

Für den Produktivbetrieb ist es unablässig, dass der Vault sich öffnet, wenn er neu gestartet wird. Es gibt hier mehrere Möglichkeiten:

Ich habe hier ein wenig experimentiert und bin letztlich beim KMS von AWS gelandet. Dieser ist für 1 USD pro Monat durchaus erschwinglich und ist einfach einzurichten (wenn man weiß, wo man in AWS hinklicken muss 😱). Die anderen Lösungen sind durchaus gangbar, allerdings aufwändiger:

Damit eine kurze Anleitung für AWS:

{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Sid": "VaultKMSUnseal",
			"Effect": "Allow",
			"Action": [
				"kms:Decrypt",
				"kms:Encrypt",
				"kms:DescribeKey"
			],
			"Resource": "*"
		}
	]
}

Damit besitzen wir folgende Informationen:

Start von Vault auf den einzelnen Servern

app01

Wir beginnen mit app01 - das Vorgehen wiederholt sich unten. Wir benötigen die Schlüsseldaten von oben und führen Folgendes aus:

access_key=123
secret_key=secret
kms_key_id=key
kms_region=eu-central-1

cd /etc/vault

# Config
sudo tee vault.hcl << EOF
cluster_addr  = "https://192.168.100.111:8201"
api_addr      = "https://0.0.0.0:8200"

storage "raft" {
  path = "/vault/data"
  
  retry_join {
    leader_api_addr = "https://app02:8200"
    leader_ca_cert_file = "/vault/config/ca.crt"
    leader_client_cert_file = "/vault/config/vault02.crt"
    leader_client_key_file = "/vault/config/vault02.key"
  }
  retry_join {
    leader_api_addr = "https://app03:8200"
    leader_ca_cert_file = "/vault/config/ca.crt"
    leader_client_cert_file = "/vault/config/vault03.crt"
    leader_client_key_file = "/vault/config/vault03.key"
  }
}
listener "tcp" {
  address = "0.0.0.0:8200"
  tls_cert_file = "/vault/config/vault01.crt"
  tls_key_file = "/vault/config/vault01.key"
}
seal "awskms" {
  region     = "${kms_region}"
  access_key = "${access_key}"
  secret_key = "${secret_key}"
  kms_key_id = "${kms_key_id}"
}
EOF

sudo chmod 600 vault.hcl

# Network
docker network create --driver=bridge --subnet=192.168.128.0/24 app

# Volumes
docker volume create vault
docker volume create vault_log

# Access
docker run --rm -v /etc/vault:/data:rw docker.io/hashicorp/vault chown vault:vault /data
docker run --rm -v vault:/data:rw docker.io/hashicorp/vault chown vault:vault /data
docker run --rm -v vault_log:/data:rw docker.io/hashicorp/vault chown vault:vault /data

# Service
docker run -d --restart unless-stopped --network=app --name vault --cap-add IPC_LOCK -p 192.168.100.111:8200:8200 \
   -p 192.168.100.111:8201:8201 --add-host app01:192.168.100.111 --add-host app02:192.168.100.112 \
   --add-host app03:192.168.100.113 -v /etc/vault:/vault/config:ro -v vault:/vault/data -v vault_log:/vault/logs \
   docker.io/hashicorp/vault server

Was passiert hier?

Damit sollte der Server laufen. Im Log kann man sehen, ob alles in Ordnung ist:

docker logs vault

Das Log wird im Moment noch eine Menge Fehlermeldungen enthalten, das ist in Ordnung und auch normal. Wir richten zunächst die anderen Nodes ein.

app02

Hier nur die Befehle:

access_key=123
secret_key=secret
kms_key_id=key
kms_region=eu-central-1

cd /etc/vault

sudo tee vault.hcl << EOF
cluster_addr  = "https://192.168.100.112:8201"
api_addr      = "https://0.0.0.0:8200"

storage "raft" {
  path = "/vault/data"
  
  retry_join {
    leader_api_addr = "https://app01:8200"
    leader_ca_cert_file = "/vault/config/ca.crt"
    leader_client_cert_file = "/vault/config/vault01.crt"
    leader_client_key_file = "/vault/config/vault01.key"
  }
  retry_join {
    leader_api_addr = "https://app03:8200"
    leader_ca_cert_file = "/vault/config/ca.crt"
    leader_client_cert_file = "/vault/config/vault03.crt"
    leader_client_key_file = "/vault/config/vault03.key"
  }
}
listener "tcp" {
  address = "0.0.0.0:8200"
  tls_cert_file = "/vault/config/vault02.crt"
  tls_key_file = "/vault/config/vault02.key"
}
seal "awskms" {
  region     = "${kms_region}"
  access_key = "${access_key}"
  secret_key = "${secret_key}"
  kms_key_id = "${kms_key_id}"
}
EOF

sudo chmod 600 vault.hcl

# Network
docker network create --driver=bridge --subnet=192.168.128.0/24 app

# Volumes
docker volume create vault
docker volume create vault_log

# Access
docker run --rm -v /etc/vault:/data:rw docker.io/hashicorp/vault chown vault:vault /data
docker run --rm -v vault:/data:rw docker.io/hashicorp/vault chown vault:vault /data
docker run --rm -v vault_log:/data:rw docker.io/hashicorp/vault chown vault:vault /data

# Service
docker run -d --restart unless-stopped --network=app --name vault --cap-add IPC_LOCK -p 192.168.100.112:8200:8200 \
  -p 192.168.100.112:8201:8201 --add-host app01:192.168.100.111 --add-host app02:192.168.100.112 \
  --add-host app03:192.168.100.113 -v /etc/vault:/vault/config:ro -v vault:/vault/data -v vault_log:/vault/logs \
  docker.io/hashicorp/vault server

Start app03

Hier nur die Befehle:

access_key=123
secret_key=secret
kms_key_id=key
kms_region=eu-central-1

cd /etc/vault

sudo tee vault.hcl << EOF
cluster_addr  = "https://192.168.100.113:8201"
api_addr      = "https://0.0.0.0:8200"

storage "raft" {
  path = "/vault/data"
  
  retry_join {
    leader_api_addr = "https://app01:8200"
    leader_ca_cert_file = "/vault/config/ca.crt"
    leader_client_cert_file = "/vault/config/vault01.crt"
    leader_client_key_file = "/vault/config/vault01.key"
  }
  retry_join {
    leader_api_addr = "https://app02:8200"
    leader_ca_cert_file = "/vault/config/ca.crt"
    leader_client_cert_file = "/vault/config/vault02.crt"
    leader_client_key_file = "/vault/config/vault02.key"
  }
}
listener "tcp" {
  address = "0.0.0.0:8200"
  tls_cert_file = "/vault/config/vault03.crt"
  tls_key_file = "/vault/config/vault03.key"
}
seal "awskms" {
  region     = "${kms_region}"
  access_key = "${access_key}"
  secret_key = "${secret_key}"
  kms_key_id = "${kms_key_id}"
}
EOF

sudo chmod 600 vault.hcl

# Network
docker network create --driver=bridge --subnet=192.168.128.0/24 app

# Volumes
docker volume create vault
docker volume create vault_log

# Access
docker run --rm -v /etc/vault:/data:rw docker.io/hashicorp/vault chown vault:vault /data
docker run --rm -v vault:/data:rw docker.io/hashicorp/vault chown vault:vault /data
docker run --rm -v vault_log:/data:rw docker.io/hashicorp/vault chown vault:vault /data

# Service
docker run -d --restart unless-stopped --network=app --name vault --cap-add IPC_LOCK -p 192.168.100.113:8200:8200 \
  -p 192.168.100.113:8201:8201 --add-host app01:192.168.100.111 --add-host app02:192.168.100.112 \
  --add-host app03:192.168.100.113 -v /etc/vault:/vault/config:ro -v vault:/vault/data -v vault_log:/vault/logs \
  docker.io/hashicorp/vault server

Vault initialisieren

Nachdem Vault nun auf allen Nodes läuft, können wir den Service initialisieren. Dazu führen wir einen temporären Container auf einem beliebigen Server aus - das hat den Vorteil, dass nach der Beendigung der Arbeit nichts in der History des Benutzers oder der Benutzerin übrig bleibt:

docker run --rm -ti --network=app -v /etc/vault:/vault/config:ro -e VAULT_ADDR=https://vault:8200 \
  -e VAULT_CACERT=/vault/config/ca.crt -P docker.io/hashicorp/vault ash

Der Container ist also temporär und interaktiv und im selben Netzwerk wie unser Vault-Dienst. Wir benötigen auch die Konfiguration und setzen gleich zwei Umgebungsvariablen für den vault-Befehl unten. Das -P ist wichtig, damit wir nicht unseren obigen Portbereich überschreiben. Das Image verwendet die ash-Shell.

In der Shell initialisieren wir den Cluster:

vault operator init

Die Schlüssel und das initiale root-Token sollten wir an einem sicheren Ort speichern!

Damit ist vault einsatzbereit und sollte auch nach Neustarts einzelner Container wieder geöffnet werden. Um das zu testen, kann man den Status prüfen (im temporären Container):

vault operator status

Auf dem Host kann man nun Vault neu starten (docker restart vault) und nach kurzer Zeit sollte der Status wieder auf unsealed stehen.

Exkurs: Zugang und Go-Schnipsel

Hier noch ein kleiner Exkurs, wie ein Go-Programm einen Secret aufrufen könnte.

Daten in Vault ablegen

Wir legen dazu einige Daten in Vault an. Wieder loggen wir uns auf einer beliebigen Node ein und erstellen wieder einen temporären Container für die Vault-Administration:

docker run --rm -ti --network=app -v /etc/vault:/vault/config:ro -e VAULT_ADDR=https://vault:8200 \
  -e VAULT_CACERT=/vault/config/ca.crt -P docker.io/hashicorp/vault ash

Im Container müssen wir das root-Token setzten, das wir in der Initialisation bekommen haben:

export VAULT_TOKEN=MyTOken

Zum Test erstellen wir zunächst einen Secret, den unser Programm holen soll:

vault secrets enable -version=2 -path=app -description="Application secrets" kv

vault kv put -mount=app apiKey key=PaipCijvonEtdysilgEirlOwUbHahachdyazVopejEnerekBiOmukvauWigbimVi

Die Anwendung benötigt einen Zugang, den wir per Approle erstellen:

vault auth enable approle

echo 'path "app/data/apiKey" {
  capabilities = ["read"]
}' | vault policy write myapp -

vault write auth/approle/role/myapp token_ttl=1h token_max_ttl=8h secret_id_ttl=0 token_policies="myapp"
# read data
vault read auth/approle/role/myapp/role-id
# create secret id
vault write -force auth/approle/role/myapp/secret-id

Wir bekommen zwei UUIDs zurückgeliefert, nämlich die Rollen-Id und das Secret. Beides benötigen wir in der Applikation. Im Life-Betrieb kann man die Rolle noch einschränken, indem man IP-Bereiche setzt, von den aus die Applikation zugreifen kann - das habe ich hier ausgelassen, um das Testen zu erleichtern. Probieren wir den Login doch gleich mal aus:

vault write auth/approle/login role_id="123" secret_id="456"

export VAULT_TOKEN=AppRoleToken

vault kv get -mount=app apiKey

Wir erstellen zunächst einen Login mit der Rolle und dem Secret. Wir bekommen ein Token zurück, das wir wiederum als Umgebungsvariable setzen können (und damit das root-Token überschreiben). Wir nehmen quasi die Rolle des Dienstes an. In dieser Rolle können wir versuchen, den API-Key zu lesen, was hoffentlich klappen sollte.

Go-Programm

Ich stelle nur einen Code-Schnipsel vor, der gerne verwendet werden kann. Ich habe Teil der Daten vom Token-Renewal-Beispiel verwendet und es in eine eigene kleine Library gepackt.

Für das Programm müssen/können Umgebungsvariablen gesetzt werden:

Die beiden letzten müssen gesetzt werden!

Hier zunächst die Library:

// vault.go
package main

import (
	"cmp"
	"context"
	"fmt"
	vault "github.com/hashicorp/vault/api"
	"github.com/hashicorp/vault/api/auth/approle"
	"github.com/rs/zerolog/log"
	"os"
)

// VaultClient is the global vault client
var VaultClient *vault.Client

// InitVault initializes the vault client
func InitVault() {
	vaultAddress := cmp.Or(os.Getenv("VAULT_ADDR"), "https://vault:8200")
	vaultCAFile := cmp.Or(os.Getenv("VAULT_CACERT"), "/etc/vault/ca.crt")

	// define config
	config := vault.DefaultConfig() // modify for more granular configuration
	config.Address = vaultAddress
	if err := config.ConfigureTLS(&vault.TLSConfig{
		CAPath: vaultCAFile,
	}); err != nil {
		log.Fatal().Str("VAULT_ADDR", vaultAddress).Str("VAULT_CACERT", vaultCAFile).Err(err).Msg("Failed to configure Vault TLS")
	}

	// create client
	client, err := vault.NewClient(config)
	if err != nil {
		log.Fatal().Str("VAULT_ADDR", vaultAddress).Str("VAULT_CACERT", vaultCAFile).Err(err).Msg("Failed to create Vault client")
	}

	ctx, cancelContextFunc := context.WithCancel(context.Background())
	defer cancelContextFunc()

	// copy to global variable
	VaultClient = client

	// initial login
	authInfo, err := vaultLogin(ctx)
	if err != nil {
		log.Fatal().Str("VAULT_ADDR", vaultAddress).Str("VAULT_CACERT", vaultCAFile).Err(err).Msg("Failed to login to Vault")
	}
	// start the lease-renewal goroutine & wait for it to finish on exit
	go vaultStartRenewLeases(authInfo)

	// everything ok, log success
	log.Info().Str("VAULT_ADDR", vaultAddress).Msg("Vault successfully connected and initial token created.")
}

func vaultLogin(ctx context.Context) (*vault.Secret, error) {
	// Get environment variables for Vault
	vaultAppRoleId := os.Getenv("APPROLE_ROLE_ID")

	if vaultAppRoleId == "" {
		log.Fatal().Msg("Error: Vault App Role not set.")
	}

	// initial login with AppRole
	appRoleAuth, err := approle.NewAppRoleAuth(vaultAppRoleId, &approle.SecretID{
		FromEnv: "APPROLE_SECRET_ID",
	})
	// TODO: we might want to create ResponseWrapping somehow
	// ref: https://www.vaultproject.io/docs/concepts/response-wrapping
	// ref: https://learn.hashicorp.com/tutorials/vault/secure-introduction?in=vault/app-integration#trusted-orchestrator
	// ref: https://learn.hashicorp.com/tutorials/vault/approle-best-practices?in=vault/auth-methods#secretid-delivery-best-practices
	// and example in: https://github.com/hashicorp/hello-vault-go/blob/main/sample-app/vault.go
	if err != nil {
		return nil, err
	}

	return VaultClient.Auth().Login(ctx, appRoleAuth)
}

func vaultStartRenewLeases(authToken *vault.Secret) {
	ctx, cancelContextFunc := context.WithCancel(context.Background())
	defer cancelContextFunc()

	log.Info().Msg("Starting lease renewal service.")
	defer log.Info().Msg("Stopping lease renewal service.")

	currentAuthToken := authToken

	for {
		renewed, err := renewLeases(ctx, currentAuthToken)
		if err != nil {
			log.Fatal().Err(err).Msg("Failed to renew leases")
		}

		if renewed&exitRequested != 0 {
			return
		}

		if renewed&expiringAuthToken != 0 {
			log.Printf("auth token: can no longer be renewed; will log in again")

			authToken, err := vaultLogin(ctx)
			if err != nil {
				log.Fatal().Err(err).Msg("Failed to login to Vault")
			}

			currentAuthToken = authToken
		}
	}
}

// renewResult is a bitmask which could contain one or more of the values below
type renewResult uint8

const (
	renewError renewResult = 1 << iota
	exitRequested
	expiringAuthToken                // will be revoked soon
)

func renewLeases(ctx context.Context, authToken *vault.Secret) (renewResult, error) {
	log.Info().Msg("Starting lease renewal.")

	// auth token
	authTokenWatcher, err := VaultClient.NewLifetimeWatcher(&vault.LifetimeWatcherInput{
		Secret: authToken,
	})
	if err != nil {
		return renewError, fmt.Errorf("unable to initialize auth token lifetime watcher: %w", err)
	}

	go authTokenWatcher.Start()
	defer authTokenWatcher.Stop()

	// monitor events from all watchers
	for {
		select {
		case <-ctx.Done():
			return exitRequested, nil

		// DoneCh will return if renewal fails, or if the remaining lease
		// duration is under a built-in threshold and either renewing is not
		// extending it or renewing is disabled.  In both cases, the caller
		// should attempt a re-read of the secret. Clients should check the
		// return value of the channel to see if renewal was successful.
		case err := <-authTokenWatcher.DoneCh():
			// Leases created by a token get revoked when the token is revoked.
			return expiringAuthToken, err

		// RenewCh is a channel that receives a message when a successful
		// renewal takes place and includes metadata about the renewal.
		case info := <-authTokenWatcher.RenewCh():
			log.Printf("auth token: successfully renewed; remaining duration: %ds", info.Secret.Auth.LeaseDuration)

			//case info := <-databaseCredentialsWatcher.RenewCh():
			//	log.Printf("database credentials: successfully renewed; remaining lease duration: %ds", info.Secret.LeaseDuration)
			//}
		}
	}
}

Die Bibliothek kann in einem lang laufenden Dienst eingesetzt werden und erneuert automatisch das Zugangstoken im Hintergrund (als Go-Routine).

Ein kleines Test-Programm:

// main.go
package main

import (
  "context"
  "fmt"
)

func main() {
  InitVault()

  secret, err := VaultClient.KVv2("app").Get(context.Background(), "apiKey")
  if err != nil {
    panic("Failed to get token key from Vault")
  }

  fmt.Printf("API-Key: %s\n", secret.Data["key"])
}

In einem Dienst könnte immer auf die globale Variable VaultClient zugegriffen werden in der Sicherheit, dass die Session bzw. das aktuelle Token noch gültig ist.

Titelbild: Immeuble du Crédit Lyonnais - Verwendet unter CC BY-SA 3.0 .

Durch die Einloggen bei den Kommentaren werden zwei Cookies gesetzt. Mehr Informationen im Impressum.
Follow me