CONNECTED Conference 2023 - Aufzeichnungen jetzt hier verfügbar +++                     

Suche

über alle News und Events

 

Alle News

 

Viele unserer Kunden überlegen derzeit, was sie in...

Weiterlesen

Lösungsansatz – was benötigt man dafür:

  • Einen...
Weiterlesen

Es gibt im Preview die Möglichkeit Azure Blob...

Weiterlesen

In diesem Blogeintrag werden wir drei sichere und...

Weiterlesen

Im vergangenen Jahr war bei uns in Hamburg viel in...

Weiterlesen

Heutzutage werden Token für die Authentifizierung...

Weiterlesen

Versionsverwaltungssoftware gehört zu den...

Weiterlesen

Das Azure API Management self-hosted Gateway von...

Weiterlesen

Alles Hybrid! QUIBIQ Talkmasters bietet gemeinsam...

Weiterlesen

Während man in Visual Studio für BizTalk basierend...

Weiterlesen

How-to: Azure API Management Self-Hosted Gateway - Token Cycling

The self-hosted gateway is a feature of the API Management to support the management of hybrid and multi-cloud environments. More clearly, this feature enables organizations to manage their APIs hosted either on Azure cloud, on-premises, or on any other cloud, from a single API Management service in Azure. This is done through deploying a containerized version of the API Management Gateway to the same environment where the APIs are hosted. This containerized version of the gateway can be deployed in Docker, or in a Kubernetes cluster.

Connectivity to Azure:

The Self-Hosted Gateway requires a connection to Azure in order to:

  • Report its status through sending a heartbeat every minute.
  • Check regularly for configuration updates and apply them.

However, in order for the self-hosted gateway to be able to download the configuration data from the API Management endpoint; it needs a valid access token.

Token Cycling / Token Rotation:

The access token given for the self-hosted gateway is only valid for 30 days. Therefore, this token needs to be regenerated and assigned to the self-hosted gateway either manually or through automation before it expires in order for the self-hosted gateway to keep accessing and getting the configuration updates.

In order to automatically rotate API management self-hosted gateway tokens, we can have Kubernetes CronJob that runs on a regular basis and regenerate the token and assign it to the self-hosted gateway.

