使用带认证和 RBAC 的 MLflow Python SDK
在 Alauda AI 中,MLflow Tracking Server 运行在单点登录和多租户之后:OAuth proxy 会对每个调用方进行身份验证,server 会按调用用户记录每次 run,并基于 Kubernetes RBAC 对其进行授权。本指南将标准 MLflow Python SDK 通过该 OAuth proxy 以你自己的身份进行访问,且无需浏览器,使用针对平台登录脚本化的 OAuth2 authorization code 流程(带 PKCE)——不使用 password grant,也绝不直接访问 MLflow container 端口。
有两种无需浏览器的方式来呈现你的身份;请选择其一:
- Bearer token(推荐)。从 CLI 或 Python 获取 Dex id token,并将其作为
MLFLOW_TRACKING_TOKEN 传入;使用 refresh token 进行续期。operator 默认启用此方式(请在下方验证设置)。
- Session cookie(无需平台更改)。驱动 proxy 自身的登录以获取其
_oauth2_proxy cookie,并将其附加到请求中。可直接在任何安装环境中使用,无需改动(见下文)。
认证如何工作
在你的 runs 前面有两层:
- OAuth proxy(
oauth2-proxy)对请求进行身份验证——可以是作为 Authorization: Bearer … 发送的 Dex id token(token 方式),也可以是其 _oauth2_proxy session cookie(cookie 方式)。
- MLflow server 的
kubernetes-auth plugin 会从该凭证中读取你的身份,将其记录为 run 的 owner,并基于 workspace 中的 Kubernetes 权限对其进行授权。
client 始终通过 OAuth proxy 访问——切勿直接连接到 MLflow container 端口。
前提条件
mlflow 3.13.0(pip install "mlflow==3.13.0"),这是本次发布验证过的 client 版本。workspace 选择(mlflow.set_workspace)在此版本中可用。Python token helper 还会使用 requests 和 cryptography。
- 可访问目标 workspace 的平台 username 和 password(参见 MLflow Workspaces and Access Control)。这是一种普通的平台登录——不是 Kubernetes ServiceAccount——普通用户账号即可。对于共享或自动化使用场景(pipeline、headless job),建议使用专用的、非个人的账号,而不是某个个人登录,因为凭证/token 会存储在
Secret 中,并且每次 run 都会记录为你所认证的身份。
- 平台的 OAuth client id 和 secret——MLflow proxy 使用的 client(由管理员提供)。在 Alauda 中,这是平台 auth client,例如
alauda-auth;其 secret 存储在 Kubernetes Secret 中(例如 cpaas-oidc-secret)。
从 MLflow Operator v3.13.0 开始,workload-cluster OAuth proxy 默认接受 Dex id token。请确认 MLflow custom resource 保持启用该设置:
apiVersion: mlflow.alauda.io/v1alpha1
kind: MLflow
spec:
auth:
oauth:
skipJwtBearerTokens: true
这是 workload cluster 上的 MLflow proxy,不是 平台的全局 auth server。不需要对 Dex 或 global-auth 进行任何更改:下面的登录使用的是平台 client 已经允许的 authorization_code grant。如果你是从较旧版本升级,或者替换了默认 OAuth 配置,请恢复 skipJwtBearerTokens: true。cookie 方式不需要任何设置。
从命令行获取 token(无需浏览器)
平台登录是一个 SSO 页面,但其 API 支持带 PKCE 的标准 OAuth authorization code 流程,因此你可以通过脚本完成登录——无需浏览器重定向。密码会使用 login service 的公钥(/dex/pubkey)进行 RSA 加密,和登录页面的处理方式完全一致,然后交换为 id token(以及用于 headless 续期的 refresh token)。
Python helper
import base64, hashlib, json, os, secrets
from urllib.parse import urlparse, parse_qs
import requests
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.serialization import load_pem_public_key
PLATFORM = os.environ["PLATFORM_ADDRESS"].rstrip("/") # https://<platform>
CLIENT_ID = os.environ["DEX_CLIENT_ID"] # the MLflow proxy's client, e.g. alauda-auth
CLIENT_SECRET = os.environ["DEX_CLIENT_SECRET"]
USERNAME = os.environ["MLFLOW_USERNAME"]
PASSWORD = os.environ["MLFLOW_PASSWORD"]
REDIRECT_URI = f"{PLATFORM}/oauth2/callback" # any URI the client has registered
VERIFY_TLS = os.environ.get("PLATFORM_CA", False) # CA bundle path, or False to skip (lab only)
s = requests.Session(); s.verify = VERIFY_TLS
_b64url = lambda b: base64.urlsafe_b64encode(b).rstrip(b"=").decode()
def get_tokens() -> dict:
"""Run the authorization-code + PKCE flow headlessly. Returns the Dex token response."""
verifier = _b64url(secrets.token_bytes(48))
challenge = _b64url(hashlib.sha256(verifier.encode()).digest())
# 1) start the flow -> auth-request id
req = s.get(f"{PLATFORM}/dex/api/v1/authorize", params={
"client_id": CLIENT_ID, "redirect_uri": REDIRECT_URI, "response_type": "code",
"scope": "openid email groups offline_access", "state": "cli",
"code_challenge": challenge, "code_challenge_method": "S256"}).json()["req"]
# 2) RSA-encrypt the password, then log in via the local connector -> auth code
pk = s.get(f"{PLATFORM}/dex/pubkey").json() # {"ts": ..., "pubkey": "<PEM>"}
payload = json.dumps({"ts": pk["ts"], "password": PASSWORD}, separators=(",", ":")).encode()
enc = base64.b64encode(load_pem_public_key(pk["pubkey"].encode()).encrypt(payload, padding.PKCS1v15())).decode()
redirect = s.post(f"{PLATFORM}/dex/api/v1/authorize/local", params={"req": req},
json={"account": USERNAME, "password": enc}).json()["redirect_url"]
code = parse_qs(urlparse(redirect).query)["code"][0]
# 3) exchange the code (with the PKCE verifier) -> id_token + refresh_token
return s.post(f"{PLATFORM}/dex/token", data={
"grant_type": "authorization_code", "code": code, "redirect_uri": REDIRECT_URI,
"code_verifier": verifier, "client_id": CLIENT_ID, "client_secret": CLIENT_SECRET}).json()
def refresh(refresh_token: str) -> str:
"""Mint a fresh id token from a refresh token — no login, no browser."""
return s.post(f"{PLATFORM}/dex/token", data={
"grant_type": "refresh_token", "refresh_token": refresh_token,
"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET,
"scope": "openid email groups"}).json()["id_token"]
Shell 等价实现(curl + openssl,无 Python 依赖)
PLATFORM=https://<platform>; CLIENT_ID=<client>; CLIENT_SECRET=<secret>
USERNAME='<user>'; PASSWORD='<password>'; REDIRECT_URI="$PLATFORM/oauth2/callback"
V=$(openssl rand -base64 48 | tr '+/' '-_' | tr -d '=' | cut -c1-64) # PKCE verifier
C=$(printf %s "$V" | openssl dgst -sha256 -binary | openssl base64 -A | tr '+/' '-_' | tr -d '=')
RU=$(jq -rn --arg u "$REDIRECT_URI" '$u|@uri'); SC=$(jq -rn '"openid email groups offline_access"|@uri')
REQ=$(curl -sk "$PLATFORM/dex/api/v1/authorize?client_id=$CLIENT_ID&redirect_uri=$RU&response_type=code&scope=$SC&state=cli&code_challenge=$C&code_challenge_method=S256" | jq -r .req)
PK=$(curl -sk "$PLATFORM/dex/pubkey"); TS=$(echo "$PK"|jq -r .ts); echo "$PK"|jq -r .pubkey >/tmp/dex_pub.pem
ENC=$(printf '{"ts":"%s","password":"%s"}' "$TS" "$PASSWORD" | openssl pkeyutl -encrypt -pubin -inkey /tmp/dex_pub.pem -pkeyopt rsa_padding_mode:pkcs1 | openssl base64 -A)
CODE=$(curl -sk -X POST "$PLATFORM/dex/api/v1/authorize/local?req=$REQ" -H 'Content-Type: application/json' \
--data "$(jq -nc --arg a "$USERNAME" --arg p "$ENC" '{account:$a,password:$p}')" | jq -r .redirect_url | sed -E 's/.*code=([^&]+).*/\1/')
curl -sk "$PLATFORM/dex/token" -d grant_type=authorization_code -d code="$CODE" \
--data-urlencode redirect_uri="$REDIRECT_URI" -d code_verifier="$V" \
-d client_id="$CLIENT_ID" --data-urlencode client_secret="$CLIENT_SECRET" | jq -r .id_token
连接 SDK
import os, mlflow
tok = get_tokens()
os.environ["MLFLOW_TRACKING_TOKEN"] = tok["id_token"].strip() # → Authorization: Bearer
mlflow.set_tracking_uri("http://mlflow-tracking-server.kubeflow:5000") # in-cluster Service (fronted by the OAuth proxy)
mlflow.set_workspace("team-a") # workspace namespace → X-MLFLOW-WORKSPACE
mlflow.set_experiment("my-experiment")
with mlflow.start_run(run_name="sdk-quickstart") as run:
mlflow.log_param("learning_rate", 2e-4)
mlflow.log_metric("loss", 0.123)
print("run:", run.info.run_id)
该 run 会出现在 Alauda AI → 工具 → MLFlow 下,归属于你认证时使用的用户。(在受保护的安装环境中已端到端验证:run owner 是 token 的用户身份。)
当 client 在集群内部运行时(pipeline component、Workbench notebook),请使用集群内 Service URL http://mlflow-tracking-server.kubeflow:5000。在集群外部,则改为指向平台路由 https://<platform>/clusters/<cluster>/mlflow——两者都会到达同一个 OAuth proxy(如果平台证书不被你的机器信任,请设置 MLFLOW_TRACKING_INSECURE_TLS=true)。
WARNING
对于共享或 headless 使用场景,建议使用专用的、非个人的账号(普通的平台登录,而不是 Kubernetes ServiceAccount),并将其凭证和 client secret 保存在 Kubernetes Secret 中,切勿写入代码。务必对 token 调用 .strip()(末尾换行会导致 Invalid … character(s) in header value: 'Bearer …\n')。id token 会过期(默认 24 小时);对于长时间运行的 job,请使用 refresh(tok["refresh_token"]) 续期,而不要再次登录。
选择 workspace
run 会记录在你所选择的 workspace 中;如果未选择,则使用 server 的默认 workspace。以下任一方式都可以设置它(SDK 会将其转换为 X-MLFLOW-WORKSPACE header):
- 在代码中调用
mlflow.set_workspace("team-a"),
- 或设置环境变量
MLFLOW_WORKSPACE=team-a。
你只能使用你账号有权限访问的 workspace;参见 MLflow Workspaces and Access Control。
注册模型
model registry 以 workspace 为作用域,并以相同方式进行授权,因此一旦连接成功,常规 SDK 调用即可正常工作:
mlflow.set_workspace("team-a")
with mlflow.start_run():
mlflow.sklearn.log_model(sk_model, name="model", registered_model_name="fraud-detector")
然后可在 MLflow UI 中将已注册版本提升为 Staging 或 Production。
替代方案:session cookie(无需平台更改)
如果你无法启用 spec.auth.oauth.skipJwtBearerTokens,可以驱动 proxy 自身的登录流程来获取其 _oauth2_proxy cookie,并将其附加到请求中——这在任何安装环境中都可直接使用,无需更改。proxy 会为你启动 OAuth 流程(它自己的 PKCE 和 redirect_uri);你只需通过同一个脚本化登录重放该流程,并将 code 交回给 proxy callback:
PLATFORM=https://<platform>; CLUSTER=<cluster>
USERNAME='<user>'; PASSWORD='<password>'
JAR=$(mktemp)
# 1) start the MLflow proxy login -> the Dex auth query it wants
LOC=$(curl -sk -c "$JAR" -D - -o /dev/null "$PLATFORM/clusters/$CLUSTER/mlflow/" \
| awk 'BEGIN{IGNORECASE=1}/^location:/{print $2}' | tr -d '\r')
QS=${LOC#*\?}
# 2) authorize -> req, then 3) scripted local login -> the proxy callback URL
REQ=$(curl -sk -b "$JAR" -c "$JAR" "$PLATFORM/dex/api/v1/authorize?$QS" | jq -r .req)
PK=$(curl -sk "$PLATFORM/dex/pubkey"); TS=$(echo "$PK"|jq -r .ts); echo "$PK"|jq -r .pubkey >/tmp/dex_pub.pem
ENC=$(printf '{"ts":"%s","password":"%s"}' "$TS" "$PASSWORD" | openssl pkeyutl -encrypt -pubin -inkey /tmp/dex_pub.pem -pkeyopt rsa_padding_mode:pkcs1 | openssl base64 -A)
CB=$(curl -sk -b "$JAR" -c "$JAR" -X POST "$PLATFORM/dex/api/v1/authorize/local?req=$REQ" -H 'Content-Type: application/json' \
--data "$(jq -nc --arg a "$USERNAME" --arg p "$ENC" '{account:$a,password:$p}')" | jq -r .redirect_url)
# 4) the proxy callback exchanges the code and sets the _oauth2_proxy cookie
curl -sk -b "$JAR" -c "$JAR" -o /dev/null "$CB"
COOKIE=$(awk -F'\t' '$6 ~ /^_oauth2_proxy/{printf "%s=%s; ",$6,$7}' "$JAR" | sed 's/; $//') # includes any _oauth2_proxy_N chunks
echo "$COOKIE"
然后使用 header provider 附加 cookie(该 cookie 携带你的身份——没有 token,也不需要平台设置):
import os, mlflow
from mlflow.tracking.request_header.abstract_request_header_provider import RequestHeaderProvider
from mlflow.tracking.request_header.registry import _request_header_provider_registry
class ProxySessionHeader(RequestHeaderProvider):
def in_context(self):
return bool(os.environ.get("MLFLOW_PROXY_COOKIE")) # export MLFLOW_PROXY_COOKIE='_oauth2_proxy=<value>'
def request_headers(self):
return {"Cookie": os.environ["MLFLOW_PROXY_COOKIE"]}
_request_header_provider_registry.register(ProxySessionHeader)
mlflow.set_tracking_uri("https://<platform>/clusters/<cluster>/mlflow")
mlflow.set_workspace("team-a")
在集群内部,请改为指向集群内 Service URL http://mlflow-tracking-server.kubeflow:5000(不会有 TLS 问题)。在外部的 https://<platform>/… 路由上,平台证书是自签名的,因此请设置 MLFLOW_TRACKING_INSECURE_TLS=true(或者将 REQUESTS_CA_BUNDLE 指向平台 CA)。
你也可以从浏览器会话中复制 _oauth2_proxy cookie(DevTools → Application/Storage → Cookies)。session cookie 会过期——当调用开始返回登录重定向时,请重新生成。
故障排查