Vai al contenuto
Avanet

Autenticare la Sophos Email API e instradarla al tenant corretto

Ogni chiamata alla Sophos Email Management API inizia con tre passaggi distinti: un service principal ottiene un token OAuth2 di breve durata, whoami determina tipo e ID del chiamante e solo allora il tenant di destinazione viene associato al proprio host regionale. Un token valido non seleziona da solo tenant o regione.

Il risultato sicuro è un insieme inscindibile: SOPHOS_ACCESS_TOKEN, SOPHOS_TENANT_ID e SOPHOS_API_HOST. L’host globale serve solo per individuare identità e tenant. Le operazioni Email vanno a ${SOPHOS_API_HOST}/email/v1 con Authorization e X-Tenant-ID.

Creare un service principal per l’accesso API Preview

Per un tenant diretto, accedere a Sophos Central Admin come Super Admin e aprire Global Settings > API Credentials. In Sophos Central Partner usare Settings & Policies > API Credentials. Finché le API sono in Preview, l’accesso è di livello SuperAdmin. Limitare queste credenziali all’automazione controllata e non presumere che sia selezionabile un ruolo service principal più ristretto.

  1. Creare credenziali separate per applicazione e ambiente.
  2. Indicare proprietario, scopo e tenant nel nome e nella descrizione.
  3. Documentare come rischio lo stato Preview e l’accesso SuperAdmin e limitare l’uso al tenant approvato.
  4. Salvare subito Client ID e Client Secret in un secret store aziendale.
  5. Monitorare esternamente scadenza, rotazione e contatto di emergenza.

Richiedere l’access token senza lasciare tracce

L’esempio Bash richiede curl e jq, legge il segreto senza mostrarlo, codifica i campi e li invia tramite standard input. In questo modo il segreto non compare nella cronologia shell né negli argomenti di processo espansi:

set -e -o pipefail
cleanup() {
  if [[ -n ${TENANTS_FILE:-} ]]; then
    rm -f "$TENANTS_FILE" "${TENANTS_FILE}.new"
  fi
  unset SOPHOS_CLIENT_SECRET TOKEN_RESPONSE PAGE_RESPONSE WHOAMI
}
trap cleanup EXIT

read -r -p "Client ID: " SOPHOS_CLIENT_ID
read -r -s -p "Client Secret: " SOPHOS_CLIENT_SECRET
printf '\n'

TOKEN_RESPONSE=$(
  printf 'grant_type=client_credentials&client_id=%s&client_secret=%s&scope=token' \
    "$(jq -rn --arg v "$SOPHOS_CLIENT_ID" '$v|@uri')" \
    "$(jq -rn --arg v "$SOPHOS_CLIENT_SECRET" '$v|@uri')" |
  curl --fail-with-body --silent --show-error \
    --request POST \
    --header 'Content-Type: application/x-www-form-urlencoded' \
    --data-binary @- \
    https://id.sophos.com/api/v2/oauth2/token
) || { cleanup; exit 1; }
unset SOPHOS_CLIENT_SECRET