The following section describes a project called “apim-shg-rotate” which is a Docker container image that can be deployed in a Kubernetes CronJob to automatically rotate API management self-hosted gateway tokens regularly. ]1[

1. Requirements:

  • A Kubernetes cluster, and a self-hosted gateway into that cluster.
  • A service principal with one of the following roles:
    • API Management Service Contributor (built-in role). This role gives access to far more than what we actually need here.
    • API Management Self-Hosted Gateway Token Operator (custom role). This role gives only two permissions, to list the self-hosted gateways and to generate new tokens. ]2[ There are specific steps that should be followed in order to create a custom role.

2. Deployment in the cluster:

The script should be deployed in the same namespace as the API management self-hosted gateway.

The following is required for the deployment:

  • Kubernetes service principal secret, client ID, client secret, and tenant ID.
  • RBAC which is a .yaml file for Role, Role Binding, and Service Account.
  • CronJob

]1[ https://github.com/phealy/apim-shg-rotate
]2[ https://docs.microsoft.com/en-us/azure/role-based-access-control/custom-roles

The following steps describe in detail the deployment process:

  1. Create the following “updateToken.sh” script file.
    PS: Double click the below text snippet to open the full script file.

#! /bin/bash
set -eo pipefail

# updateToken.sh
#
# A shell script to use the Azure CLI and kubectl in order to rotate a token on a
# self-hosted application gateway. Designed to be run in a container using a Kubernetes
# CronJob.
#
# Based on an idea from https://github.com/phealy/apim-shg-rotate
# but rewritten.

 function usage {
    cat <<-EOF

     Usage: $(basename $0)
      -s|--subscription-id <SUBSCRIPTION_ID>
      -r|--resource-group <RESOURCE_GROUP>
      -a|--apim-instance <APIM_INSTANCE>
      -g|--apim-gateway <APIM_GATEWAY>
      -n|--namespace <NAMESPACE>
      -t|--token-secret <TOKEN_SECRET>
      -k|--token-key <TOKEN_KEY: last, rotate, primary, secondary>
      -o|--k8s-object <K8S_OBJECT>
      -c|--client-id <SP_ID>
      -p|--client-secret <SP_SECRET>
      --debug

     Generates a new token for an APIM self-hosted gateway, updates the Kubernetes secret, then performs a rolling restart.
    Arguments may be specified as parameters or as environment variables.     

    Arguments:

      -s, --subscription-id, SUBSCRIPTION_ID environment variable
            The GUID of the scription containing the APIM instance. 

      -r, --resource-group, RESOURCE_GROUP environment variable
            The name of the resource group containing the APIM instance. 

      -a, --apim-instance, APIM_INSTANCE environment variable
            The name of the APIM instance 

      -g, --apim-gateway, APIM_GATEWAY environment variable
            The name of the self-hosted gateway 

      -n, --namespace, NAMESPACE environment variable
            The namespace containing the token to rotate

       -t, --token-secret, TOKEN_SECRET environment variable
            The name of the Kubernetes secret object containing the token to rotate 

      -k, --token-key, TOKEN_KEY
            Switch the key used to generate the token. Can be set to one of the following values:
              "last" - default, the value from the last-key-used annotation on the token will be used

                      if no annotation is present, defaults to primary
              "rotate" - rotate from the last used key to the other key: primary -> secondary or secondary -> primary
              "primary" - generate the new token from the primary key
              "secondary" - generate the new token from the secondary key

      -o, --k8s-object, K8S_OBJECT
            The name of the Kubernetes object to restart after updating the secret, using kubectl rollout restart.           Should be specified in the form "type/name", e.g. "deployment/apim-gateway" or "statefulset/apim-gateway"

      --client-id, SP_ID
            The client ID of the service principal to use, if the login method is "sp"       

      --client-secret, SP_SECRET
            The client secret of the service principal to use, if the login method is "sp". This can be either a secret or the path to a certificate.

      --tenant, TENANT
            The tenant ID of the service principal to use, if the login method is "sp".       

      --debug
            Enable debug logging (set -x) 

    EOF
    exit 1

} 

# For SP login, we make a temporary directory. Clean it up on exit.
function cleanup {
  [[ ! -z "${MY_AZURE_CONFIG_DIR}" ]] && {
    rm -rf "${MY_AZURE_CONFIG_DIR}"
  }

}
trap cleanup EXIT

PARSED_ARGUMENTS=$(getopt -a -n "$(basename $0)" -o s:r:a:g:n:t:k:o:l:h --long subscription-id:,resource-group:,apim-instance:,apim-gateway:,namespace:,token-secret:,token-key:,k8s-object:,client-id:,client-secret:,tenant:,debug,help -- "$@")
VALID_ARGUMENTS=$?
if "$VALID_ARGUMENTS" != "0" ]; then
    usage

fi 

eval set -- "$PARSED_ARGUMENTS"
while :
do

    case "$1" in
      --debug) set -x; shift;;
      -s | --subscription-id) SUBSCRIPTION_ID="${2}"shift 2;;
      -r | --resource-group) RESOURCE_GROUP="${2}"shift 2;;
      -a | --apim-instance) APIM_INSTANCE="${2}"shift 2;;
      -g | --apim-gateway) APIM_GATEWAY="${2}"shift 2;;
      -n | --namespace) NAMESPACE="${2}"shift 2;;
      -t | --token-secret) TOKEN_SECRET="${2}"shift 2;;
      -k | --token-key) TOKEN_KEY="${2}"shift 2;;
      -o | --k8s-object) K8S_OBJECT="${2}"shift 2;;
    --client-id) SP_ID="${2}"shift 2;;
    --client-secret) SP_SECRET="${2}"shift 2;;
    --tenant) TENANT="${2}"shift 2;;
      -h | --help) usage;;
      --) shiftbreak ;;
      *) echo "ERROR: didn't parse an argument properly: ${1} ${2}"; usage;;
    esac

