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.
- Creare credenziali separate per applicazione e ambiente.
- Indicare proprietario, scopo e tenant nel nome e nella descrizione.
- Documentare come rischio lo stato Preview e l’accesso SuperAdmin e limitare l’uso al tenant approvato.
- Salvare subito Client ID e Client Secret in un secret store aziendale.
- 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/v1e 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.