// Command mciasctl is the MCIAS admin CLI. // // It connects to a running mciassrv instance and provides subcommands for // managing accounts, roles, tokens, Postgres credentials, policy rules, and // account tags. // // Usage: // // mciasctl [global flags] [args] // // Global flags: // // -server URL of the mciassrv instance (default: https://localhost:8443) // -token Bearer token for authentication (or set MCIAS_TOKEN env var) // -cacert Path to CA certificate for TLS verification (optional) // // Commands: // // auth login -username NAME [-totp CODE] // auth change-password (passwords always prompted interactively) // // account list // account create -username NAME [-type human|system] // account get -id UUID // account update -id UUID [-status active|inactive] // account delete -id UUID // account set-password -id UUID // // role list -id UUID // role set -id UUID -roles role1,role2,... // // token issue -id UUID // token revoke -jti JTI // // pgcreds set -id UUID -host HOST [-port PORT] -db DB -user USER [-password PASS] // pgcreds get -id UUID // // policy list // policy create -description STR -json FILE [-priority N] [-not-before RFC3339] [-expires-at RFC3339] // policy get -id ID // policy update -id ID [-priority N] [-enabled true|false] [-not-before RFC3339] [-expires-at RFC3339] [-clear-not-before] [-clear-expires-at] // policy delete -id ID // // tag list -id UUID // tag set -id UUID -tags tag1,tag2,... package main import ( "crypto/tls" "crypto/x509" "encoding/json" "flag" "fmt" "net/http" "os" "strings" "time" "golang.org/x/term" ) func main() { // Global flags. serverURL := flag.String("server", "https://localhost:8443", "mciassrv base URL") tokenFlag := flag.String("token", "", "bearer token (or set MCIAS_TOKEN)") caCert := flag.String("cacert", "", "path to CA certificate for TLS") flag.Usage = usage flag.Parse() // Resolve token from flag or environment. bearerToken := *tokenFlag if bearerToken == "" { bearerToken = os.Getenv("MCIAS_TOKEN") } args := flag.Args() if len(args) == 0 { usage() os.Exit(1) } // Build HTTP client. client, err := newHTTPClient(*caCert) if err != nil { fatalf("build HTTP client: %v", err) } ctl := &controller{ serverURL: strings.TrimRight(*serverURL, "/"), token: bearerToken, client: client, } command := args[0] subArgs := args[1:] switch command { case "auth": ctl.runAuth(subArgs) case "account": ctl.runAccount(subArgs) case "role": ctl.runRole(subArgs) case "token": ctl.runToken(subArgs) case "pgcreds": ctl.runPGCreds(subArgs) case "policy": ctl.runPolicy(subArgs) case "tag": ctl.runTag(subArgs) default: fatalf("unknown command %q; run with no args to see usage", command) } } // controller holds shared state for all subcommands. type controller struct { client *http.Client serverURL string token string } // ---- auth subcommands ---- func (c *controller) runAuth(args []string) { if len(args) == 0 { fatalf("auth requires a subcommand: login, change-password") } switch args[0] { case "login": c.authLogin(args[1:]) case "change-password": c.authChangePassword(args[1:]) default: fatalf("unknown auth subcommand %q", args[0]) } } // authLogin authenticates with the server using username and password, then // prints the resulting bearer token to stdout. The password is always prompted // interactively; it is never accepted as a command-line flag to prevent it from // appearing in shell history, ps output, and process argument lists. // // Security: terminal echo is disabled during password entry // (golang.org/x/term.ReadPassword); the raw byte slice is zeroed after use. func (c *controller) authLogin(args []string) { fs := flag.NewFlagSet("auth login", flag.ExitOnError) username := fs.String("username", "", "username (required)") totpCode := fs.String("totp", "", "TOTP code (required if TOTP is enrolled)") _ = fs.Parse(args) if *username == "" { fatalf("auth login: -username is required") } // Security: always prompt interactively; never accept password as a flag. // This prevents the credential from appearing in shell history, ps output, // and /proc/PID/cmdline. fmt.Fprint(os.Stderr, "Password: ") raw, err := term.ReadPassword(int(os.Stdin.Fd())) //nolint:gosec // uintptr==int on all target platforms fmt.Fprintln(os.Stderr) // newline after hidden input if err != nil { fatalf("read password: %v", err) } passwd := string(raw) // Zero the raw byte slice once copied into the string. for i := range raw { raw[i] = 0 } body := map[string]string{ "username": *username, "password": passwd, } if *totpCode != "" { body["totp_code"] = *totpCode } var result struct { Token string `json:"token"` ExpiresAt string `json:"expires_at"` } c.doRequest("POST", "/v1/auth/login", body, &result) // Print token to stdout so it can be captured by scripts, e.g.: // export MCIAS_TOKEN=$(mciasctl auth login -username alice) fmt.Println(result.Token) if result.ExpiresAt != "" { fmt.Fprintf(os.Stderr, "expires: %s\n", result.ExpiresAt) } } // authChangePassword allows an authenticated user to change their own password. // A valid bearer token must be set (via -token flag or MCIAS_TOKEN env var). // Both passwords are always prompted interactively; they are never accepted as // command-line flags to prevent them from appearing in shell history, ps // output, and process argument lists. // // Security: terminal echo is disabled during entry (golang.org/x/term); // raw byte slices are zeroed after use. The server requires the current // password to prevent token-theft attacks. On success all other active // sessions are revoked server-side. func (c *controller) authChangePassword(_ []string) { // Security: always prompt interactively; never accept passwords as flags. fmt.Fprint(os.Stderr, "Current password: ") rawCurrent, err := term.ReadPassword(int(os.Stdin.Fd())) //nolint:gosec // uintptr==int on all target platforms fmt.Fprintln(os.Stderr) if err != nil { fatalf("read current password: %v", err) } currentPasswd := string(rawCurrent) for i := range rawCurrent { rawCurrent[i] = 0 } fmt.Fprint(os.Stderr, "New password: ") rawNew, err := term.ReadPassword(int(os.Stdin.Fd())) //nolint:gosec // uintptr==int on all target platforms fmt.Fprintln(os.Stderr) if err != nil { fatalf("read new password: %v", err) } newPasswd := string(rawNew) for i := range rawNew { rawNew[i] = 0 } body := map[string]string{ "current_password": currentPasswd, "new_password": newPasswd, } c.doRequest("PUT", "/v1/auth/password", body, nil) fmt.Println("password changed; other active sessions revoked") } // ---- account subcommands ---- func (c *controller) runAccount(args []string) { if len(args) == 0 { fatalf("account requires a subcommand: list, create, get, update, delete, set-password") } switch args[0] { case "list": c.accountList() case "create": c.accountCreate(args[1:]) case "get": c.accountGet(args[1:]) case "update": c.accountUpdate(args[1:]) case "delete": c.accountDelete(args[1:]) case "set-password": c.accountSetPassword(args[1:]) default: fatalf("unknown account subcommand %q", args[0]) } } func (c *controller) accountList() { var result []json.RawMessage c.doRequest("GET", "/v1/accounts", nil, &result) printJSON(result) } func (c *controller) accountCreate(args []string) { fs := flag.NewFlagSet("account create", flag.ExitOnError) username := fs.String("username", "", "username (required)") accountType := fs.String("type", "human", "account type: human or system") _ = fs.Parse(args) if *username == "" { fatalf("account create: -username is required") } // Security: always prompt interactively for human-account passwords; never // accept them as a flag. Terminal echo is disabled; the raw byte slice is // zeroed after conversion to string. System accounts have no password. var passwd string if *accountType == "human" { fmt.Fprint(os.Stderr, "Password: ") raw, err := term.ReadPassword(int(os.Stdin.Fd())) //nolint:gosec // uintptr==int on all target platforms fmt.Fprintln(os.Stderr) if err != nil { fatalf("read password: %v", err) } passwd = string(raw) for i := range raw { raw[i] = 0 } } body := map[string]string{ "username": *username, "account_type": *accountType, } if passwd != "" { body["password"] = passwd } var result json.RawMessage c.doRequest("POST", "/v1/accounts", body, &result) printJSON(result) } func (c *controller) accountGet(args []string) { fs := flag.NewFlagSet("account get", flag.ExitOnError) id := fs.String("id", "", "account UUID (required)") _ = fs.Parse(args) if *id == "" { fatalf("account get: -id is required") } var result json.RawMessage c.doRequest("GET", "/v1/accounts/"+*id, nil, &result) printJSON(result) } func (c *controller) accountUpdate(args []string) { fs := flag.NewFlagSet("account update", flag.ExitOnError) id := fs.String("id", "", "account UUID (required)") status := fs.String("status", "", "new status: active or inactive") _ = fs.Parse(args) if *id == "" { fatalf("account update: -id is required") } if *status == "" { fatalf("account update: -status is required") } body := map[string]string{"status": *status} c.doRequest("PATCH", "/v1/accounts/"+*id, body, nil) fmt.Println("account updated") } func (c *controller) accountDelete(args []string) { fs := flag.NewFlagSet("account delete", flag.ExitOnError) id := fs.String("id", "", "account UUID (required)") _ = fs.Parse(args) if *id == "" { fatalf("account delete: -id is required") } c.doRequest("DELETE", "/v1/accounts/"+*id, nil, nil) fmt.Println("account deleted") } // accountSetPassword resets a human account's password (admin operation). // No current password is required. All active sessions for the target account // are revoked by the server on success. // // Security: the new password is always prompted interactively; it is never // accepted as a command-line flag to prevent it from appearing in shell // history, ps output, and process argument lists. Terminal echo is disabled // (golang.org/x/term); the raw byte slice is zeroed after use. func (c *controller) accountSetPassword(args []string) { fs := flag.NewFlagSet("account set-password", flag.ExitOnError) id := fs.String("id", "", "account UUID (required)") _ = fs.Parse(args) if *id == "" { fatalf("account set-password: -id is required") } // Security: always prompt interactively; never accept password as a flag. fmt.Fprint(os.Stderr, "New password: ") raw, err := term.ReadPassword(int(os.Stdin.Fd())) //nolint:gosec // uintptr==int on all target platforms fmt.Fprintln(os.Stderr) if err != nil { fatalf("read password: %v", err) } passwd := string(raw) for i := range raw { raw[i] = 0 } body := map[string]string{"new_password": passwd} c.doRequest("PUT", "/v1/accounts/"+*id+"/password", body, nil) fmt.Println("password updated; all active sessions revoked") } // ---- role subcommands ---- func (c *controller) runRole(args []string) { if len(args) == 0 { fatalf("role requires a subcommand: list, set") } switch args[0] { case "list": c.roleList(args[1:]) case "set": c.roleSet(args[1:]) default: fatalf("unknown role subcommand %q", args[0]) } } func (c *controller) roleList(args []string) { fs := flag.NewFlagSet("role list", flag.ExitOnError) id := fs.String("id", "", "account UUID (required)") _ = fs.Parse(args) if *id == "" { fatalf("role list: -id is required") } var result json.RawMessage c.doRequest("GET", "/v1/accounts/"+*id+"/roles", nil, &result) printJSON(result) } func (c *controller) roleSet(args []string) { fs := flag.NewFlagSet("role set", flag.ExitOnError) id := fs.String("id", "", "account UUID (required)") rolesFlag := fs.String("roles", "", "comma-separated list of roles") _ = fs.Parse(args) if *id == "" { fatalf("role set: -id is required") } roles := []string{} if *rolesFlag != "" { for _, r := range strings.Split(*rolesFlag, ",") { r = strings.TrimSpace(r) if r != "" { roles = append(roles, r) } } } body := map[string][]string{"roles": roles} c.doRequest("PUT", "/v1/accounts/"+*id+"/roles", body, nil) fmt.Printf("roles set: %v\n", roles) } // ---- token subcommands ---- func (c *controller) runToken(args []string) { if len(args) == 0 { fatalf("token requires a subcommand: issue, revoke") } switch args[0] { case "issue": c.tokenIssue(args[1:]) case "revoke": c.tokenRevoke(args[1:]) default: fatalf("unknown token subcommand %q", args[0]) } } func (c *controller) tokenIssue(args []string) { fs := flag.NewFlagSet("token issue", flag.ExitOnError) id := fs.String("id", "", "system account UUID (required)") _ = fs.Parse(args) if *id == "" { fatalf("token issue: -id is required") } body := map[string]string{"account_id": *id} var result json.RawMessage c.doRequest("POST", "/v1/token/issue", body, &result) printJSON(result) } func (c *controller) tokenRevoke(args []string) { fs := flag.NewFlagSet("token revoke", flag.ExitOnError) jti := fs.String("jti", "", "JTI of the token to revoke (required)") _ = fs.Parse(args) if *jti == "" { fatalf("token revoke: -jti is required") } c.doRequest("DELETE", "/v1/token/"+*jti, nil, nil) fmt.Println("token revoked") } // ---- pgcreds subcommands ---- func (c *controller) runPGCreds(args []string) { if len(args) == 0 { fatalf("pgcreds requires a subcommand: get, set") } switch args[0] { case "get": c.pgCredsGet(args[1:]) case "set": c.pgCredsSet(args[1:]) default: fatalf("unknown pgcreds subcommand %q", args[0]) } } func (c *controller) pgCredsGet(args []string) { fs := flag.NewFlagSet("pgcreds get", flag.ExitOnError) id := fs.String("id", "", "account UUID (required)") _ = fs.Parse(args) if *id == "" { fatalf("pgcreds get: -id is required") } var result json.RawMessage c.doRequest("GET", "/v1/accounts/"+*id+"/pgcreds", nil, &result) printJSON(result) } func (c *controller) pgCredsSet(args []string) { fs := flag.NewFlagSet("pgcreds set", flag.ExitOnError) id := fs.String("id", "", "account UUID (required)") host := fs.String("host", "", "Postgres host (required)") port := fs.Int("port", 5432, "Postgres port") dbName := fs.String("db", "", "Postgres database name (required)") username := fs.String("user", "", "Postgres username (required)") password := fs.String("password", "", "Postgres password (prompted if omitted)") _ = fs.Parse(args) if *id == "" || *host == "" || *dbName == "" || *username == "" { fatalf("pgcreds set: -id, -host, -db, and -user are required") } // Prompt for the Postgres password interactively if not supplied so it // stays out of shell history. // Security: terminal echo is disabled during entry; the raw byte slice is // zeroed after conversion to string. passwd := *password if passwd == "" { fmt.Fprint(os.Stderr, "Postgres password: ") raw, err := term.ReadPassword(int(os.Stdin.Fd())) //nolint:gosec // uintptr==int on all target platforms fmt.Fprintln(os.Stderr) if err != nil { fatalf("read password: %v", err) } passwd = string(raw) for i := range raw { raw[i] = 0 } } body := map[string]interface{}{ "host": *host, "port": *port, "database": *dbName, "username": *username, "password": passwd, } c.doRequest("PUT", "/v1/accounts/"+*id+"/pgcreds", body, nil) fmt.Println("credentials stored") } // ---- policy subcommands ---- func (c *controller) runPolicy(args []string) { if len(args) == 0 { fatalf("policy requires a subcommand: list, create, get, update, delete") } switch args[0] { case "list": c.policyList() case "create": c.policyCreate(args[1:]) case "get": c.policyGet(args[1:]) case "update": c.policyUpdate(args[1:]) case "delete": c.policyDelete(args[1:]) default: fatalf("unknown policy subcommand %q", args[0]) } } func (c *controller) policyList() { var result json.RawMessage c.doRequest("GET", "/v1/policy/rules", nil, &result) printJSON(result) } func (c *controller) policyCreate(args []string) { fs := flag.NewFlagSet("policy create", flag.ExitOnError) description := fs.String("description", "", "rule description (required)") jsonFile := fs.String("json", "", "path to JSON file containing the rule body (required)") priority := fs.Int("priority", 100, "rule priority (lower = evaluated first)") notBefore := fs.String("not-before", "", "earliest activation time (RFC3339, optional)") expiresAt := fs.String("expires-at", "", "expiry time (RFC3339, optional)") _ = fs.Parse(args) if *description == "" { fatalf("policy create: -description is required") } if *jsonFile == "" { fatalf("policy create: -json is required (path to rule body JSON file)") } // G304: path comes from a CLI flag supplied by the operator. ruleBytes, err := os.ReadFile(*jsonFile) //nolint:gosec if err != nil { fatalf("policy create: read %s: %v", *jsonFile, err) } // Validate that the file contains valid JSON before sending. var ruleBody json.RawMessage if err := json.Unmarshal(ruleBytes, &ruleBody); err != nil { fatalf("policy create: invalid JSON in %s: %v", *jsonFile, err) } body := map[string]interface{}{ "description": *description, "priority": *priority, "rule": ruleBody, } if *notBefore != "" { if _, err := time.Parse(time.RFC3339, *notBefore); err != nil { fatalf("policy create: -not-before must be RFC3339: %v", err) } body["not_before"] = *notBefore } if *expiresAt != "" { if _, err := time.Parse(time.RFC3339, *expiresAt); err != nil { fatalf("policy create: -expires-at must be RFC3339: %v", err) } body["expires_at"] = *expiresAt } var result json.RawMessage c.doRequest("POST", "/v1/policy/rules", body, &result) printJSON(result) } func (c *controller) policyGet(args []string) { fs := flag.NewFlagSet("policy get", flag.ExitOnError) id := fs.String("id", "", "rule ID (required)") _ = fs.Parse(args) if *id == "" { fatalf("policy get: -id is required") } var result json.RawMessage c.doRequest("GET", "/v1/policy/rules/"+*id, nil, &result) printJSON(result) } func (c *controller) policyUpdate(args []string) { fs := flag.NewFlagSet("policy update", flag.ExitOnError) id := fs.String("id", "", "rule ID (required)") priority := fs.Int("priority", -1, "new priority (-1 = no change)") enabled := fs.String("enabled", "", "true or false") notBefore := fs.String("not-before", "", "earliest activation time (RFC3339)") expiresAt := fs.String("expires-at", "", "expiry time (RFC3339)") clearNotBefore := fs.Bool("clear-not-before", false, "remove not_before constraint") clearExpiresAt := fs.Bool("clear-expires-at", false, "remove expires_at constraint") _ = fs.Parse(args) if *id == "" { fatalf("policy update: -id is required") } body := map[string]interface{}{} if *priority >= 0 { body["priority"] = *priority } if *enabled != "" { switch *enabled { case "true": b := true body["enabled"] = b case "false": b := false body["enabled"] = b default: fatalf("policy update: -enabled must be true or false") } } if *clearNotBefore { body["clear_not_before"] = true } else if *notBefore != "" { if _, err := time.Parse(time.RFC3339, *notBefore); err != nil { fatalf("policy update: -not-before must be RFC3339: %v", err) } body["not_before"] = *notBefore } if *clearExpiresAt { body["clear_expires_at"] = true } else if *expiresAt != "" { if _, err := time.Parse(time.RFC3339, *expiresAt); err != nil { fatalf("policy update: -expires-at must be RFC3339: %v", err) } body["expires_at"] = *expiresAt } if len(body) == 0 { fatalf("policy update: at least one flag is required") } var result json.RawMessage c.doRequest("PATCH", "/v1/policy/rules/"+*id, body, &result) printJSON(result) } func (c *controller) policyDelete(args []string) { fs := flag.NewFlagSet("policy delete", flag.ExitOnError) id := fs.String("id", "", "rule ID (required)") _ = fs.Parse(args) if *id == "" { fatalf("policy delete: -id is required") } c.doRequest("DELETE", "/v1/policy/rules/"+*id, nil, nil) fmt.Println("policy rule deleted") } // ---- tag subcommands ---- func (c *controller) runTag(args []string) { if len(args) == 0 { fatalf("tag requires a subcommand: list, set") } switch args[0] { case "list": c.tagList(args[1:]) case "set": c.tagSet(args[1:]) default: fatalf("unknown tag subcommand %q", args[0]) } } func (c *controller) tagList(args []string) { fs := flag.NewFlagSet("tag list", flag.ExitOnError) id := fs.String("id", "", "account UUID (required)") _ = fs.Parse(args) if *id == "" { fatalf("tag list: -id is required") } var result json.RawMessage c.doRequest("GET", "/v1/accounts/"+*id+"/tags", nil, &result) printJSON(result) } func (c *controller) tagSet(args []string) { fs := flag.NewFlagSet("tag set", flag.ExitOnError) id := fs.String("id", "", "account UUID (required)") tagsFlag := fs.String("tags", "", "comma-separated list of tags (empty string clears all tags)") _ = fs.Parse(args) if *id == "" { fatalf("tag set: -id is required") } tags := []string{} if *tagsFlag != "" { for _, t := range strings.Split(*tagsFlag, ",") { t = strings.TrimSpace(t) if t != "" { tags = append(tags, t) } } } body := map[string][]string{"tags": tags} c.doRequest("PUT", "/v1/accounts/"+*id+"/tags", body, nil) fmt.Printf("tags set: %v\n", tags) } // ---- HTTP helpers ---- // doRequest performs an authenticated JSON HTTP request. If result is non-nil, // the response body is decoded into it. Exits on error. func (c *controller) doRequest(method, path string, body, result interface{}) { url := c.serverURL + path var bodyReader *strings.Reader if body != nil { b, err := json.Marshal(body) if err != nil { fatalf("marshal request body: %v", err) } bodyReader = strings.NewReader(string(b)) } else { bodyReader = strings.NewReader("") } req, err := http.NewRequest(method, url, bodyReader) if err != nil { fatalf("create request: %v", err) } req.Header.Set("Content-Type", "application/json") if c.token != "" { req.Header.Set("Authorization", "Bearer "+c.token) } resp, err := c.client.Do(req) if err != nil { fatalf("HTTP %s %s: %v", method, path, err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode >= 400 { var errBody map[string]string _ = json.NewDecoder(resp.Body).Decode(&errBody) msg := errBody["error"] if msg == "" { msg = resp.Status } fatalf("server returned %d: %s", resp.StatusCode, msg) } if result != nil && resp.StatusCode != http.StatusNoContent { if err := json.NewDecoder(resp.Body).Decode(result); err != nil { fatalf("decode response: %v", err) } } } // newHTTPClient builds an http.Client with optional custom CA certificate. // Security: TLS 1.2+ is required; the system CA pool is used by default. func newHTTPClient(caCertPath string) (*http.Client, error) { tlsCfg := &tls.Config{ MinVersion: tls.VersionTLS12, } if caCertPath != "" { // G304: path comes from a CLI flag supplied by the operator, not from // untrusted input. File inclusion is intentional. pemData, err := os.ReadFile(caCertPath) //nolint:gosec if err != nil { return nil, fmt.Errorf("read CA cert: %w", err) } pool := x509.NewCertPool() if !pool.AppendCertsFromPEM(pemData) { return nil, fmt.Errorf("no valid certificates found in %s", caCertPath) } tlsCfg.RootCAs = pool } return &http.Client{ Transport: &http.Transport{ TLSClientConfig: tlsCfg, }, Timeout: 30 * time.Second, }, nil } // printJSON pretty-prints a JSON value to stdout. func printJSON(v interface{}) { enc := json.NewEncoder(os.Stdout) enc.SetIndent("", " ") if err := enc.Encode(v); err != nil { fatalf("encode output: %v", err) } } // fatalf prints an error message and exits with code 1. func fatalf(format string, args ...interface{}) { fmt.Fprintf(os.Stderr, "mciasctl: "+format+"\n", args...) os.Exit(1) } func usage() { fmt.Fprintf(os.Stderr, `mciasctl - MCIAS admin CLI Usage: mciasctl [global flags] [args] Global flags: -server URL of the mciassrv instance (default: https://localhost:8443) -token Bearer token (or set MCIAS_TOKEN env var) -cacert Path to CA certificate for TLS verification Commands: auth login -username NAME [-totp CODE] Obtain a bearer token. Password is always prompted interactively (never accepted as a flag) to avoid shell-history exposure. Token is written to stdout; expiry to stderr. Example: export MCIAS_TOKEN=$(mciasctl auth login -username alice) auth change-password Change the current user's own password. Requires a valid bearer token. Current and new passwords are always prompted interactively. Revokes all other active sessions on success. account list account create -username NAME [-type human|system] account get -id UUID account update -id UUID -status active|inactive account delete -id UUID account set-password -id UUID Admin: reset a human account's password without requiring the current password. New password is always prompted interactively. Revokes all active sessions for the account. role list -id UUID role set -id UUID -roles role1,role2,... token issue -id UUID token revoke -jti JTI pgcreds get -id UUID pgcreds set -id UUID -host HOST [-port PORT] -db DB -user USER [-password PASS] policy list policy create -description STR -json FILE [-priority N] [-not-before RFC3339] [-expires-at RFC3339] FILE must contain a JSON rule body, e.g.: {"effect":"allow","actions":["pgcreds:read"],"resource_type":"pgcreds","owner_matches_subject":true} policy get -id ID policy update -id ID [-priority N] [-enabled true|false] [-not-before RFC3339] [-expires-at RFC3339] [-clear-not-before] [-clear-expires-at] policy delete -id ID tag list -id UUID tag set -id UUID -tags tag1,tag2,... Pass empty -tags "" to clear all tags. `) }