done 

# Set defaults
TOKEN_KEY="${TOKEN_KEY:-last}"
MASKED_SP_SECRET="${SP_SECRET//?/*}" 

# Error out if variables aren't set
for var in SUBSCRIPTION_ID RESOURCE_GROUP APIM_INSTANCE APIM_GATEWAY NAMESPACE TOKEN_SECRET; do
    if [[ -z "${!var}" ]]; then
    FAIL=1
      echo -e "ERROR: Parameter ${var} is undefined. Please specify the parameter as either an environment variable or a command argument."
    fi

done
if [[ "${FAIL:-0}" == "1" ]]; then
  echo -e "\nRun $(basename $0) --help for usage information."
  exit 1

fi

cat << EOF

APIM Self-hosted Gateway Secret Rotation
Date: $(date)
----------------------------------------

Parameters:
Subscription ID:  $SUBSCRIPTION_ID
Resource group:   $RESOURCE_GROUP
APIM Instance:    $APIM_INSTANCE
APIM Gateway:     $APIM_GATEWAY
K8s Namespace:    $NAMESPACE
K8s Token Secret: $TOKEN_SECRET
APIM Key Source:  $TOKEN_KEY
K8S Object:       ${K8S_OBJECT:-not provided}
Client ID:        ${SP_ID:-not provided}
Client Secret:    ${MASKED_SP_SECRET:-not provided}
Client Tenant:    ${TENANT:-not provided}

---------------------------------------- 

EOF

for var in SP_ID SP_SECRET TENANT; do
    if [[ -z "${!var}" ]]; then
    FAIL=1
        echo -e "ERROR: When using service principal login type, ${var} must be defined!"
    fi

done
if [[ "${FAIL:-0}" == "1" ]]; then
    echo -e "\nRun $(basename $0) --help for usage information."
    exit 1

fi
MY_AZURE_CONFIG_DIR=$(mktemp -d)
AZURE_CONFIG_DIR="${MY_AZURE_CONFIG_DIR}"
echo -n "Checking for service principal access..."
# Try a service principal
az login --service-principal -u "$SP_ID" -p "$SP_SECRET" --tenant $TENANT >/dev/null
if [[ $(az account list --refresh --query "length([?id=='$SUBSCRIPTION_ID'])" 2>/dev/null) == 1 ]]; then
    echo "done (logged in via service principal)."

else
    echo -e "\n\nERROR: Failed to access the subscription via az cli, either via already logged in credentials or identity."
    exit 1

fi 

echo -n "Validating APIM instance is present and correct..."
APIM_RESOURCE_ID=$(az apim show --subscription $SUBSCRIPTION_ID --resource-group $RESOURCE_GROUP --name $APIM_INSTANCE --query "id" --only-show-errors -o tsv 2>&1) || {
    echo -e "\n\nERROR: Unable to find $APIM_INSTANCE in resource group $RESOURCE_GROUP in subscription $SUBSCRIPTION_ID."
    echo -e "\nCommand output:\n${APIM_RESOURCE_ID}"
    exit 1

}
echo "done." 


echo -n "Validating APIM gateway instance is present and correct..."
GATEWAY_RESOURCE_ID="${APIM_RESOURCE_ID}/gateways/${APIM_GATEWAY}"
OUTPUT=$(az rest --method GET --uri "https://management.azure.com${GATEWAY_RESOURCE_ID}" --uri-parameters "api-version=2021-08-01" 2>&1) || {
    echo -e "\n\nERROR: Unable to query APIM self-hosted gateway instance properties."
    echo -e "\nAPI output:\n${OUTPUT}"
    exit 1

}
echo "done."

