This document belongs to Cloud WAF — Enterprise Web Protection Management Platform
CLI tool:zcloud· 5 modules: guard / sys / analytics / cli_release / auth
Full API index: /api/openapi.json · Sitemap: /sitemap.xml · AI Quick Read: /llms.txt
Authentication
Overview
Cloud WAF supports two authentication methods. Every protected endpoint requires Authorization, and each request must pick exactly one channel:
| Scenario | Header | Description |
|---|---|---|
| Human login / Web Console / interactive CLI login | Authorization: Bearer <token> |
The token is issued by POST /api/auth/login and is valid for several hours (subject to your security policy) |
| Scripts / CI / third-party integrations | Authorization: ApiKey zck_<prefix>.<secret> |
API Key is a machine credential; plaintext is returned only once at issuance and is suitable for long-running automation |
Recommendation: use
Bearersessions for human operations, and use API Keys for machine-to-machine integrations. The legacy “username/password → short-lived token → 401 auto-relogin” pattern still works, but should be treated as a compatibility fallback.
API Key Machine Credential
An API Key is a machine credential issued for scripts, CI, and third-party systems. It represents a specific user under a specific OEM and is constrained by two boundaries:
- the issuing user's current RBAC permissions;
- the API Key's own
scopeslist.
The effective permission set is: effective_perms = user.RBAC ∩ key.scope. scope can only narrow permissions, not expand them. If scopes is empty, the key inherits the issuing user's current RBAC fully.
Issue an API Key
curl -sS -X POST https://waf.example.com/api/sys/api-keys \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"name": "prod-integration",
"scopes": ["guard.domain.list", "guard.domain.view"],
"expires_in_days": 90
}'
The data.api_key field is the full plaintext key:
{
"code": 0,
"message": "ok",
"data": {
"key_id": "8f21c0c5-55ae-4cbd-a60a-8e64a6e2b1d0",
"api_key": "zck_abc12345.A1b2C3d4E5f6G7h8I9j0K1L2M3n4O5p6Q7r8S9t0",
"prefix": "zck_abc12345",
"last4": "5t0",
"expires_at": 1732982400000
}
}
⚠ The plaintext
api_keyis returned only once. Later list responses show onlyprefixandlast4; the full secret cannot be recovered. If lost, revoke it and issue a new one.
Call APIs with an API Key
curl -sS https://waf.example.com/api/guard/domains \
-H "Authorization: ApiKey zck_abc12345.A1b2C3d4..." \
-H 'Accept-Language: en-US'
API Key vs Bearer token
| Dimension | Bearer token | API Key |
|---|---|---|
| Primary use | Short-lived session after human login | Long-running calls from machines, scripts, CI, third-party systems |
| How to obtain | POST /api/auth/login |
POST /api/sys/api-keys |
| Header | Authorization: Bearer <token> |
Authorization: ApiKey zck_<prefix>.<secret> |
| Lifetime | Usually several hours, governed by auto-logout policy | Default 90 days, max 365 days, revocable |
| Permission boundary | Current user RBAC | Current user RBAC ∩ Key scopes |
| Plaintext storage | Client stores the token | Returned only once; server never stores plaintext |
Scope matching rules (important)
API Key scopes are deliberately separated from user-role RBAC matching semantics. Read this before issuing keys:
- Exact string equality (case-sensitive): an entry in
scopesmust match the permission key required by the endpoint exactly to grant access. - No wildcard expansion: writing
guard.*orguard.domain.*intoscopesdoes not match any endpoint — the server does not perform prefix / glob / regex expansion. For example, an endpoint requiringguard.domain.listwill returncode=1053when the only scope present isguard.domain.*. - Independent from user RBAC: a user role may carry
guard.domain.*for convenience, but API Key scopes must be enumerated down to leaf permission keys. Both layers are checked serially aseffective_perms = user.RBAC ∩ key.scope; an endpoint covered by a wildcard in user RBAC is still rejected if its leaf key is absent fromscopes. - To grant a full module to an API Key, list every concrete permission key explicitly (e.g.
guard.domain.list,guard.domain.view,guard.domain.create, …). UseGET /api/sys/permissions/treeto enumerate the leaf permissions. - Failure code: scope miss returns
code=1053(HTTP 403). This is distinct fromcode=1056(apikey forbidden, OEM boundary violation); operators can disambiguate the two by error code 1:1.
Security guidance
- Create a dedicated low-privilege user for CI / third-party systems, then issue API Keys under that user.
- Grant only necessary
scopes; for read-only domain listing, useguard.domain.list/guard.domain.viewonly. - Never put the full API Key in git, logs, or tickets; show only
prefix/last4. - Periodically call
GET /api/sys/api-keysand auditlast_used_at/last_used_ip. - Revoke leaked or unused keys immediately with
DELETE /api/sys/api-keys/:id.
Login Flow (text diagram)
┌─────────────┐ ┌────────────────┐
│ Client / CLI│ ──── POST /api/auth/login ───▶ │ Cloud WAF API │
└─────────────┘ {username, password} └────────────────┘
│ │
│ ◀────── 200 OK {data: {token, user}} ───────────│
│ │
│ ──── GET /api/sys/users (Bearer token) ────────▶│
│ ◀────── 200 OK {code:0, data:[...]} ────────────│
│ │
│ ──── (token expired) GET /api/... ─────────────▶│
│ ◀────── 401 Unauthorized ───────────────────────│
│ │
│ ──── POST /api/auth/login (re-login) ──────────▶│
│ ◀────── 200 OK {data: {token: <new>}} ──────────│
Full curl Walkthrough
1) Log in
curl -sS -X POST https://waf.example.com/api/auth/login \
-H 'Content-Type: application/json' \
-d '{
"username": "admin",
"password": "YOUR_PASSWORD"
}'
Response:
{
"code": 0,
"message": "ok",
"data": {
"token": "eyJhbGciOi...",
"user": { "uuid": "abc-123", "username": "admin", "role_id": 1 }
}
}
2) Call an endpoint with the token
TOKEN='eyJhbGciOi...'
curl -sS https://waf.example.com/api/sys/users \
-H "Authorization: Bearer $TOKEN" \
-H 'Accept-Language: en-US'
3) Log out
curl -sS -X POST https://waf.example.com/api/auth/logout \
-H "Authorization: Bearer $TOKEN"
The token is invalidated immediately; subsequent use yields 401.
CLI Multi-Profile Mechanism
The CLI persists credentials in ~/.zcloud/credentials.toml:
active = "default"
[profiles.default]
api_url = "https://waf.example.com"
username = "admin"
token = "eyJhbGciOi..."
oem_id = "default"
[profiles.prod]
api_url = "https://prod.example.com"
username = "ops"
token = "eyJhbGciOi..."
Switch profiles:
zcloud config profiles list
zcloud config profiles activate prod
# Note: profiles create accepts only the positional <name>, no flags
zcloud config profiles create staging
zcloud --profile staging config set api_url https://staging.example.com
zcloud auth login updates the active profile's token; profiles are isolated.
401 Auto-Retry Pattern
Token expiration is normal. Wrap your client to "401 → relogin → retry".
Python pseudo-code
import requests
class CloudWAF:
def __init__(self, base_url, username, password):
self.base = base_url.rstrip('/')
self.username = username
self.password = password
self.token = None
self._login()
def _login(self):
r = requests.post(f"{self.base}/api/auth/login", json={
"username": self.username,
"password": self.password,
})
r.raise_for_status()
self.token = r.json()["data"]["token"]
def call(self, method, path, **kwargs):
headers = kwargs.pop("headers", {})
headers["Authorization"] = f"Bearer {self.token}"
url = f"{self.base}{path}"
r = requests.request(method, url, headers=headers, **kwargs)
if r.status_code == 401:
self._login() # relogin
headers["Authorization"] = f"Bearer {self.token}"
r = requests.request(method, url, headers=headers, **kwargs)
r.raise_for_status()
return r.json()
Go pseudo-code
type Client struct {
BaseURL string
User string
Password string
Token string
HTTP *http.Client
}
func (c *Client) login() error {
body, _ := json.Marshal(map[string]string{"username": c.User, "password": c.Password})
resp, err := c.HTTP.Post(c.BaseURL+"/api/auth/login", "application/json", bytes.NewReader(body))
if err != nil { return err }
defer resp.Body.Close()
var out struct{ Data struct{ Token string } }
json.NewDecoder(resp.Body).Decode(&out)
c.Token = out.Data.Token
return nil
}
func (c *Client) Do(req *http.Request) (*http.Response, error) {
req.Header.Set("Authorization", "Bearer "+c.Token)
resp, err := c.HTTP.Do(req)
if err != nil { return nil, err }
if resp.StatusCode == 401 {
resp.Body.Close()
if err := c.login(); err != nil { return nil, err }
req.Header.Set("Authorization", "Bearer "+c.Token)
return c.HTTP.Do(req)
}
return resp, nil
}
Machine Integration Pattern (compatibility fallback)
Machine integrations should use API Keys by default. Use the “username/password → short-lived token → 401 auto-relogin” pattern only when API Keys cannot be issued yet or when keeping an old pipeline compatible.
zcloud auth login is an interactive command (it reads the password through util.ReadPassword without echo) and does not accept --username/--password flags. CI pipelines and third-party systems should call the login REST API directly to obtain a token, then pass it to the CLI through the ZCLOUD_TOKEN environment variable.
Recommended approach:
- Evaluate API Key first: if the job only calls REST APIs, issue an API Key directly; continue below only when an old CLI flow must be reused
- Create a dedicated account: in the console, create a
ci-botuser with the minimum required permissions - Store credentials in secrets:
CLOUDWAF_USERNAME/CLOUDWAF_PASSWORDgo in GitHub Actions / GitLab CI secrets - Exchange credentials for a token at job start: call
POST /api/auth/loginwith the password base64-encoded - Subsequent CLI commands read
ZCLOUD_TOKEN— no need to write the local credentials file - Always wrap calls with the 401 retry pattern: long jobs may outlive a token; on 401, refresh by re-running the login REST call
# Gitea Actions example
jobs:
publish-cert:
steps:
- run: |
curl -fsSL ${{ vars.WAF_URL }}/api/cli/install.sh | sh
zcloud config set api_url ${{ vars.WAF_URL }}
# 1) Exchange credentials for a token via REST (password base64-encoded)
PWD_B64=$(printf '%s' "${{ secrets.WAF_PASS }}" | base64 -w0)
TOKEN=$(curl -fsS "${{ vars.WAF_URL }}/api/auth/login" \
-H 'Content-Type: application/json' \
-d "{\"username\":\"${{ secrets.WAF_USER }}\",\"password\":\"$PWD_B64\"}" \
| jq -r '.data.token')
# 2) Export ZCLOUD_TOKEN; the CLI picks it up automatically
export ZCLOUD_TOKEN="$TOKEN"
zcloud guard certs upload --name star --cert ./fullchain.pem --key ./private.key
# 3) Revoke the token when the job ends
zcloud auth logout
Security Guidance
- Always use HTTPS in production; HTTP is for local dev only
- Never commit tokens to git or write them to logs
- Store plaintext API Keys only in a secret manager; never write them to config repositories or ordinary logs
- Use a dedicated CI account with minimal permissions; never reuse an admin
- Periodically audit
GET /api/sys/sessionsandkillanomalies - Periodically audit
GET /api/sys/api-keysand revoke anomalies immediately - Do not embed tokens in frontend code (the frontend should obtain them via login)
- Do not embed API Keys in frontend code or public clients
- Do not share a profile across operators (shared profile defeats audit attribution)
Troubleshooting
| Symptom | Resolution |
|---|---|
Login returns non-zero code |
Inspect message; typical causes: account locked, wrong password, password expired |
| Repeated 401s | Verify the full token was pasted; verify the Authorization header format |
| API Key call returns 401 | Verify the header is Authorization: ApiKey zck_<prefix>.<secret> and confirm the key is not expired or revoked |
API Key call returns code=1053 (HTTP 403) |
scopes do not cover the permission required by the endpoint. Note: scope matching is exact string equality — writing guard.* matches nothing; enumerate every leaf permission key |
API Key call returns code=1056 (HTTP 403) |
OEM boundary violation: the request hostname does not belong to the OEM bound to the API Key. Retry against a hostname under the same OEM |
Login returns 200 but data.token is empty |
Server misconfiguration; contact operations |
| Multiple CLI processes evicting each other's token | Isolate them with separate profiles |
Related Documents
- API Documentation — full schema for
/api/auth/*and/api/sys/api-keys - Examples — 401 retry implementations in three languages
- Permission Matrix — basis for granting minimum permissions to CI accounts
Cloud WAF · dual-channel authentication with Bearer sessions and API Key machine credentials