Skip to content

Commit 4227e09

Browse files
feat(auth): add non-interactive login helper and docs
1 parent e41a787 commit 4227e09

File tree

4 files changed

+179
-0
lines changed

4 files changed

+179
-0
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,10 @@ If you would like to get builds of arbitrary commit or PR, see:
203203

204204
[Try new features before release](doc/try_new_features_before_release.md)
205205

206+
### Non-interactive authentication for automation
207+
208+
We've added a short guide and helper script for non-interactive authentication (service principal, managed identity, workload identity) used by CI/CD and headless servers. See `doc/non_interactive_auth.md` and `scripts/non_interactive_login.py` for examples and secure patterns.
209+
206210
## Developer setup
207211

208212
If you would like to setup a development environment and contribute to the CLI, see:

doc/non_interactive_auth.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Non-interactive authentication for Azure CLI
2+
3+
This document explains recommended non-interactive authentication modes for automation (CI/CD, scripts, headless servers) and provides safe examples and a small helper script.
4+
5+
Supported modes
6+
7+
- Service Principal (client secret) — recommended for CI systems where a secret can be stored securely.
8+
- Managed Identity (MSI) — recommended when running inside Azure (VM, VMSS, App Service, Function) and you can assign a managed identity.
9+
- Device Code — useful for ad-hoc non-browser sign-ins when interactive approval is acceptable.
10+
- Workload Identity — recommended for Kubernetes-based workloads using federated credentials.
11+
12+
Environment variables
13+
14+
The helper scripts and CI examples below use the following environment variables (common conventions):
15+
16+
- AZURE_AUTH_MODE: one of `service-principal`, `managed-identity`, `device-code`, `workload-identity` (optional; script will try to auto-detect)
17+
- AZURE_CLIENT_ID
18+
- AZURE_CLIENT_SECRET
19+
- AZURE_TENANT_ID
20+
- AZURE_FEDERATED_TOKEN_FILE (for workload identity / token file)
21+
22+
Security guidance
23+
24+
- Store secrets in your CI secret store (GitHub Secrets, Azure DevOps variable groups, etc.).
25+
- Prefer managed identities or workload identity federation where possible to avoid long-lived secrets.
26+
- Limit permissions using least-privilege service principals.
27+
28+
Examples
29+
30+
Service principal (GitHub Actions)
31+
32+
```yaml
33+
# GitHub Actions snippet
34+
env:
35+
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
36+
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
37+
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
38+
39+
steps:
40+
- name: Install Azure CLI
41+
uses: azure/CLI@v1
42+
43+
- name: Login with service principal
44+
run: az login --service-principal -u "$AZURE_CLIENT_ID" -p "$AZURE_CLIENT_SECRET" --tenant "$AZURE_TENANT_ID"
45+
```
46+
47+
Managed Identity (Azure VM / App Service)
48+
49+
On an Azure VM with a system-assigned or user-assigned managed identity you can run:
50+
51+
```bash
52+
az login --identity
53+
```
54+
55+
Device code (interactive)
56+
57+
```bash
58+
az login --use-device-code
59+
```
60+
61+
Workload identity (Kubernetes with federated credentials)
62+
63+
Use Azure AD Workload Identity or federated credential to obtain a token and then use `az account get-access-token` or configure the environment according to your runtime. For CI that provides a token file, a short helper can run `az login --federated-token` style workflows; see the example helper script in `scripts/non_interactive_login.py`.
64+
65+
Helper script
66+
67+
A small helper script `scripts/non_interactive_login.py` is included that demonstrates safe detection of environment variables and returns the recommended `az login` command for common automation scenarios. The script is intentionally conservative and does not run `az` by default in unit tests.
68+
69+
Further reading
70+
71+
- https://docs.microsoft.com/azure/active-directory/develop/howto-create-service-principal-portal
72+
- https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/overview
73+
- https://learn.microsoft.com/azure/aks/workload-identity-overview

