◆
Arquitectura de autenticación por entorno
metrics-engine · 3 entornos · 3 credenciales
🖥 Local / Dev
DeveloperToolsCredential
az login → az login (Azure CLI)
☁ Producción (AKS)
ManagedIdentityCredential
System-assigned · sin secretos
⚙ CI/CD (GitHub Actions)
ClientSecretCredential
Service Principal · env vars
App Rust
metrics-engine
Tokio async
→
Token JWT
Crate
azure_identity
Entra ID (OAuth2)
→
Bearer auth
Key Vault
API Keys
cliente-secrets
Blob Storage
Datasets
parquet campañas
Cognitive Svc
ML Inference
modelos predicción
📦
Cargo.toml
dependencias oficiales azure-sdk
ℹ
Solo crates oficiales: publicadas por el usuario
azure-sdk en crates.io. No usar azure_sdk_* (deprecated MindFlavor). No añadir azure_core directamente — lo re-exportan las crates de servicio.
TOML
[package]
name = "metrics-engine"
version = "0.1.0"
edition = "2021"
[dependencies]
# ── Azure Identity (autenticación) ──────────────────────────────
azure_identity = "0.21" # DeveloperTools / ManagedIdentity / ClientSecret
# ── Servicios Azure (re-exportan azure_core) ─────────────────────
azure_security_keyvault_secrets = "0.21"
azure_storage_blobs = "0.21"
# ── Runtime async ────────────────────────────────────────────────
tokio = { version = "1", features = ["full"] }
# ── Config y errores ─────────────────────────────────────────────
anyhow = "1"
dotenvy = "0.15" # carga .env en local (nunca en producción)
# ── Serialización ────────────────────────────────────────────────
serde = { version = "1", features = ["derive"] }
serde_json = "1"
🔑
src/auth.rs — Factory de credenciales
detecta entorno · devuelve Arc-wrapped credential
RUST
/// auth.rs — CultivaMetrics · metrics-engine
/// Factory de credenciales Azure según entorno de ejecución.
///
/// Estrategia:
/// APP_ENV=development → DeveloperToolsCredential (az login)
/// APP_ENV=production → ManagedIdentityCredential (AKS system-assigned)
/// APP_ENV=ci → ClientSecretCredential (Service Principal)
use anyhow::{Context, Result};
use azure_identity::{
ClientSecretCredential,
DeveloperToolsCredential,
ManagedIdentityCredential,
};
use std::sync::Arc;
/// Tipo alias — agrupa todos los credenciales posibles tras un Arc
pub type AzureCredential = Arc<dyn azure_core::auth::TokenCredential>;
#[derive(Debug, Clone, PartialEq)]
pub enum AppEnv {
Development,
Production,
Ci,
}
impl AppEnv {
pub fn from_env() -> Self {
match std::env::var("APP_ENV").as_deref() {
Ok("production") => AppEnv::Production,
Ok("ci") => AppEnv::Ci,
_ => AppEnv::Development, // seguro por defecto
}
}
}
/// Construye la credencial adecuada para el entorno actual.
/// El caller recibe un Arc thread-safe: clonar es barato.
pub fn build_credential(env: &AppEnv) -> Result<AzureCredential> {
let cred: AzureCredential = match env {
AppEnv::Development => {
// Prueba az login, luego azd auth login. Sin secretos en código.
let c = DeveloperToolsCredential::new(None)
.context("DeveloperToolsCredential: ejecuta `az login` primero")?;
Arc::new(c)
}
AppEnv::Production => {
// AKS system-assigned managed identity: sin secretos, sin rotación manual.
let c = ManagedIdentityCredential::new(None)
.context("ManagedIdentityCredential: verifica que el pod tenga la MI asignada")?;
Arc::new(c)
}
AppEnv::Ci => {
// Service Principal para GitHub Actions. Vars en GitHub Secrets.
let tenant_id = std::env::var("AZURE_TENANT_ID")
.context("falta AZURE_TENANT_ID")?;
let client_id = std::env::var("AZURE_CLIENT_ID")
.context("falta AZURE_CLIENT_ID")?;
let client_secret = std::env::var("AZURE_CLIENT_SECRET")
.context("falta AZURE_CLIENT_SECRET")?;
let c = ClientSecretCredential::new(
&tenant_id, &client_id, &client_secret, None
).context("ClientSecretCredential: verifica las vars de entorno")?;
Arc::new(c)
}
};
Ok(cred)
}
▶
src/main.rs — Uso con 3 servicios Azure
Key Vault · Blob Storage · Cognitive Services
RUST
/// main.rs — CultivaMetrics · metrics-engine
/// Muestra cómo reutilizar una sola credencial para múltiples clientes.
/// Los clientes son thread-safe: crea una vez, comparte en todo Tokio.
mod auth;
use anyhow::Result;
use auth::{AppEnv, build_credential};
use azure_security_keyvault_secrets::SecretClient;
use azure_storage_blobs::prelude::*;
/// Configuración del tenant de CultivaMetrics (no son secretos)
const KEY_VAULT_URL: &str = "https://cultiva-metrics-kv.vault.azure.net/";
const STORAGE_ACCOUNT: &str = "cultivametricsdata";
const BLOB_CONTAINER: &str = "campañas-parquet";
#[tokio::main]
async fn main() -> Result<()> {
// Carga .env solo en desarrollo (dotenvy es no-op si no existe el archivo)
let _ = dotenvy::dotenv();
let env = AppEnv::from_env();
let credential = build_credential(&env)?;
println!("Entorno: {:?} → credencial configurada", env);
// ── 1. Key Vault: leer API key del cliente ────────────────────────────
// Clonar credential es O(1) — es un Arc, sin copia del token subyacente
let kv_client = SecretClient::new(
KEY_VAULT_URL,
credential.clone(), // ← clon del Arc, thread-safe
None,
)?;
let secret = kv_client
.get_secret("cliente-acme-api-key", None)
.await?
.into_model()?;
println!("API key recuperada: {} chars", secret.value.len());
// ── 2. Blob Storage: leer dataset de campañas ─────────────────────────
let blob_client = ClientBuilder::new(
STORAGE_ACCOUNT,
StorageCredentials::token_credential(credential.clone()),
)
.blob_client(BLOB_CONTAINER, "q2-2026-campaign-data.parquet");
let data = blob_client.get_content().await?;
println!("Dataset descargado: {} bytes", data.len());
// ── 3. Cognitive Services: inferencia ML (llamada REST con Bearer token)
let token = credential
.get_token(&["https://cognitiveservices.azure.com/.default"])
.await?;
println!(
"Token Cognitive Services obtenido (expira: {:?})",
token.expires_on
);
// Aquí iría la llamada HTTP a /score con Bearer token.value
// Los clientes son reusables: créalos una vez fuera de esta función.
Ok(())
}
⚙
Variables de entorno
SHELL
# .env (solo local, en .gitignore)
# Dev: solo APP_ENV, az login provee el token
APP_ENV=development
# CI/CD: GitHub Secrets → Actions env
APP_ENV=ci
AZURE_TENANT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
AZURE_CLIENT_ID=yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy
AZURE_CLIENT_SECRET=${{ secrets.AZURE_CLIENT_SECRET }}
# Producción AKS: solo APP_ENV
APP_ENV=production
# → ManagedIdentityCredential no necesita vars adicionales
▶
Comandos de desarrollo
SHELL
# 1. Login local (una vez por sesión)
az login
# o: azd auth login
# 2. Añadir dependencias
cargo add azure_identity tokio
# 3. Compilar
cargo build
# 4. Ejecutar en local
APP_ENV=development cargo run
# 5. Verificar RBAC del Service Principal
az role assignment list --assignee <client-id> -o table
# 6. Roles mínimos necesarios:
# Key Vault Secrets User → leer secretos
# Storage Blob Data Reader → leer blobs
📋
Referencia de credenciales azure_identity
crate oficial azure-sdk · v0.21
| Tipo de Credencial | Caso de uso | Entorno |
|---|---|---|
| DeveloperToolsCredential | Desarrollo local — prueba az login, luego azd auth login | Local |
| ManagedIdentityCredential | VMs, App Service, Functions, AKS — sin secretos | Azure |
| WorkloadIdentityCredential | Kubernetes workload identity federation | K8s |
| ClientSecretCredential | Service principal con secreto — CI/CD pipelines | CI/CD |
| ClientCertificateCredential | Service principal con certificado — más seguro que secreto | CI/Prod |
| AzureCliCredential | Autenticación directa con Azure CLI | Local |
| AzureDeveloperCliCredential | Autenticación directa con azd CLI | Local |
| AzurePipelinesCredential | Azure Pipelines service connection | CI/CD |
| ClientAssertionCredential | Federated identity — assertions custom (OIDC) | Ambos |
🔒
Buenas prácticas de seguridad
checklist CultivaMetrics · metrics-engine
-
Sin DefaultAzureCredential — usa el tipo correcto por entorno El SDK Rust no incluye DefaultAzureCredential. Elige explícitamente DeveloperToolsCredential (dev) o ManagedIdentityCredential (prod).
-
Cero credenciales hardcodeadas Todos los secretos vienen de variables de entorno o de managed identity. El código no contiene ningún valor sensible.
-
Clona credential.clone() al construir múltiples clientes Las credenciales son Arc-wrapped e internamente thread-safe. Clonar es O(1), sin copias del token JWT.
-
RBAC mínimo necesario (Principle of Least Privilege) "Key Vault Secrets User" para leer secretos, "Storage Blob Data Reader" para blobs. Nunca "Owner" ni "Contributor".
-
Clientes Tokio creados una vez, reutilizados SecretClient, ClientBuilder, etc. son thread-safe. Crear una instancia por servicio al arrancar el proceso, no en cada request.
-
Solo crates oficiales (azure-sdk user en crates.io) No usar azure_sdk_* (MindFlavor/deprecated). Verificar versión en crates.io antes de añadir dependencias.