SOPHOS_ACCESS_TOKEN=$(jq -er '
  select(.token_type == "bearer") |
  select((.expires_in | type) == "number" and .expires_in > 0) |
  .access_token | select(type == "string" and length > 0)
' <<<"$TOKEN_RESPONSE") || { cleanup; exit 1; }
unset TOKEN_RESPONSE

grant_type=client_credentials, scope=token, il percorso token e Content-Type: application/x-www-form-urlencoded sono fissi. client_id e client_secret provengono dalle credenziali. La risposta valida contiene access_token, token_type: "bearer" ed expires_in. Alla scadenza richiedere un nuovo token con lo stesso flusso, senza registrare la risposta completa.

Identificare il chiamante con Who-am-I

whoami è una chiamata di discovery globale; anche qui il bearer header passa tramite standard input di curl. Il token viene inserito in questo flusso di configurazione, che l’esempio non mostra:

WHOAMI=$(
  printf 'header = "Authorization: Bearer %s"\n' "$SOPHOS_ACCESS_TOKEN" |
  curl --fail-with-body --silent --show-error \
    --config - https://api.central.sophos.com/whoami/v1
)

SOPHOS_ID=$(jq -er '.id | select(type == "string" and length > 0)' <<<"$WHOAMI")
SOPHOS_ID_TYPE=$(jq -er '
  .idType | select(. == "tenant" or . == "partner" or . == "organization")
' <<<"$WHOAMI")

id è l’UUID del chiamante; idType distingue tenant, partner e organization; apiHosts.global è l’host globale; apiHosts.dataRegion è l’host regionale di un tenant diretto.

Con idType: "tenant", id è anche l’ID tenant richiesto. Validare entrambi i valori prima dell’uso:

UUID_RE='^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$'

[[ "$SOPHOS_ID_TYPE" == "tenant" ]] || {
  printf 'Direct-tenant flow requires idType tenant\n' >&2
  exit 1
}

SOPHOS_TENANT_ID=$(jq -er --arg re "$UUID_RE" '
  .id | select(type == "string" and test($re))
' <<<"$WHOAMI")
SOPHOS_API_HOST=$(jq -er '
  .apiHosts.dataRegion |
  select(type == "string" and test("^https://api-[a-z0-9-]+\\.central\\.sophos\\.com$"))
' <<<"$WHOAMI")

dataRegion assente, idType inatteso o host non valido impongono l’arresto. Non dedurre l’host da esempi, codici regione o tabelle statiche.

Risolvere il tenant di destinazione come partner

Con idType: "partner", l’id di whoami è l’ID partner e non deve diventare X-Tenant-ID. Elencare i tenant tramite l’endpoint partner globale e X-Partner-ID; ogni oggetto fornisce id, dataRegion e apiHost propri.

La prima pagina richiede pageTotal=true; poi si leggono le pagine numerate fino a pages.total, con un limite locale:

[[ "$SOPHOS_ID_TYPE" == "partner" ]] || {
  printf 'Partner flow requires idType partner\n' >&2
  exit 1
}

TENANTS_FILE=$(mktemp) || exit 1
trap cleanup EXIT
printf '[]\n' >"$TENANTS_FILE"

page=1
max_pages=1000
expected_pages=
while (( page <= max_pages )); do
  if (( page == 1 )); then
    PAGE_QUERY=(--data-urlencode 'page=1' --data-urlencode 'pageSize=100' --data-urlencode 'pageTotal=true')
  else
    PAGE_QUERY=(--data-urlencode "page=$page" --data-urlencode 'pageSize=100')
  fi

  PAGE_RESPONSE=$(
    printf 'header = "Authorization: Bearer %s"\n' "$SOPHOS_ACCESS_TOKEN" |
    curl --fail-with-body --silent --show-error --get --config - \
      --header "X-Partner-ID: $SOPHOS_ID" \
      "${PAGE_QUERY[@]}" \
      https://api.central.sophos.com/partner/v1/tenants
  )

  if (( page == 1 )); then
    expected_pages=$(jq -er '
      select((.items | type) == "array") |
      .pages.total | select(type == "number" and floor == . and . >= 1 and . <= 1000)
    ' <<<"$PAGE_RESPONSE")
  else
    jq -e '(.items | type) == "array"' <<<"$PAGE_RESPONSE" >/dev/null || exit 1
  fi

  jq -e --argjson page "$PAGE_RESPONSE" '. + $page.items' \
    "$TENANTS_FILE" >"${TENANTS_FILE}.new" &&
    mv "${TENANTS_FILE}.new" "$TENANTS_FILE" || exit 1

  (( page >= expected_pages )) && break
  ((page++))
done
(( page == expected_pages )) || exit 1
unset PAGE_RESPONSE PAGE_QUERY page expected_pages max_pages

Selezionare il tenant mediante UUID approvato, mai solo tramite name. dataRegion è un identificatore; la richiesta usa il relativo apiHost completo:

read -r -p 'Approved target tenant UUID: ' TARGET_TENANT_ID
TARGET_TENANT=$(jq -cer --arg id "$TARGET_TENANT_ID" --arg re "$UUID_RE" '
  [.[] |
    select((.id | type) == "string" and (.id | test($re))) |
    select((.id | ascii_downcase) == ($id | ascii_downcase)) |
    select((.apiHost | type) == "string" and
           (.apiHost | test("^https://api-[a-z0-9-]+\\.central\\.sophos\\.com$")))] |
  select(length == 1) | .[0]
' "$TENANTS_FILE")

SOPHOS_TENANT_ID=$(jq -r '.id' <<<"$TARGET_TENANT")
SOPHOS_API_HOST=$(jq -r '.apiHost' <<<"$TARGET_TENANT")
rm -f "$TENANTS_FILE"
unset TARGET_TENANT TARGET_TENANT_ID TENANTS_FILE

Anche idType: "organization" è un contesto distinto, non un ID tenant. Questa guida implementa solo tenant e partner; non trattare un’organizzazione come partner.

Validare routing e header con una lettura

Token, ID tenant e host regionale sono un unico insieme. Questa verifica indipendente dall’operazione controlla solo formato e associazione e non chiama endpoint di runbook successivi:

[[ -n "$SOPHOS_ACCESS_TOKEN" ]] || exit 1
[[ "$SOPHOS_TENANT_ID" =~ $UUID_RE ]] || exit 1
[[ "$SOPHOS_API_HOST" =~ ^https://api-[a-z0-9-]+\.central\.sophos\.com$ ]] || exit 1
EMAIL_API_HOST="${SOPHOS_API_HOST%/}/email/v1"

La discovery passa solo se whoami restituisce tipo e ID attesi, l’UUID proviene dalla mappatura approvata e l’host appartiene a quel tenant. Per una lettura sicura eseguibile e il relativo schema, usare il runbook di automazione delle mailbox; la panoramica API classifica tutte le famiglie.

Ogni richiesta JSON Email richiede l’host regionale da apiHosts.dataRegion o apiHost, più /email/v1; Authorization: Bearer <access-token>; X-Tenant-ID: <tenant-uuid>; Accept: application/json; e Content-Type: application/json per JSON.

Distinguere 401, 403, 404 e 429

  • 401 Unauthorized: credenziali mancanti, non valide o bloccate, oppure JWT scaduto. Verificare credenziali e versione del secret, quindi autenticarsi una sola volta; non cambiare tenant o regione.
  • 403 Forbidden: autenticazione riuscita. Poiché l’accesso Preview è già di livello SuperAdmin, verificare associazione tenant, autorizzazione dell’operazione e disponibilità attuale dell’API; cambiare credenziali non corregge il routing.
  • 404 Not Found: controllare host regionale, /email/v1 e ID oggetto; non provare altre regioni.
  • 429 Too Many Requests: rispettare gli header di retry o rate limit e limitare i tentativi.

I limiti generali documentati sono: massimo consigliato 10 chiamate al secondo, limite applicato 100 al minuto con burst fino a 300, consigliato 1.000 all’ora e limite applicato 200.000 al giorno. A seconda del limite, il conteggio comprende credenziali API, account e IP sorgente; non cambiare credenziali o IP per aggirarlo. Possono valere anche limiti specifici dell’operazione.

Per 429 e 5xx temporanei usare Full Jitter: random_between(0, min(cap, base * (2 ** attempt))). Sophos indica base = 1000 ms e cap = 30000 ms come esempi. Limitare anche numero e durata totale, poi interrompere e segnalare. Non riprovare automaticamente 401, 403 o 404.

Non ripetere alla cieca scritture, eliminazioni, rilasci o clawback: il server può aver accettato l’operazione prima del timeout. Verificare prima stato specifico e idempotenza.

Rimuovere secret e dati di sessione

I corpi delle risposte possono contenere dati del tenant e dati personali. Registrare solo orario, metodo, percorso oscurato, stato HTTP e request ID o trackingId, mai Client Secret, token di accesso o refresh o risposte complete. Pulire le variabili al termine:

cleanup
unset SOPHOS_ACCESS_TOKEN SOPHOS_CLIENT_ID SOPHOS_ID SOPHOS_ID_TYPE
unset SOPHOS_TENANT_ID SOPHOS_API_HOST EMAIL_API_HOST WHOAMI
trap - EXIT

Per un secret perso o sospetto, creare nuove credenziali, validare il routing e quindi eseguire il test controllato del runbook pertinente, migrare l’applicazione ed eliminare quelle vecchie. L’eliminazione revoca chiamate future, ma non annulla operazioni Email già eseguite.