echo -n "Validating Kubernetes secret is present and correct..."
LAST_USED_KEY_ANNOTATION=$(kubectl --namespace $NAMESPACE get secret $TOKEN_SECRET -o jsonpath='{.metadata.annotations.last-used-key}' 2>&1) || {
    echo -e "\n\nERROR: unable to retrieve Kubernetes secret ${TOKEN_SECRET}."
    echo -e "\nkubectl output:\n${LAST_USED_KEY_ANNOTATION}"
    exit 1

}
echo "done (last used key: \"${LAST_USED_KEY_ANNOTATION:-unset}\")."
 

echo -n "Determining which key to use to generate the token..."
case "${TOKEN_KEY}" in
    last)
      TOKEN_KEY="${LAST_USED_KEY_ANNOTATION:-primary}"
      ;;
    primary)
      TOKEN_KEY="primary"
      ;;
    secondary)
      TOKEN_KEY="secondary"
      ;;
    rotate)
      case "${LAST_USED_KEY_ANNOTATION:-secondary}" in
        primary) TOKEN_KEY="secondary";;
        secondary) TOKEN_KEY="primary";;
        esac
      ;;
    *)
      echo -e "\n\nERROR: Unrecognized argument to -k/--token-key/TOKEN_KEY."
      usage
      ;;

esac
echo "done - token will be generated from the ${TOKEN_KEY} key."

echo -n "Generating new token for gateway..."
GENERATE_TOKEN_URL="https://management.azure.com${GATEWAY_RESOURCE_ID}/generateToken?api-version=2021-08-01"
TOKEN_EXPIRATION_DATE="$(date -Iseconds -d"@$(($(date +%s)+1592000))")" # Date +30 days; have to use this format for busybox date
TOKEN=$(az rest --method POST --uri $GENERATE_TOKEN_URL --body "{ \"expiry\": \"${TOKEN_EXPIRATION_DATE}\", \"keyType\": \"${TOKEN_KEY}\" }" --query value -o tsv) || {
    echo -e "\n\nERROR: unable to generate new token for gateway."
    echo -e "\nAPI call output:\n${TOKEN}"
    exit 1

}
echo "done." 


echo -n "Updating Kubernetes secret..."
OUTPUT=$(kubectl --namespace $NAMESPACE create secret generic ${TOKEN_SECRET} --from-literal value="GatewayKey ${TOKEN}" --dry-run=client -o yaml 2>&1 | kubectl --namespace $NAMESPACE apply -f - 2>&1) || {
    echo -e "\n\nERROR: Unable to update token secret."
    echo -e "\nkubectl output:\n${OUTPUT}"
    exit 1

}
echo "${OUTPUT}"

 

echo -n "Annotating token secret with \"last-used-key: ${TOKEN_KEY}\"..."
OUTPUT=$(kubectl --namespace $NAMESPACE annotate --overwrite=true secret $TOKEN_SECRET last-used-key="${TOKEN_KEY}" 2>&1) || {
    echo -e "\n\nERROR: Failed to annotate token secret."
    echo -e "\nkubectl output:\n${OUTPUT}"
    exit 1

}
echo "${OUTPUT}"

 

if [[ ! -z "${K8S_OBJECT}" ]]; then
    echo -n "Performing a rolling restart of the self-hosted gateway..."
    OUTPUT=$(kubectl --namespace $NAMESPACE rollout restart $K8S_OBJECT 2>&1) || {
      echo -e "\n\nERROR: Failed to restart $K8S_OBJECT."
      echo -e "\nkubectl output:\n${OUTPUT}"
      exit 1
    }
    echo "${OUTPUT}"

fi

 
echo 

echo "Token rotation complete."
echo "A new token was generated based on the ${TOKEN_KEY} APIM key and will expire on ${TOKEN_EXPIRATION_DATE}."

