Autenticar la API de Sophos Email y enrutarla al tenant correcto
Cada llamada a la API de gestión de Sophos Email comienza con tres pasos separados: un service principal obtiene un token OAuth2 de corta duración, whoami determina el tipo y el ID del llamante y, solo entonces, el tenant de destino se vincula con su host regional. Un token válido no selecciona por sí solo ni el tenant ni la región.
El resultado seguro es un conjunto inseparable: SOPHOS_ACCESS_TOKEN, SOPHOS_TENANT_ID y SOPHOS_API_HOST. El host global solo sirve para descubrir identidad y tenants. Las operaciones de Email se envían a ${SOPHOS_API_HOST}/email/v1 con Authorization y X-Tenant-ID.
Crear un service principal para el acceso API Preview
Para un tenant directo, entre como Super Admin en Sophos Fusion Admin (antes Sophos Central Admin) y abra Global Settings > API Credentials. En Sophos Fusion Partner, use Settings & Policies > API Credentials. Mientras las API estén en Preview, el acceso se realiza con nivel SuperAdmin. Limite esta credencial al propósito de automatización controlado y no presuponga que puede seleccionarse un rol de service principal más restringido.
- Cree una credencial distinta por aplicación y entorno.
- Identifique propietario, finalidad y tenant de destino en el nombre y la descripción.
- Documente como riesgo el estado Preview y el acceso SuperAdmin, y limite el uso al tenant aprobado.
- Guarde inmediatamente Client ID y Client Secret en un gestor corporativo de secretos.
- Supervise fuera de Sophos Fusion la caducidad, la rotación y el contacto de emergencia.
Solicitar el access token sin dejar rastros
El ejemplo Bash requiere curl y jq. Lee el secreto sin mostrarlo, codifica los valores del formulario y los envía por la entrada estándar. Así el secreto no aparece en el historial de la shell ni como argumento de proceso expandido:
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
Son fijos grant_type=client_credentials, scope=token, la ruta del token y Content-Type: application/x-www-form-urlencoded. client_id y client_secret proceden de la credencial. Una respuesta correcta contiene access_token, token_type: "bearer" y expires_in. Al caducar, solicite otro token con el mismo flujo y no registre la respuesta completa.
Determinar el llamante con Who-am-I
whoami es una llamada global de descubrimiento; el bearer header vuelve a pasar por la entrada estándar de curl. El token se inserta en ese flujo de configuración, que el ejemplo no muestra:
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 es el UUID del llamante; idType distingue tenant, partner y organization; apiHosts.global es el host global; y apiHosts.dataRegion es el host regional de un tenant directo.
Con idType: "tenant", id también es el ID de tenant requerido. Valide ambos valores antes de usarlos:
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")
La ausencia de dataRegion, un idType inesperado o un host fuera del formato validado obligan a detenerse. No deduzca el host de un ejemplo, código de región o tabla estática.
Resolver el tenant de destino como partner
Con idType: "partner", el id de whoami es un ID de partner y nunca debe usarse como X-Tenant-ID. Enumere los tenants mediante el endpoint global de partner y X-Partner-ID; cada objeto devuelve sus propios id, dataRegion y apiHost.
La primera página solicita pageTotal=true y después se leen las páginas numeradas hasta pages.total, con un límite local:
[[ "$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
Seleccione el destino mediante un UUID aprobado, nunca solo por name. dataRegion es un identificador; la solicitud utiliza el apiHost completo correspondiente:
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
idType: "organization" también es un contexto independiente, no un ID de tenant. Esta guía implementa solo los flujos tenant y partner; no trate una organización como partner.
Validar los valores de routing antes de la primera operación Email
Mantenga token, ID de tenant y host regional como un único conjunto. Esta comprobación neutral respecto a la operación valida solo el formato y la vinculación; no llama a endpoints de runbooks posteriores:
[[ -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 detección solo es válida si whoami devuelve el tipo y el ID esperados, el UUID procede de la asignación aprobada y el host pertenece a ese tenant. Para una lectura segura ejecutable y su esquema, siga el runbook de automatización de buzones; la introducción a la API clasifica todas las familias.
Cada llamada JSON de Email requiere el host regional de apiHosts.dataRegion o apiHost, más /email/v1; Authorization: Bearer <access-token>; X-Tenant-ID: <tenant-uuid>; Accept: application/json; y Content-Type: application/json para JSON.
Distinguir 401, 403, 404 y 429
401 Unauthorized: credencial ausente, inválida o bloqueada, o JWT caducado. Compruebe credencial y versión del secreto y autentique una sola vez más; no cambie tenant o región.403 Forbidden: la autenticación funcionó. Como el acceso Preview ya es de nivel SuperAdmin, compruebe la asignación del tenant, el permiso de operación y la disponibilidad actual de la API; cambiar la credencial no corrige el routing.404 Not Found: compruebe host regional, ruta/email/v1e ID del objeto; no pruebe otras regiones.429 Too Many Requests: respete los headers de reintento o límite y reintente de forma acotada.
Los límites generales documentados son: máximo recomendado de 10 llamadas por segundo, 100 por minuto obligatorias con ráfagas de hasta 300, 1.000 por hora recomendadas y 200.000 por día obligatorias. Según el límite, el recuento abarca la credencial API, la cuenta y la IP de origen; no cambie credenciales o IP para eludirlo. Pueden añadirse límites propios de cada operación.
Para 429 y 5xx transitorios use Full Jitter: random_between(0, min(cap, base * (2 ** attempt))). Sophos propone base = 1000 ms y cap = 30000 ms. Limite también intentos y duración total; después detenga y alerte. No reintente automáticamente 401, 403 ni 404.
No repita a ciegas escrituras, borrados, liberaciones o clawback: el servidor puede haber aceptado la operación antes del timeout. Consulte antes el estado específico y la idempotencia.
Eliminar secretos y datos de sesión
Los cuerpos de respuesta pueden contener datos del tenant y datos personales. Registre solo fecha, método, ruta censurada, estado HTTP y cualquier request ID o trackingId; nunca Client Secret, tokens de acceso o refresh ni respuestas completas. Limpie las variables al terminar:
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
Ante un secreto perdido o sospechoso, cree otra credencial, valide el routing y ejecute después la prueba controlada del runbook correspondiente, migre la aplicación y elimine la anterior. El borrado revoca llamadas futuras, pero no deshace operaciones ya ejecutadas.