Secure deployments
Deployments authenticate scoring requests through one or more SecurityOptions entries. Carrying two or more entries lets you mix mechanisms (for example, OIDC alongside a passphrase) and rotate credentials without downtime.
Prerequisites
Before you begin, complete the following steps:
- Import the necessary Python packages. For instructions, see Step 1: Import the required packages.
- Connect to H2O MLOps. For instructions, see Connect to H2O MLOps.
- Create a workspace. For instructions, see Create a workspace.
- Create an experiment. For instructions, see Create an experiment.
- Create a model and register the experiment with it. For instructions, see Register an experiment with a model.
The examples below assume workspace, comp_opts, model, and scoring_runtime are already defined.
Security types
The table below describes each SecurityType value, when to use it, and any trade-offs to consider.
SecurityType | Use when | Notes |
|---|---|---|
DISABLED | Testing or internal-only deployments that require no authentication. | Endpoint is publicly accessible. Do not use in production. |
PLAIN_PASSPHRASE | You accept passphrase exposure in transit. | The passphrase travels in clear text. Less secure than the hashed variant. |
HASHED_PASSPHRASE | You want passphrase authentication with stronger server-side storage. | Uses PBKDF2 hashing. The actual secret does not retransmit after create. |
OIDC_AUTH | You need token-based authentication integrated with your identity provider. | Requires server-side configuration. See Endpoint security. |
API_KEY | You need a high-entropy, server-generated service-to-service credential. | Server returns the plaintext exactly once on the create response. See Rotate an API_KEY. |
Not all security types work in every environment. Cluster operators configure the allowed types.
To check the allowed types using the H2O MLOps Python client, run: mlops.configs.allowed_security_types.
API_KEY requires "API_KEY" in securityOptions.activated in your values.yaml. Without it, the server rejects API_KEY entries with "unsupported api_key security option in current environment".
Multi-entry security model
A deployment can carry one or more SecurityOptions entries. At scoring time, any single entry that passes authenticates the request. The server evaluates entries independently.
A naming convention to know up front: the read-side property is deployment.securities_options (plural), and the keyword argument on create() and update() is security_options= (singular), even when you pass a list. Avoid the deprecated singular deployment.security_options, which returns only the first entry.
Each entry carries an optional name (a human-readable label) and a server-assigned uid (stable identifier). When updating, read entries from deployment.securities_options to get the uid values you need for EDIT and DELETE actions. See the SecurityOptions field reference for the full field list.
Read the security entries on a deployment
Use the securities_options property (plural) to read the current list of entries. Code examples in this page reuse the _print_entries(dep) helper defined below:
def _print_entries(dep):for s in dep.securities_options:print(f" uid={s.uid!r} type={s.security_type.name} name={s.name!r}")_print_entries(deployment)
security_options (singular)deployment.security_options returns only the first entry and emits a DeprecationWarning:
deployment.security_options is deprecated and returns only the first security entry. Use deployment.securities_options instead.
Use deployment.securities_options (plural) in all new code.
Constraints on the entry list
The server enforces these constraints on the entry list. They apply to both create and update calls.
DISABLEDcannot coexist with other entries. The server rejects a list that mixesDISABLEDwith any other security type:"disabled_security cannot be combined with other security options".- Combined cap of 100 passphrase and
API_KEYentries. The server caps the combined count ofPLAIN_PASSPHRASE,HASHED_PASSPHRASE, andAPI_KEYentries on a single deployment at 100. OIDC entries do not count toward this cap. Exceeding the cap returns"number of passphrase/api_key entries must not exceed 100". - A deployment must carry at least one entry. The server rejects an update that would leave the deployment with no entries:
"missing authentication type". To switch a secured deployment to no authentication, replace the entry list with a singleDISABLEDentry rather than emptying it.
Create a deployment with security
Single entry (shorthand)
Pass a single SecurityOptions instance to create a one-entry secured deployment. The read-side returns the entry with a server-assigned uid.
from h2o_mlops.options import SecurityOptions, CompositionOptionsfrom h2o_mlops.types import DeploymentModeType, SecurityTypesec_single = SecurityOptions(security_type=SecurityType.PLAIN_PASSPHRASE,passphrase="single-entry-secret",name="default-passphrase",)dep_single = workspace.deployments.create(name="security-demo-single",composition_options=[comp_opts],mode=DeploymentModeType.SINGLE_MODEL,security_options=sec_single,)_print_entries(dep_single)# Scoring uses the passphrase as auth value:dep_single.scorer.score(auth_value="single-entry-secret",payload=dep_single.scorer.sample_request(auth_value="single-entry-secret"),)
Two or more entries
Pass a list of SecurityOptions instances. Each entry independently authenticates incoming requests.
from h2o_mlops.options import SecurityOptions, CompositionOptionsfrom h2o_mlops.types import DeploymentModeType, SecurityTypesec_oidc = SecurityOptions(security_type=SecurityType.OIDC_AUTH,name="company-oidc",)sec_pass = SecurityOptions(security_type=SecurityType.HASHED_PASSPHRASE,passphrase="initial-secret",name="hashed-passphrase",)dep_multi = workspace.deployments.create(name="security-demo-multi",composition_options=[comp_opts],mode=DeploymentModeType.SINGLE_MODEL,security_options=[sec_oidc, sec_pass],)_print_entries(dep_multi)# Either credential is accepted at scoring time:dep_multi.scorer.score(auth_value="initial-secret",payload=dep_multi.scorer.sample_request(auth_value="initial-secret"),)dep_multi.scorer.score(auth_value="<oidc-bearer-token>",payload=dep_multi.scorer.sample_request(auth_value="<oidc-bearer-token>"),)
Create with API_KEY
The server generates the API key. Set security_type=SecurityType.API_KEY and leave api_key unset. The client raises ValueError if you try to supply one.
from h2o_mlops.options import SecurityOptions, CompositionOptionsfrom h2o_mlops.types import DeploymentModeType, SecurityTypesec_api_key = SecurityOptions(security_type=SecurityType.API_KEY,name="service-key",)dep_api_key = workspace.deployments.create(name="security-demo-api-key",composition_options=[comp_opts],mode=DeploymentModeType.SINGLE_MODEL,security_options=sec_api_key,)# Read the plaintext now (see the caution below).plaintext = dep_api_key.securities_options[0].api_keyprint(f"API key (save this now): {plaintext!r}")# api_key_prefix is safe to log on every read.prefix = dep_api_key.securities_options[0].api_key_prefixprint(f"API key prefix: {prefix!r}")# Use the plaintext as auth_value for scoring:dep_api_key.scorer.score(auth_value=plaintext,payload=dep_api_key.scorer.sample_request(auth_value=plaintext),)
The create response carries api_key in plaintext exactly once. Later reads return None for api_key; only api_key_prefix is available for identification. Store the plaintext securely before proceeding.
Update security on an existing deployment
Each entry in an update sets action (ADD, EDIT, or DELETE) to indicate the per-entry operation. All operations in the list bundle into a single PATCH and trigger a single redeployment.
ADD
Insert a new entry.
from h2o_mlops.types import SecurityActiondep_multi.update(security_options=[SecurityOptions(action=SecurityAction.ADD,security_type=SecurityType.PLAIN_PASSPHRASE,passphrase="granular-added-secret",name="granular-add",),],)
EDIT to rotate or rename
Fetch the current entries to get their server-assigned uid values, then submit both edits in one call.
from h2o_mlops.types import SecurityActionentries = dep_multi.securities_optionsgranular_added = next(s for s in entries if s.name == "granular-add")oidc_entry = next(s for s in entries if s.security_type == SecurityType.OIDC_AUTH)dep_multi.update(security_options=[SecurityOptions(action=SecurityAction.EDIT,uid=granular_added.uid,security_type=SecurityType.PLAIN_PASSPHRASE,passphrase="granular-rotated-secret", # supplying a value re-hashesname="granular-add",),SecurityOptions(action=SecurityAction.EDIT,uid=oidc_entry.uid,security_type=SecurityType.OIDC_AUTH,name="company-oidc-renamed", # rename only),],)
EDIT to preserve a stored hash
Set passphrase=None on a passphrase-type EDIT to keep the existing hash and change only metadata such as name.
from h2o_mlops.types import SecurityActionhashed = next(s for s in dep_multi.securities_optionsif s.security_type == SecurityType.HASHED_PASSPHRASE)dep_multi.update(security_options=[SecurityOptions(action=SecurityAction.EDIT,uid=hashed.uid,security_type=SecurityType.HASHED_PASSPHRASE,passphrase=None, # keep the stored hashname="hashed-passphrase-renamed",),],)# Original secret still works:dep_multi.scorer.score(auth_value="initial-secret",payload=dep_multi.scorer.sample_request(auth_value="initial-secret"),)
DELETE
from h2o_mlops.types import SecurityActiontarget = next(s for s in dep_multi.securities_options if s.name == "granular-add")dep_multi.update(security_options=[SecurityOptions(action=SecurityAction.DELETE, uid=target.uid),],)
Combined operations in one PATCH
Bundle ADD, EDIT, and DELETE into a single call for atomic rotation: one PATCH, one redeployment. The deployment never loses the entries that remain valid.
ADD + EDIT + DELETE:
from h2o_mlops.types import SecurityActionentries = dep_multi.securities_optionsoidc = next(s for s in entries if s.security_type == SecurityType.OIDC_AUTH)hashed = next(s for s in entries if s.security_type == SecurityType.HASHED_PASSPHRASE)dep_multi.update(security_options=[SecurityOptions(action=SecurityAction.ADD,security_type=SecurityType.PLAIN_PASSPHRASE,passphrase="combined-added-secret",name="combined-add",),SecurityOptions(action=SecurityAction.EDIT,uid=oidc.uid,security_type=SecurityType.OIDC_AUTH,name="company-oidc-final",),SecurityOptions(action=SecurityAction.DELETE, uid=hashed.uid),],)
Granular security combined with kubernetes_options:
You can send security operations alongside non-security updates in the same call: one PATCH, one redeployment.
from h2o_mlops.options import KubernetesOptionsfrom h2o_mlops.types import SecurityActiontarget = next(s for s in dep_multi.securities_options if s.name == "combined-add")dep_multi.update(security_options=[SecurityOptions(action=SecurityAction.EDIT,uid=target.uid,security_type=SecurityType.PLAIN_PASSPHRASE,passphrase=None, # preserve the stored hashname="combined-add-renamed",),],kubernetes_options=KubernetesOptions(replicas=2),)
Rotate an API_KEY
An EDIT on an API_KEY entry silently preserves the existing key. The server does not generate a new key on update and does not raise an error. To rotate:
- In a single
update()call,ADDa newAPI_KEYentry andDELETEthe old one. Bundling both operations keeps the deployment continuously authenticated through one PATCH. - Read the new plaintext from
dep.securities_optionsimmediately after the call returns.
from h2o_mlops.types import SecurityAction# Find the existing API_KEY entry to retire.old_key = next(s for s in dep_api_key.securities_optionsif s.security_type == SecurityType.API_KEY)dep_api_key.update(security_options=[SecurityOptions(action=SecurityAction.ADD,security_type=SecurityType.API_KEY,name="service-key-rotated",),SecurityOptions(action=SecurityAction.DELETE, uid=old_key.uid),],)# Read the new plaintext immediately from the response.new_plaintext = next(s.api_key for s in dep_api_key.securities_optionsif s.name == "service-key-rotated")print(f"New API key (save this now): {new_plaintext!r}")
SecurityOptions field reference
| Field | Type | Notes |
|---|---|---|
security_type | SecurityType | None | Required for most operations. May be None only when action == SecurityAction.DELETE. |
passphrase | str | None | Required for PLAIN_PASSPHRASE and HASHED_PASSPHRASE. Must be None for DISABLED, OIDC_AUTH, and API_KEY (raises ValueError otherwise). On EDIT, None preserves the stored hash; supplying a value re-hashes. |
api_key | str | None | Read-only. Server-populated on the create response, exactly once. Must be None on create and update (raises ValueError otherwise). |
api_key_prefix | str | None | Read-only. Short display prefix returned on every read. Safe to include in audit logs. |
name | str | None | Optional human-readable label. Not used for identity or deduplication. |
uid | str | None | Server-assigned. Leave unset on create. Required (positive-int string) for EDIT and DELETE. |
action | SecurityAction | None | Per-entry operation tag (ADD, EDIT, or DELETE) for update calls. Required on every entry in an update list; all entries in a single update must consistently set action. |
SecurityAction enum reference
SecurityAction | Wire value | Constraints |
|---|---|---|
ADD | "add" | Leave uid as None; the server assigns one. Provide security_type. For passphrase types, provide a non-empty passphrase. |
EDIT | "edit" | Provide a positive-int uid matching an existing entry. Provide security_type. For passphrase types, passphrase=None preserves the stored hash; a non-None value re-hashes. |
DELETE | "delete" | Provide a positive-int uid matching an existing entry. The server ignores all other fields. security_type may be None. |
All entries in a single update must consistently set action. Mixing tagged and untagged entries raises ValueError before any HTTP call.
Validation rules
The client enforces all rules below before any HTTP call. The deployment stays untouched on any ValueError.
| Failure | Why | Exact message (partial) |
|---|---|---|
| Mixing tagged and untagged entries | All entries in a single update must consistently set action. | "All SecurityOptions entries must have an action, or none of them must." |
ADD with non-None uid | The server assigns the uid; clients must not supply one. | "SecurityOptions with action=ADD must have uid=None..." |
ADD with missing security_type | Required to know what to insert. | "SecurityOptions with action=ADD requires security_type to be set." |
ADD with passphrase type but empty passphrase | A passphrase entry without a passphrase is incomplete. | "SecurityOptions with action=ADD and security_type=... requires a non-empty passphrase." |
EDIT with missing security_type | Required to know what to write back. | "SecurityOptions with action=EDIT requires security_type to be set." |
EDIT or DELETE with missing or non-integer uid | Must be a positive-int string. | "SecurityOptions with action=... requires uid to be set (positive-integer string)." / "SecurityOptions.uid ... must be a positive integer string for action=..." |
Duplicate uid across actions in one call | Ambiguous which operation wins. | "duplicate uid ... across EDIT/DELETE entries; each uid may appear at most once per update." |
The following example triggers the mixed-mode ValueError:
from h2o_mlops.types import SecurityAction# Raises ValueError before any HTTP call:dep_multi.update(security_options=[SecurityOptions(action=SecurityAction.ADD, security_type=SecurityType.OIDC_AUTH),SecurityOptions(security_type=SecurityType.DISABLED), # untagged -- raises ValueError],)
- Submit and view feedback for this page
- Send feedback about H2O MLOps to cloud-feedback@h2o.ai