scripts/non_interactive_login.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Helper for non-interactive Azure CLI login in CI and automation.
4+
This script prints the recommended `az login` command for the detected environment.
5+
It does not execute commands by default to keep unit testing simple and safe.
6+
"""
7+
import os
8+
import sys
9+
import argparse
10+
11+
12+
def detect_mode(env=os.environ):
13+
if env.get("AZURE_CLIENT_ID") and env.get("AZURE_CLIENT_SECRET") and env.get("AZURE_TENANT_ID"):
14+
return "service-principal"
15+
if env.get("AZURE_FEDERATED_TOKEN_FILE"):
16+
return "workload-identity"
17+
if env.get("AZURE_CLIENT_ID") and env.get("AZURE_FEDERATED_TOKEN_FILE"):
18+
return "workload-identity"
19+
if env.get("AZURE_USE_MANAGED_IDENTITY") == "true" or env.get("MSI_ENDPOINT"):
20+
return "managed-identity"
21+
return "unknown"
22+
23+
24+
def build_command(mode, env=os.environ):
25+
if mode == "service-principal":
26+
return (
27+
f"az login --service-principal -u \"{env.get('AZURE_CLIENT_ID')}\" -p \"{env.get('AZURE_CLIENT_SECRET')}\" --tenant \"{env.get('AZURE_TENANT_ID')}\""
28+
)
29+
if mode == "workload-identity":
30+
# Assumes federated token file path in AZURE_FEDERATED_TOKEN_FILE
31+
token_file = env.get("AZURE_FEDERATED_TOKEN_FILE")
32+
if token_file:
33+
return f"az login --federated-token @\"{token_file}\" --allow-no-subscriptions"
34+
return "# workload-identity detected but AZURE_FEDERATED_TOKEN_FILE not set"
35+
if mode == "managed-identity":
36+
return "az login --identity"
37+
return "# Unable to detect non-interactive auth mode. Use 'az login --help'"
38+
39+
40+
def main():
41+
parser = argparse.ArgumentParser(description="Detect environment and print recommended az login command.")
42+
parser.add_argument("--run", action="store_true", help="Execute the suggested az login command")
43+
args = parser.parse_args()
44+
45+
mode = detect_mode()
46+
cmd = build_command(mode)
47+
print(f"Detected mode: {mode}")
48+
print(cmd)
49+
50+
if args.run:
51+
print("Execution requested. Running the command...")
52+
rc = os.system(cmd)
53+
sys.exit(rc)
54+
55+
56+
if __name__ == "__main__":
57+
main()
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import os
2+
import tempfile
3+
import pytest
4+
from scripts import non_interactive_login as nil
5+
6+
7+
def test_detect_service_principal(monkeypatch):
8+
monkeypatch.setenv("AZURE_CLIENT_ID", "cid")
9+
monkeypatch.setenv("AZURE_CLIENT_SECRET", "secret")
10+
monkeypatch.setenv("AZURE_TENANT_ID", "tid")
11+
assert nil.detect_mode() == "service-principal"
12+
13+
14+
def test_detect_workload_identity(monkeypatch, tmp_path):
15+
token_file = tmp_path / "token.txt"
16+
token_file.write_text("token")
17+
monkeypatch.setenv("AZURE_FEDERATED_TOKEN_FILE", str(token_file))
18+
assert nil.detect_mode() == "workload-identity"
19+
20+
21+
def test_detect_managed_identity(monkeypatch):
22+
monkeypatch.setenv("AZURE_USE_MANAGED_IDENTITY", "true")
23+
assert nil.detect_mode() == "managed-identity"
24+
25+
26+
def test_build_command_service_principal(monkeypatch):
27+
monkeypatch.setenv("AZURE_CLIENT_ID", "cid")
28+
monkeypatch.setenv("AZURE_CLIENT_SECRET", "secret")
29+
monkeypatch.setenv("AZURE_TENANT_ID", "tid")
30+
cmd = nil.build_command("service-principal")
31+
assert "az login --service-principal" in cmd
32+
assert "cid" in cmd
33+
34+
35+
def test_build_command_workload_identity(monkeypatch, tmp_path):
36+
token_file = tmp_path / "token.txt"
37+
token_file.write_text("token")
38+
monkeypatch.setenv("AZURE_FEDERATED_TOKEN_FILE", str(token_file))
39+
cmd = nil.build_command("workload-identity")
40+
assert "--federated-token" in cmd
41+
42+
43+
def test_build_command_managed_identity():
44+
cmd = nil.build_command("managed-identity")
45+
assert cmd == "az login --identity"

0 commit comments

Comments
 (0)