2. Create a Docker image.
PS: Use Dockerfile to create the image as follow:

FROM mcr.microsoft.com/azure-cli:latest
WORKDIR /root

RUN /usr/local/bin/az aks install-cli

COPY updateToken.sh /root

ENTRYPOINT "/root/updateToken.sh"


3. Push the image into a Docker Hub Repository.

4. Create the rbac.yaml file as follows:
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: apim-shg-rotate

---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: apim-shg-rotate

rules:
  - apiGroups: ["*"]
    resources: ["deployments"]
    verbs: ["get", "patch"]
  - apiGroups: ["*"]
    resources: ["secrets"]
    verbs: ["get", "update", "patch"]
  - apiGroups: ["*"]
    resources: ["statefulsets"]
    verbs: ["get", "patch"]

---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: apim-shg-rotate

subjects:
  - kind: ServiceAccount
    name: apim-shg-rotate
roleRef:
  kind: Role
  name: apim-shg-rotate
  apiGroup: rbac.authorization.k8s.io

 

5. Create the cronjob.yaml file as follows:
PS: Adapt the variables inside this cronjob.yaml file according to your information. Variables that need to be adapted are marked with #adapt.
---
apiVersion: batch/v1
kind: CronJob
metadata:
  name: apim-shg-rotate

spec:
  concurrencyPolicy: Forbid
  schedule: "0 9 1,15 * *"           #adapt
  jobTemplate:
    spec:
      activeDeadlineSeconds: 600
      completions: 1
      parallelism: 1
      template:
        metadata:
          labels:
            aadpodidbinding: apim-shg-rotate
        spec:
          containers:
            - name: update-token
              image: qbqalex/rotate4shg:latest     #adapt
              imagePullPolicy: Always
              env:
                - name: SUBSCRIPTION_ID
                  value: SUBSCRIPTION_ID_Value     #adapt
                - name: RESOURCE_GROUP
                  value: Resource_Group_Name       #adapt
                - name: APIM_INSTANCE
                  value: APIM_INSTANCE_Name   #adapt
                - name: APIM_GATEWAY                            
                  value: APIM_GATEWAY_Name         #adapt
                - name: NAMESPACE
                  value: APIM_SHG_NAMESPACE        #adapt
                - name: TOKEN_SECRET
                  value: APIM_TOKEN_Secret         #adapt
                - name: TOKEN_KEY
                  value: APIM_TOKEN_KEY            #adapt
                - name: K8S_OBJECT
                  value: APIM_SHG_OBJECT           #adapt
                - name: SP_ID
                  value: Service_Principal_ID      #adapt
                - name: SP_SECRET
                  value: Service_Principal_Secret  #adapt
                - name: TENANT

                  value: Tenant_ID                #adapt

          restartPolicy: Never

          serviceAccountName: apim-shg-rotate

          automountServiceAccountToken: true
 

6. Finally, apply the two .yaml files created in the previous two steps as follows:

             kubectl apply -f rbac.yalm
             kubectl apply -f cronjob.yalm

Now the CronJob will run on a regular basis, depending on the “schedule” chosen in the cronjob.yaml file.

For example, if the schedule was chosen as follow 0 0 */25 * * : this means that CronJob will regenerate the token at 00:00 on every 25th day of the month, i.e., the token will be regenerated one time each month before it expires, and it will be re-assigned to the self-hosted gateway.

Ihre Kontaktmöglichkeiten

Sie haben eine konkrete Frage an uns


 

Bleiben Sie immer auf dem Laufenden


 

Mit meinem "Ja" erkläre ich mich mit der Verarbeitung meiner Daten zur Zusendung von Informationen einverstanden. Ich weiß, dass ich diese Erklärung jederzeit durch einfache Mitteilung widerrufen kann. Bei einem Nein an dieser Stelle erhalte ich zukünftig keine Informationen mehr.

© QUIBIQ GmbH · Impressum · Datenschutz