Revise project-wide threat model per PMC review (DaanHoogland, vishesh92)

Generated-by: Claude Code
This commit is contained in:
Jarek Potiuk 2026-06-04 04:46:15 +02:00
parent f79272559e
commit e39e62c112
No known key found for this signature in database
1 changed files with 232 additions and 152 deletions

View File

@ -29,6 +29,9 @@
> document and add only what each satellite uniquely introduces (`§4 B1`
> reachability, the credential file shape, the wrapper-of-SDK contract,
> etc.). The deltas live at `/tmp/claude/cloudstack-<repo>-threat-model-draft.md`.
> The satellite clients' interfaces point **inward** at the management-server
> API; some satellites additionally expose **outward** interfaces that are
> designed to be safe to expose *(maintainer: DaanHoogland)*.
> An umbrella model was rejected because the satellites are uniformly thin
> "HMAC-SHA1-signing HTTP client" wrappers — a single document either
> drowns them in CloudStack-server content or, worse, drowns the
@ -60,7 +63,11 @@
response to this draft; *(inferred)* = synthesized by the producer from
code structure or domain knowledge, awaiting PMC ratification (every
*(inferred)* tag has a matching §14 question).
- **Draft confidence:** 36 documented / 0 maintainer / 41 inferred.
- **Draft confidence (provenance-tag tally):** 51 *(documented)* / 35
*(maintainer)* / 45 *(inferred)*. Five formerly-open questions (Q1, Q2,
Q4, Q5, Q12 — including the highest-leverage Root-CA strictness default)
were resolved by the CloudStack PMC review (DaanHoogland, vishesh92) and
their tags promoted from *(inferred)* to *(maintainer)*.
**About the project.** Apache CloudStack is an open-source Infrastructure-as-a-
Service (IaaS) orchestration platform *(documented: `README.md`,
@ -92,15 +99,19 @@ private/public cloud control plane", not a hosted-as-a-service appliance.
CloudStack is **not** an in-process library, **not** a single-binary
appliance, and **not** a hosted SaaS. It is a distributed control plane:
one or more management-server instances (clustered behind a load balancer
in production), a MariaDB/MySQL database, one usage server, an optional
one or more management-server instances — **a single management-server
instance for smaller clouds, or a cluster behind a load balancer for
larger deployments** *(maintainer: DaanHoogland)* — a MariaDB/MySQL
database, one usage server, an optional
SecondaryStorageVM/ConsoleProxyVM/VirtualRouter set of system VMs, and a
per-hypervisor-host `cloudstack-agent` (for KVM/Hyper-V/baremetal) or
out-of-process resource bridges (for VMware / XenServer / XCP-ng). The
operator owns the surrounding L2/L3 network (the **management network**,
per-hypervisor-host `cloudstack-agent` (for KVM/baremetal) or
out-of-process resource bridges (for VMware / XenServer / XCP-ng / Hyper-V).
The operator owns the surrounding L2/L3 network (the **management network**,
the **public network**, the **guest network**, the **storage network**)
and the physical hosts. The threat model is therefore that of a clustered
distributed service, not a library *(inferred — §14 Q1)*.
and the physical hosts. The threat model is therefore that of a
distributed service (single-instance or clustered), not a library
*(maintainer: DaanHoogland — confirms the distributed control-plane shape;
single-instance is also a supported topology)*.
### Caller roles
@ -110,11 +121,11 @@ distributed service, not a library *(inferred — §14 Q1)*.
| **Domain / Project admin** | partial trust within their domain | Bounded by RBAC (`plugins/acl/{static,dynamic,project}-role-based`) and the domain hierarchy; can manage users / VMs / networks within a domain. |
| **Root admin** | trusted control plane | Global RBAC role; can change global configuration, upload templates/ISOs, run privileged orchestration. |
| **Operator / cluster admin** | trusted | OS-level access to management-server hosts, the MariaDB database, the keystore, and the agent hosts. Sets `agent.properties`, manages `cloudstack-agent` packages, manages the JCEKS keystore used by the agent for TLS *(documented: `agent/conf/agent.properties`, `framework/security/.../KeystoreManager.java`)*. |
| **Hypervisor agent (cloudstack-agent on KVM/Hyper-V/baremetal)** | trusted-once-enrolled peer | Mutually authenticated via X.509 client cert signed by the management server's Root CA *(documented: `framework/ca/`, `plugins/ca/root-ca/`, `agent/src/main/java/com/cloud/agent/Agent.java` `setupAgentKeystore`)*. |
| **System VM (SSVM / CPVM / VR)** | trusted-once-enrolled peer | Same X.509 enrolment shape as the agent; carries the agent binary inside *(inferred — §14 Q2)*. |
| **Hypervisor agent (cloudstack-agent on KVM/baremetal)** | trusted-once-enrolled peer | Mutually authenticated via X.509 client cert signed by the management server's Root CA *(documented: `framework/ca/`, `plugins/ca/root-ca/`, `agent/src/main/java/com/cloud/agent/Agent.java` `setupAgentKeystore`)*. |
| **System VM (SSVM / CPVM / VR)** | trusted-once-enrolled peer | Same X.509 enrolment shape as the agent; carries the agent binary inside *(maintainer: confirmed — same trust tier as agents, not a separate tier)*. |
| **Hypervisor host (the underlying KVM/VMware/etc.)** | trusted by virtue of operator-controlled provisioning | CloudStack expects to drive the hypervisor via libvirt / VMware vSphere SDK / XenAPI as a privileged user *(documented: `plugins/hypervisors/kvm/`, `plugins/hypervisors/vmware/`, `plugins/hypervisors/xenserver/`)*. |
| **Hypervisor-managed guest VM (end-user workload)** | **untrusted** | A guest VM is an attacker's workload; the model defends against it. |
| **Reverse proxy / load balancer in front of management server** | trusted *(if `useForwardHeader=true`)* | When the operator enables forward-header processing, only requests from IPs in `proxy.forward.list` have their forward header honoured *(documented: `server/src/main/java/com/cloud/api/ApiServlet.java` `getClientAddress`)*. |
| **Reverse proxy / load balancer in front of management server** | trusted *(if `proxy.header.verify=true`)* | When the operator enables forward-header processing, only requests whose `Remote_Addr``proxy.cidr` have their `proxy.header.names` header honoured *(documented: `server/src/main/java/com/cloud/api/ApiServlet.java` `getClientAddress`; setting names maintainer: vishesh92)*. |
| **Underlying storage (primary / secondary)** | trusted by virtue of operator-granted credentials | CloudStack reads/writes via NFS / RBD / iSCSI / S3 with operator-supplied credentials *(documented: primary/secondary storage plugins under `plugins/storage/`)*. |
| **External integrations (Tungsten, NSX, Netscaler, Palo Alto, …)** | trusted control-plane peers | Operator-configured; CloudStack assumes truthful responses *(inferred — §14 Q3)*. |
@ -126,10 +137,10 @@ distributed service, not a library *(inferred — §14 Q1)*.
| Management server Web UI | Vue.js SPA under `ui/`, served by the same servlet container *(documented: `ui/`)* | network | **yes** (auth is the API auth) |
| Management server cluster RPC (peer-to-peer) | NIO + TLS between management-server replicas, `:9090` *(documented: `framework/cluster/`, `utils/.../nio/`)* | network | **yes** (peer auth via Root CA) |
| Management server → agent RPC | NIO + TLS on `:8250` (default `agent.properties`) *(documented: `agent/conf/agent.properties` line 47, `utils/.../nio/NioServer.java`)* | network | **yes** (mutually authenticated via Root CA) |
| `cloudstack-agent` (KVM/Hyper-V/baremetal) | reverse-connects to management server, runs commands via libvirt / hypervisor SDK *(documented: `agent/`, `plugins/hypervisors/kvm/`)* | network + hypervisor + OS | **yes** |
| `cloudstack-agent` (KVM/baremetal) | reverse-connects to management server, runs commands via libvirt / hypervisor SDK *(documented: `agent/`, `plugins/hypervisors/kvm/`)* | network + hypervisor + OS | **yes** |
| System VMs — SecondaryStorageVM, ConsoleProxyVM, Virtual Router | shipped images under `systemvm/`; agent binaries inside them *(documented: `systemvm/`)* | network (storage / public / guest) | **yes** |
| Console proxy data path | browser ↔ ConsoleProxyVM ↔ hypervisor VNC/SPICE socket; signed token issued by management server *(documented: `server/src/main/java/com/cloud/servlet/ConsoleProxyServlet.java`, `server/src/main/java/com/cloud/servlet/ConsoleProxyPasswordBasedEncryptor.java`)* | network | **yes** |
| Secondary-storage HTTP (templates, ISO downloads, snapshot copies) | SSVM serves HTTPS *(inferred — §14 Q4)* | network | **yes** |
| Secondary-storage HTTP (templates, ISO downloads, snapshot copies) | download links are UUID-named symlinks served by an Apache httpd, with **no auth on the link**; the UUID format prevents enumeration and the symlinks are removed after a period *(maintainer: vishesh92, DaanHoogland)* | network | **yes** |
| Hypervisor plugins (`plugins/hypervisors/{kvm,vmware,xenserver,hyperv,ovm,ovm3,baremetal,ucs,simulator}`) | invoked by agent or by management server *(documented: `plugins/hypervisors/`)* | hypervisor APIs | **yes** for the call shape; **out-of-model** for the upstream hypervisor's own bugs |
| Network plugins (`plugins/network-elements/{netscaler,nsx,palo-alto,tungsten,nicira-nvp,...}`) | management server outbound | external SDN/firewall APIs | **yes** for credential handling and request construction; **out-of-model** for the external endpoint |
| Storage plugins (`plugins/storage/{volume,image,object}`) | management server / agent | NFS, RBD, iSCSI, S3 endpoints | **yes** for credential handling; **out-of-model** for the storage endpoint |
@ -140,7 +151,7 @@ distributed service, not a library *(inferred — §14 Q1)*.
| Quota / metrics / DRS / HA planners | internal | none | **yes** as orchestration only; not a security boundary |
| Database layer (MariaDB/MySQL, Jasypt-encrypted secrets) | management server | network to DB | **yes** for credential handling; DB itself is trusted *(documented: `README.md` "Notice of Cryptographic Software" — JaSypt, native DB encryption)* |
| `cloud-cli`, `tools/marvin`, `test/`, `developer/`, `quickcloud/` | integration / test tooling | varies | **out of model** *(§3)* |
| `systemvm/agent/noVNC/vendor/pako`, other vendored JS / shell scripts | vendored upstream | n/a | in-model only at the wrapper boundary; upstream bugs go upstream *(inferred — §14 Q5)* |
| `systemvm/agent/noVNC` (a vendored fork of `github.com/novnc/novnc` with CloudStack-specific changes on top *(maintainer: vishesh92)*), `…/vendor/pako`, other vendored JS / shell scripts | vendored upstream | n/a | in-model only at the wrapper boundary; upstream bugs go upstream. No automated vendored-dependency update procedure exists today (dependabot does not produce viable PRs); the PMC would prefer to have one *(maintainer: DaanHoogland)* |
## §3 Out of scope (explicit non-goals)
@ -200,9 +211,14 @@ requiring any of these will be closed with the cited disposition:
8. **Bundled / vendored upstream libraries** — JaSypt, Bouncy Castle,
JSch, OpenSwan, noVNC + `pako`, MariaDB Connector/J, Spring,
Apache Commons, log4j, etc. *(documented: `README.md` Cryptographic
Software notice)*. Where CloudStack vendors source, the vendored code
is modeled at the wrapper boundary; vulnerabilities intrinsic to the
upstream project should be reported upstream *(inferred — §14 Q5)*.
Software notice)*. `systemvm/agent/noVNC` is specifically a **vendored
fork of `github.com/novnc/novnc`** carrying CloudStack-specific changes
*(maintainer: vishesh92)*. Where CloudStack vendors source, the vendored
code is modeled at the wrapper boundary; vulnerabilities intrinsic to the
upstream project should be reported upstream. There is currently **no
automated procedure** to pull upstream fixes into the vendored copies
(dependabot has not produced viable PRs); the PMC would prefer to
establish one *(maintainer: DaanHoogland)*.
`OUT-OF-MODEL: unsupported-component` (with an upstream pointer).
9. **The four satellite repos** (`apache/cloudstack-cloudmonkey`,
`apache/cloudstack-go`, `apache/cloudstack-terraform-provider`,
@ -224,7 +240,7 @@ in-model only when it cleanly maps to one of them.
| B2 | Web UI → management server (`:8080`) | same as B1 plus session cookie | same as B1 |
| B3 | Browser → ConsoleProxyVM → hypervisor VNC socket | signed token issued by management server, embedded in URL; encrypted with `ConsoleProxyPasswordBasedEncryptor` *(documented: `server/src/main/java/com/cloud/servlet/ConsoleProxyServlet.java`, `ConsoleProxyPasswordBasedEncryptor.java`)* | implicit (signed-token possession) |
| B4 | Management server ↔ management server (cluster peers) | NIO + TLS, Root CA-issued certs *(documented: `framework/cluster/`, `framework/ca/`)* | peer-trust by valid cert |
| B5 | Management server → `cloudstack-agent` (KVM/Hyper-V/baremetal) | NIO + TLS on `:8250`; agent uses X.509 client cert issued by Root CA on first connect; cert provisioning is the `SetupKeyStoreCommand` shape *(documented: `agent/src/main/java/com/cloud/agent/Agent.java` `setupAgentKeystore`, `framework/ca/.../CAService.java`, `plugins/ca/root-ca/.../RootCAProvider.java`)*; trust strictness governed by `ca.plugin.root.auth.strictness` (**default `false`** — see §5a) and `ca.plugin.root.allow.expired.cert` (**default `true`** — see §5a) | peer-trust by valid cert |
| B5 | Management server → `cloudstack-agent` (KVM/baremetal) | NIO + TLS on `:8250`; agent uses X.509 client cert issued by Root CA on first connect; cert provisioning is the `SetupKeyStoreCommand` shape *(documented: `agent/src/main/java/com/cloud/agent/Agent.java` `setupAgentKeystore`, `framework/ca/.../CAService.java`, `plugins/ca/root-ca/.../RootCAProvider.java`)*; trust strictness governed by `ca.plugin.root.auth.strictness` (**default `true` for new setups; `false` only on upgrade from pre-Aug-2017 versions** — see §5a) and `ca.plugin.root.allow.expired.cert` | peer-trust by valid cert |
| B6 | Management server → external services (LDAP / SAML2 / OAuth2 IdP, NSX, Netscaler, Tungsten, S3, backup providers) | per-provider (service account, OAuth token, etc.) | external-service-side |
| B7 | Agent → hypervisor (libvirt / vSphere SDK / XenAPI) | local Unix socket (libvirt) or operator-supplied SDK credentials | hypervisor-side |
| B8 | Management server / agent → primary/secondary storage (NFS, RBD, iSCSI, S3) | OS-level (NFS), Ceph cephx, iSCSI CHAP, IAM key / static credential (S3) | storage-side |
@ -245,15 +261,20 @@ follows:
- **Cluster RPC (B4)**: reachable from a peer that has cleared the Root CA
trust check. A flat "cluster RPC has no auth" finding is `OUT-OF-MODEL:
adversary-not-in-scope` because the model *requires* the Root CA to be
enrolled across peers; a *cleartext* cluster RPC finding is gated by the
`ca.plugin.root.auth.strictness` default (see §5a, §14 Q12).
enrolled across peers; a *cleartext*/un-certed cluster RPC finding is
gated by `ca.plugin.root.auth.strictness`, which defaults to `true` on
new setups (see §5a).
- **Management ↔ agent (B5)**: reachable from a peer that presents a
Root-CA-signed certificate the management server accepts. Crucially, the
default of `ca.plugin.root.auth.strictness = false` means the management
server *does not require* a client certificate from the connecting agent
by default *(documented: `plugins/ca/root-ca/.../RootCAProvider.java`
line 132135; `RootCACustomTrustManager.java`)*; this is the highest-
leverage configuration default in the model.
Root-CA-signed certificate the management server accepts. By default on
new setups `ca.plugin.root.auth.strictness = true`, so the management
server **does require** a client certificate from the connecting agent
*(maintainer: vishesh92 —
`https://github.com/apache/cloudstack/pull/2239`)*. The value remains
`false` only when upgrading from versions released before Aug 2017 that
predate the setting; that upgrade case is documented in the upgrade
instructions and is therefore not a concern *(maintainer: DaanHoogland)*
*(documented: `plugins/ca/root-ca/.../RootCAProvider.java`,
`RootCACustomTrustManager.java`)*.
- **Console proxy (B3)**: reachable by anyone who holds a valid signed
token. The token is the entire authorization gate.
- **Agent → hypervisor (B7)**: reachable only on the agent host, by code
@ -267,7 +288,7 @@ follows:
- **Operating system (management server / usage server)**: RHEL 8/9/10,
CentOS 8/9, Rocky 9/10, Ubuntu 22.04/24.04, SUSE 15, openSUSE Leap 15;
Java 17 (`README.md`, `INSTALL.md`, `packaging/{el8,el9,el10,debian,suse15}`).
- **Operating system (agent)**: same family on KVM/Hyper-V/baremetal hosts;
- **Operating system (agent)**: same family on KVM/baremetal hosts;
agent ships as `cloudstack-agent` package *(documented: `debian/`,
`packaging/`)*.
- **Database**: MariaDB or MySQL-compatible, accessible from each
@ -322,30 +343,39 @@ security-relevant subset:
| Knob | Default | Maintainer stance | Effect |
| --- | --- | --- | --- |
| `ca.plugin.root.auth.strictness` | **`false`** *(documented: `RootCAProvider.java` line 132)* | **maintainer ruling required**: is the default a supported production posture or a dev-mode setting operators must flip per §10? *(inferred — §14 Q12)* | When `false`, the management server's `RootCACustomTrustManager` does **not** require a client certificate from a peer attempting to connect on `:8250` (agent port) or cluster ports. A peer without a cert is allowed in. |
| `ca.plugin.root.allow.expired.cert` | **`true`** *(documented: `RootCAProvider.java` line 138)* | **maintainer ruling required** *(inferred — §14 Q12)* | When `true`, an expired client cert is accepted during SSL handshake. |
| `ca.plugin.root.issuer.dn` | `CN=ca.cloudstack.apache.org` *(documented: same file line 128)* | configured at first management-server boot | Subject DN of the auto-generated self-signed Root CA. |
| `useforwardheader` (`use.forward.header`) | `false` *(inferred — §14 Q17)* | When `true`, the operator must restrict `proxy.forward.list` to the trusted reverse-proxy CIDR | When set, `ApiServlet.getClientAddress` honours `X-Forwarded-For` / configured headers *only* for source IPs in `proxy.forward.list` *(documented: `server/src/main/java/com/cloud/api/ApiServlet.java` lines 700725)*. |
| `proxy.forward.list` | unset *(inferred — §14 Q17)* | required when `useforwardheader=true` | CIDR list of trusted reverse proxies. |
| `ca.plugin.root.auth.strictness` | **`true` for new setups; `false` only on upgrade from pre-Aug-2017 versions** *(maintainer: vishesh92 — `https://github.com/apache/cloudstack/pull/2239`)* | New setups are strict by default; the `false`-on-upgrade case is called out in the upgrade instructions and is therefore not a concern *(maintainer: DaanHoogland)* | When `false`, the management server's `RootCACustomTrustManager` does **not** require a client certificate from a peer attempting to connect on `:8250` (agent port) or cluster ports. A peer without a cert is allowed in. |
| `ca.plugin.root.allow.expired.cert` | **`true`** *(documented: `RootCAProvider.java`)* | operational default to survive cert-rotation lag *(maintainer: paired with the strictness ruling above)* | When `true`, an expired client cert is accepted during SSL handshake. |
| `ca.plugin.root.issuer.dn` | `CN=ca.cloudstack.apache.org` *(documented: same file)* | configured at first management-server boot | Subject DN of the auto-generated self-signed Root CA. |
| `proxy.header.verify` | off by default *(inferred — §14 Q17; setting name maintainer: vishesh92)* | When on, the operator must restrict `proxy.cidr` to the trusted reverse-proxy CIDR | When set, `ApiServlet.getClientAddress` honours proxy-set forward headers *only* for source IPs in `proxy.cidr` *(documented: `server/src/main/java/com/cloud/api/ApiServlet.java` `getClientAddress`; setting name maintainer: vishesh92)*. |
| `proxy.header.names` | list of header names; semantics: names to check for allowed IP addresses from a proxy-set header *(maintainer: vishesh92)* | list of header names to consult for the allowed client address when set by a proxy | Names the request header(s) carrying the proxy-set client IP. |
| `proxy.cidr` | unset *(inferred — §14 Q17; setting name maintainer: vishesh92)* | required when `proxy.header.verify` is on | List of CIDRs for which `proxy.header.names` headers are honoured when the connecting `Remote_Addr` is in this list *(semantics maintainer: vishesh92)*. |
| `enable.2fa.for.users` / `enable.2fa.for.api` | per-domain toggle *(documented: `plugins/user-two-factor-authenticators/`)* | dev-test default off; production posture depends on PMC ruling *(inferred — §14 Q18)* | When on, users must complete static-pin or TOTP 2FA after login. |
| `security.encryption.key`, `security.encryption.iv` | auto-generated at first boot *(documented: `framework/security/.../KeysManager.java`)* | trusted secret | Base64-encoded JaSypt master key + IV used to encrypt application-level secrets in the DB. |
| `auth.password.algorithm` (`hash.user.password`) | bcrypt / pbkdf2 / sha256salted *(documented: `plugins/user-authenticators/{pbkdf2,sha256salted}`)* | **maintainer ruling required**: which is the supported default for new deployments? `md5` and `plain-text` plugins still ship *(documented: `plugins/user-authenticators/{md5,plain-text}`)* — are these legacy-compat-only or in supported production? *(inferred — §14 Q19)* | governs how user passwords are stored |
| `api.signature.version` | accepts both v1 and v3 *(documented: `ApiServer.java` line ~1053)* | v1 lacks an `expires` parameter; v3 requires expiration | A request with v3 + an expired `expires` is rejected; a v1 request without `expires` is accepted |
| `post.requests.and.timestamps.enforced` | per `isPostRequestsAndTimestampsEnforced` *(documented: `ApiServer.java` line ~1074)* | bounds `expires` to a maximum future offset | Prevents an attacker who steals a signed URL with a 10-year expiration from using it forever |
| `integration.api.port` (`:8096`) | typically disabled *(inferred — §14 Q20)* | When non-zero, exposes an *unauthenticated* admin API for integration testing | An open integration port is a complete RBAC bypass on the management server |
| Hypervisor enablement (which `plugins/hypervisors/*` are installed and configured) | per zone | operator-driven | An unused hypervisor plugin still ships but is not connected to any host |
| Hostname / SAN of management-server cert (`ca.plugin.root.management.cert.custom.san`) | unset *(inferred — §14 Q15)* | when set, included in the auto-generated cert SAN | governs which hostnames clients can use to reach the management server |
| SAML2 / OAuth2 enablement (`plugins/user-authenticators/{saml2,oauth2}`) | off *(inferred — §14 Q19)* | turning on adds an external IdP trust dependency | adds B6 transitions |
| LDAP enablement (`plugins/user-authenticators/ldap`) | off *(inferred — §14 Q19)* | turning on adds an external LDAP trust dependency | adds B6 transitions |
| `user.password.encoders.order` | `PBKDF2,SHA256SALT,MD5,LDAP,SAML2,PLAINTEXT` *(maintainer: vishesh92)* | first encoder in the order is used to hash new passwords; the list also defines the verification fall-through order | Governs how user passwords are stored and which encoders are accepted on verify. |
| `user.password.encoders.exclude` | `MD5,LDAP,PLAINTEXT` *(maintainer: vishesh92)* | excluded encoders are not used to (re)hash passwords | Excludes weak/legacy encoders from being chosen, even though they remain in the order list for verifying already-stored hashes. |
| `enforce.post.requests.and.timestamps` | per `isPostRequestsAndTimestampsEnforced` *(documented: `ApiServer.java`; setting name maintainer: vishesh92)* | bounds `expires` to a maximum future offset | Prevents an attacker who steals a signed URL with a 10-year expiration from using it forever. |
| `integration.api.port` (`:8096`) | typically disabled *(inferred — §14 Q20)* | When non-zero, exposes an *unauthenticated* admin API for integration testing | An open integration port is a complete RBAC bypass on the management server. |
| Hypervisor enablement (which `plugins/hypervisors/*` are installed and configured) | per zone | operator-driven | An unused hypervisor plugin still ships but is not connected to any host. |
| Hostname / SAN of management-server cert (`ca.framework.cert.management.custom.san`) | unset *(maintainer: vishesh92)* | when set, included in the auto-generated cert SAN | governs which hostnames clients can use to reach the management server. |
| SAML2 / OAuth2 enablement (`plugins/user-authenticators/{saml2,oauth2}`) | off *(inferred — §14 Q19)* | turning on adds an external IdP trust dependency | adds B6 transitions. |
| LDAP enablement (`plugins/user-authenticators/ldap`) | off *(inferred — §14 Q19)* | turning on adds an external LDAP trust dependency | adds B6 transitions. |
**The insecure-default case (highest leverage).** `ca.plugin.root.auth.strictness`
defaulting to `false` and `ca.plugin.root.allow.expired.cert` defaulting to
`true` are the two highest-leverage defaults in the entire model. Whether a
report against an open `:8250` accepting an un-certed peer is `VALID` or
`OUT-OF-MODEL: non-default-build` depends on the maintainer ruling in
§14 Q12. The text of §3 item 1, §10, and §11a assume the answer
is **"dev/test default, operator must flip both knobs to `true` /
`false` respectively per §10 for production"**.
**The Root-CA strictness default (resolved).** Earlier drafts treated
`ca.plugin.root.auth.strictness = false` as the shipped default and the
single highest-leverage open question. The PMC has clarified that **new
setups default to `true`** — the management server *does* require a
Root-CA-signed client cert on `:8250` and the cluster ports — and the
value is `false` **only** when upgrading from versions released before
Aug 2017 that predate the setting *(maintainer: vishesh92 —
`https://github.com/apache/cloudstack/pull/2239`)*. That upgrade case is
documented in the upgrade instructions, so a leftover `false` after such
an upgrade is an operator-hardening/upgrade-hygiene item, not a shipped
insecure default *(maintainer: DaanHoogland)*. A report against an open
`:8250` accepting an un-certed peer on a **new** install is therefore
`MODEL-GAP`/`VALID` (strictness should be on), whereas the same on an
**upgraded** pre-2017 install is `OUT-OF-MODEL: non-default-build`
(documented upgrade step not applied). `ca.plugin.root.allow.expired.cert`
remains `true` as an operational concession to cert-rotation lag.
## §6 Assumptions about inputs
@ -355,14 +385,14 @@ is **"dev/test default, operator must flip both knobs to `true` /
| --- | --- | --- | --- |
| Management server `:8080`/`:8443` JSON API | command name + params | **yes** | nothing — CloudStack parses, authenticates (B1), applies RBAC, dispatches |
| Management server `:8080`/`:8443` JSON API | `signature` parameter | **yes** | HMAC-SHA1 verified *constant-time* against expected signature *(documented: `ApiServer.java` line 1137 `ConstantTimeComparator.compareStrings`)* |
| Management server `:8080`/`:8443` JSON API | `expires` parameter (sig v3) | **yes** | rejected if past, or beyond `post.requests.and.timestamps.enforced` ceiling *(documented: same file)* |
| Management server `:8080`/`:8443` JSON API | `X-Forwarded-For` and other configured forward headers | **yes** if `useforwardheader=true` | honoured **only** if request source IP ∈ `proxy.forward.list` *(documented: `ApiServlet.java` line 712)* |
| Management server `:8080`/`:8443` JSON API | `expires` parameter (sig v3) | **yes** | rejected if past, or beyond the `enforce.post.requests.and.timestamps` ceiling *(documented: same file; setting name maintainer: vishesh92)* |
| Management server `:8080`/`:8443` JSON API | proxy-set forward headers (`proxy.header.names`) | **yes** if `proxy.header.verify=true` | honoured **only** if the connecting `Remote_Addr``proxy.cidr` *(documented: `ApiServlet.java` `getClientAddress`; setting names maintainer: vishesh92)* |
| Management server `:8080`/`:8443` Web UI | session cookie | **yes** | session-fixation / invalidation handled via `invalidateHttpSession` on auth failure *(documented: `ApiServlet.java` line 418)* |
| Integration API `:8096` (if enabled) | command name + params | **yes** | **no signature check** — integration port is unauthenticated by design |
| Management ↔ agent `:8250` | NIO Thrift-like payload | **only by a peer that has cleared B5 trust** | client cert via `RootCACustomTrustManager` |
| Management ↔ cluster peer | NIO payload | **only by a peer that has cleared B4 trust** | client cert via `RootCACustomTrustManager` |
| Console proxy URL | encrypted token (containing VM identity + endpoint + duration) | **yes** | token MUST decrypt + verify with `ConsoleProxyPasswordBasedEncryptor` keys *(documented: `ConsoleProxyPasswordBasedEncryptor.java`)* |
| Secondary-storage HTTP download URL | path + signed parameters | **yes** | token / per-template ACL check *(inferred — §14 Q4)* |
| Secondary-storage HTTP download URL | UUID-named symlink path | **yes** | **no auth on the download link**; the UUID format is the anti-enumeration control and the symlink is removed after a period — timed availability of the download token is the mitigation *(maintainer: vishesh92, DaanHoogland)* |
| Template / ISO upload | URL of remote source | **yes** within RBAC | upload-gated by `registerTemplate` RBAC; bytes are then served to hypervisors as image data |
| User-data / metadata service (`169.254.169.254` from inside guests) | guest-controlled bytes (the request) | **yes from the guest**, but the service is reached *from the guest* and serves only that guest's data | guest-VM-side isolation by virtual router |
| Hypervisor agent log / event stream | bytes from hypervisor | trusted operator surface | none — assumed truthful |
@ -390,7 +420,7 @@ is **"dev/test default, operator must flip both knobs to `true` /
| Actor | In scope? | Capabilities granted |
| --- | --- | --- |
| Unauthenticated network peer reaching `:8080`/`:8443` | **yes** | TCP to the listening ports; may attempt authentication; may attempt to violate the protocol pre-auth |
| Unauthenticated peer reaching `:8250` (agent port) | **yes** *if* `ca.plugin.root.auth.strictness` is at default `false` (§5a) | TCP to the listening port; may attempt to connect as a peer without presenting a cert |
| Unauthenticated peer reaching `:8250` (agent port) | **only if** `ca.plugin.root.auth.strictness = false`, which on new setups it is **not** (default `true`); `false` arises only on un-remediated pre-Aug-2017 upgrades (§5a) | TCP to the listening port; may attempt to connect as a peer without presenting a cert |
| Unauthenticated peer reaching `:8096` (integration port) | **yes** *if* the port is open (typically not in production) | full unauthenticated admin API |
| Authenticated end user with limited RBAC role | **yes** | call APIs their role permits; manage VMs/networks/storage in their domain/account/project |
| Authenticated end user with broad RBAC role | partial | only RBAC-envelope escapes are in scope |
@ -402,7 +432,7 @@ is **"dev/test default, operator must flip both knobs to `true` /
| Operator | **out of scope** | see §3 item 1 |
| Hostile hypervisor | **out of scope** | see §3 item 3 |
| Hostile LDAP / SAML / OAuth IdP, hostile NSX/Netscaler/Tungsten, hostile S3 endpoint | **out of scope** | see §3 item 2 |
| Reverse proxy that should be trusted but is not in `proxy.forward.list` | **out of scope** | its forward headers are not honoured |
| Reverse proxy that should be trusted but is not in `proxy.cidr` | **out of scope** | its forward headers are not honoured |
| Local process on the management-server host running as a different UID | **partial** *(inferred — §14 Q24)* | same-host attackers with non-cloudstack UID can reach `:8080` unless host firewalling forbids; CloudStack does not defend against same-host `root` |
| Side-channel observer (cache timing, network timing, hypervisor side channels) | **out of scope** *(inferred — §14 Q25)* | n/a |
| Quantum adversary | **out of scope** | n/a |
@ -469,26 +499,29 @@ For each property: condition, violation symptom, severity tier, provenance.
### P5 — Mutual TLS on management ↔ agent, management ↔ cluster peer, *when configured*
- **Condition**: `ca.plugin.root.auth.strictness = true` and
`ca.plugin.root.allow.expired.cert = false` *(documented:
`RootCAProvider.java` lines 132142)*. Without these (i.e., at default)
the property is voided per §5a / §14 Q12; the management server still
accepts cleartext-/un-certed peers.
- **Violation symptom**: a peer without a Root-CA-issued cert (or with an
expired one) successfully completes a session on `:8250` or the
cluster port despite both flags being flipped to strict.
- **Condition**: `ca.plugin.root.auth.strictness = true` — **the default
on new setups** *(maintainer: vishesh92 —
`https://github.com/apache/cloudstack/pull/2239`)*. Pre-Aug-2017
upgrades may leave it `false` until the documented upgrade step is
applied *(maintainer: DaanHoogland)*. `ca.plugin.root.allow.expired.cert`
remains `true` (cert-rotation concession), so the property covers
*peer-cert presence and Root-CA chain*, not cert freshness.
- **Violation symptom**: a peer without a Root-CA-issued cert successfully
completes a session on `:8250` or the cluster port on a setup where
strictness is on.
- **Severity**: **security-critical**, `VALID` per §13.
- *(documented for the strict configuration; the default-state coverage
depends on the §14 Q12 ruling.)*
- *(documented; default resolved by maintainer.)*
### P6 — Reverse-proxy IP-trust gating for forward headers
- **Condition**: `useforwardheader = true` *(inferred — §14 Q17)*; only
source IPs in `proxy.forward.list` have their `X-Forwarded-For` (or
configured equivalent) header consulted *(documented: `ApiServlet.java`
line 712 `NetUtils.isIpInCidrList`)*.
- **Condition**: `proxy.header.verify` on *(inferred — §14 Q17; setting
name maintainer: vishesh92)*;
only requests whose `Remote_Addr` falls in `proxy.cidr` have their
`proxy.header.names` forward header(s) consulted *(documented:
`ApiServlet.java` `getClientAddress` `NetUtils.isIpInCidrList`; setting
names maintainer: vishesh92)*.
- **Violation symptom**: a request from a source IP **outside**
`proxy.forward.list` succeeds with an attacker-supplied forward header
`proxy.cidr` succeeds with an attacker-supplied forward header
taking effect.
- **Severity**: **security-critical**, `VALID` per §13.
- *(documented)*
@ -604,8 +637,10 @@ disclaimer.
- **`ca.plugin.root.auth.strictness = false` is not "TLS off" — it is
"client cert not required"** *(documented: `RootCAProvider.java`)*. TLS
on the wire is still there; what is missing is the peer-cert check.
A scanner that flags "client cert not requested" is *correctly*
identifying a §5a knob default, not a transport-encryption bug.
Note the value is `true` on new setups *(maintainer: vishesh92)*; a
scanner that flags "client cert not requested" is only correct on an
un-remediated pre-Aug-2017 upgrade, and even then it identifies a
documented upgrade step, not a transport-encryption bug.
- **`ca.plugin.root.allow.expired.cert = true` is the operational default
to survive cert-rotation lag** but is not a security boundary.
- **The HMAC-SHA1 signature is request-integrity over the URL, not
@ -644,10 +679,13 @@ disclaimer.
The operator deploying CloudStack in production **must**:
1. Set `ca.plugin.root.auth.strictness = true` and
`ca.plugin.root.allow.expired.cert = false`. Without these, agent and
cluster-peer ports accept peers without a cert / with expired certs
*(documented: `RootCAProvider.java`; pending §14 Q12 ruling)*.
1. Keep `ca.plugin.root.auth.strictness = true` (the default on new
setups). When **upgrading from a pre-Aug-2017 version**, follow the
documented upgrade step to turn strictness on — otherwise agent and
cluster-peer ports accept peers without a cert *(maintainer: vishesh92,
DaanHoogland — `https://github.com/apache/cloudstack/pull/2239`)*.
Consider tightening `ca.plugin.root.allow.expired.cert` (default `true`)
once cert rotation is reliable.
2. Restrict the management network at L2/L3 so that `:8250` (agent),
`:9090` (cluster), and the MariaDB port are reachable only from the
intended peers *(inferred — §14 Q13)*.
@ -656,16 +694,22 @@ The operator deploying CloudStack in production **must**:
4. Terminate TLS for the JSON API and Web UI on `:8443` (not `:8080`); if
`:8080` is exposed at all, only behind a TLS-terminating reverse
proxy *(inferred — §14 Q32)*.
5. When using a reverse proxy, set `useforwardheader = true` *and*
`proxy.forward.list` to the proxy's CIDR — failing to set
`proxy.forward.list` means the header is ignored (safe-default per P6),
but a misconfigured wide CIDR is a trust-bypass.
5. When using a reverse proxy, set `proxy.header.verify = true`,
`proxy.header.names` to the forward header(s) the proxy sets, *and*
`proxy.cidr` to the proxy's CIDR — leaving `proxy.cidr` unset/empty
means the header is ignored (safe-default per P6), but a misconfigured
wide CIDR is a trust-bypass *(setting names maintainer: vishesh92)*.
6. Protect the `security.encryption.key` / `security.encryption.iv`
files, the JaSypt-encrypted DB, the Root CA private key, and the
`cloudstack-management` Unix user's home directory at OS level.
7. Configure a password authenticator from the supported set (bcrypt /
pbkdf2 / sha256salted) — **not** `md5` or `plain-text` *(pending §14
Q19 ruling on whether the legacy plugins are supported)*.
7. Keep the password-encoder configuration at safe defaults:
`user.password.encoders.order` defaults to
`PBKDF2,SHA256SALT,MD5,LDAP,SAML2,PLAINTEXT` (so PBKDF2 is used to hash
new passwords) and `user.password.encoders.exclude` defaults to
`MD5,LDAP,PLAINTEXT` (so the weak encoders are not chosen for hashing,
only retained for verifying already-stored hashes) *(maintainer:
vishesh92)*. Do not remove `MD5`/`PLAINTEXT` from the exclude list in
production *(pending §14 Q19 ruling on the supported greenfield set)*.
8. Enable 2FA (`totp` or `static-pin`) for administrators and ideally for
all users *(pending §14 Q18 ruling)*.
9. Rotate per-user API secret keys on personnel change and on suspected
@ -685,23 +729,27 @@ The operator deploying CloudStack in production **must**:
## §11 Known misuse patterns
- **Leaving `:8250` open to the world with `ca.plugin.root.auth.strictness=false`.**
Any peer can connect as an agent on the default. Pending §14 Q12,
this is either a `VALID` operator-hardening report or `OUT-OF-MODEL:
non-default-build`.
- **Leaving `:8250` open to the world with `ca.plugin.root.auth.strictness=false`
on an upgraded pre-Aug-2017 cluster.** New setups default to `true`;
the `false` value only survives an upgrade where the documented step
was skipped *(maintainer: vishesh92, DaanHoogland)*. In that state any
peer can connect as an agent — an upgrade-hygiene gap, dispositioned
`OUT-OF-MODEL: non-default-build` (documented upgrade step not applied).
- **Exposing `:8096` (integration API) publicly.** Anyone reaching the
port executes admin API commands without auth.
- **Exposing `:8080` (HTTP JSON API) publicly without a TLS-terminating
reverse proxy.** Signed-request integrity holds, but the API secret-
key-derived signature is visible to any wire observer; replay within
the `expires` window is trivial.
- **Setting `useforwardheader=true` with `proxy.forward.list` wider than
- **Setting `proxy.header.verify=true` with `proxy.cidr` wider than
the actual reverse-proxy CIDR.** An attacker outside the proxy can
spoof `X-Forwarded-For` and claim any IP address for audit logs and
authentication-IP checks.
- **Using the `md5` or `plain-text` user authenticator plugin in
production.** Both still ship in `plugins/user-authenticators/`. Pending
§14 Q19 on whether they are legacy-compat-only.
spoof a `proxy.header.names` header and claim any IP address for audit
logs and authentication-IP checks *(setting names maintainer: vishesh92)*.
- **Removing `MD5`/`PLAINTEXT` from `user.password.encoders.exclude` (or
reordering them to the front of `user.password.encoders.order`) in
production.** The encoders ship for verifying legacy hashes; promoting
them to hash new passwords stores weakly-protected credentials
*(maintainer: vishesh92)*. Pending §14 Q19 on the greenfield-supported set.
- **Granting domain admin to too many users.** A domain admin can manage
all accounts within the domain — including reading guest console URLs.
- **Embedding console-proxy URLs in screenshots, ticketing systems, or
@ -709,9 +757,10 @@ The operator deploying CloudStack in production **must**:
- **Reusing `security.encryption.key` across environments of different
trust levels.** A staging-env leak becomes a production-env decrypt
primitive *(inferred — §14 Q33)*.
- **Disabling the cluster-peer TLS by leaving `ca.plugin.root.auth.strictness`
default in a multi-management-server deployment.** A peer can join the
cluster without a cert.
- **Leaving `ca.plugin.root.auth.strictness=false` after a pre-Aug-2017
upgrade in a multi-management-server deployment.** A peer can join the
cluster without a cert until the documented upgrade step flips it to the
new-setup default of `true` *(maintainer: vishesh92, DaanHoogland)*.
- **Uploading large or pathological templates and relying on hypervisor
to enforce size.** Per-account resource limits, not the engine, are the
enforcement.
@ -722,12 +771,15 @@ This section is the highest-leverage input for automated agentic security
scans. Each entry: tool symptom, why it is safe under the model, the §
that licenses the call.
- **"Management ↔ agent port `:8250` accepts plaintext / no client cert"
against a default `ca.plugin.root.auth.strictness=false`.** This is the
*documented default*. Pending §14 Q12, this is either `OUT-OF-MODEL:
non-default-build` (operator must flip) or `VALID-HARDENING` (default
should change). The model assumes the former until the PMC rules
otherwise.
- **"Management ↔ agent port `:8250` accepts no client cert" reported
against a setup with `ca.plugin.root.auth.strictness=false`.** New setups
default to `true` and **do** require a Root-CA-signed client cert
*(maintainer: vishesh92 — `https://github.com/apache/cloudstack/pull/2239`)*.
The value is `false` only on an upgrade from a pre-Aug-2017 version that
predates the setting, and the upgrade instructions document turning it on
*(maintainer: DaanHoogland)*. → On a new install: `KNOWN-NON-FINDING`
(strictness is on). On an upgraded install with the step skipped:
`OUT-OF-MODEL: non-default-build` (documented upgrade step not applied).
- **"Integration port `:8096` is unauthenticated."** The port is
unauthenticated by design; operator responsibility per §10 to close /
bind to localhost. → `OUT-OF-MODEL: non-default-build` once the PMC
@ -745,8 +797,9 @@ that licenses the call.
first boot per `RootCAProvider.java`. Browsers will warn until the
operator bootstraps trust. → `BY-DESIGN: property-disclaimed`.
- **"Expired agent cert is accepted (`ca.plugin.root.allow.expired.cert=true`)."**
Documented default. Pending §14 Q12. → `OUT-OF-MODEL:
non-default-build` once the PMC confirms.
Documented default — an operational concession to cert-rotation lag, paired
with the strictness default *(maintainer: vishesh92, DaanHoogland)*. →
`VALID-HARDENING` at most; tightening it is an operator choice per §10.
- **"Hardcoded password / keytab in `tools/marvin/`, `test/`, `developer/`,
`quickcloud/`."** These directories are unsupported components per §3
item 7. → `OUT-OF-MODEL: unsupported-component`.
@ -764,12 +817,22 @@ that licenses the call.
resource limits are the bound. → `VALID-HARDENING` at most, unless the
decompression reaches §8 P9 memory-safety violations.
- **"Vendored Bouncy Castle / JaSypt / noVNC / `pako` has CVE-X."** Report
upstream; CloudStack picks up the fix on the next vendored sync. →
`OUT-OF-MODEL: unsupported-component` (upstream pointer) *(inferred —
§14 Q5)*.
- **"`X-Forwarded-For` is honoured without authentication."** Honoured
only if (a) `useforwardheader=true` *and* (b) source IP ∈
`proxy.forward.list`. → `KNOWN-NON-FINDING`.
upstream; `systemvm/agent/noVNC` is a vendored fork of
`github.com/novnc/novnc` with CloudStack changes, and there is no
automated sync procedure today *(maintainer: vishesh92, DaanHoogland)*. →
`OUT-OF-MODEL: unsupported-component` (upstream pointer); a
CloudStack-introduced change *to* the fork is in-model.
- **"Secondary-storage download URL has no authentication / can be replayed."**
By design: download links are UUID-named symlinks served by an Apache
httpd with no auth on the link; the UUID format defeats enumeration and
the symlink is removed after a period, so timed availability is the
mitigation *(maintainer: vishesh92, DaanHoogland)*. → `BY-DESIGN:
property-disclaimed` for the no-auth aspect; a link that is *not* removed
after its window, or a guessable (non-UUID) name, is `VALID-HARDENING`.
- **"A proxy-set forward header is honoured without authentication."**
Honoured only if (a) `proxy.header.verify=true`, (b) the header is one of
`proxy.header.names`, *and* (c) the connecting `Remote_Addr`
`proxy.cidr` *(setting names maintainer: vishesh92)*. → `KNOWN-NON-FINDING`.
- **"Session-fixation: a session ID is reusable after failed login."**
`invalidateHttpSession` is called on each auth failure path per
`ApiServlet.java`. → `KNOWN-NON-FINDING` (verify the symptom; if
@ -809,7 +872,7 @@ following:
| `OUT-OF-MODEL: trusted-input` | Requires attacker control of a §6 parameter the model marks trusted (e.g. operator-supplied config flag, hostile LDAP/SAML/NSX/etc.). | §6 |
| `OUT-OF-MODEL: adversary-not-in-scope` | Requires a §7 actor the model excludes (operator, hostile hypervisor, hostile external IdP / SDN, Byzantine peer, side-channel observer, same-host non-`cloudstack` `root`). | §7 |
| `OUT-OF-MODEL: unsupported-component` | Lands in `tools/marvin/`, `test/`, `developer/`, `quickcloud/`, vendored upstream code, `simulator` hypervisor, etc. | §3 items 78 |
| `OUT-OF-MODEL: non-default-build` | Only manifests under a §5a flag the maintainer has ruled is dev/test (e.g. `ca.plugin.root.auth.strictness=false` if so ruled, integration port `:8096` open). | §5a |
| `OUT-OF-MODEL: non-default-build` | Only manifests under a §5a flag that is not the new-setup default (e.g. `ca.plugin.root.auth.strictness=false` surviving an un-remediated pre-Aug-2017 upgrade, integration port `:8096` open). | §5a |
| `OUT-OF-MODEL: equivalent-harm` | An actor already-authorized under the model can cause the same harm via a documented path (root admin doing root-admin things, RBAC-licensed user using their RBAC-licensed commands). | §3 items 4, 5 |
| `BY-DESIGN: property-disclaimed` | Concerns a §9 property the project explicitly does not provide (template sandboxing, side-channel resistance, hypervisor isolation, etc.). | §9 |
| `KNOWN-NON-FINDING` | Matches a §11a recurring false positive. | §11a |
@ -822,14 +885,17 @@ are inline; please confirm, correct, or strike.
### Wave 1 — scope, intended use, the two big insecure defaults
**Q1.** The model assumes CloudStack is "a clustered distributed
**Q1.** ~~The model assumes CloudStack is "a clustered distributed
control plane deployed inside an operator-controlled datacenter
network", not a single-host appliance or a hosted SaaS. Confirm? *(maps
to §2)*
network", not a single-host appliance or a hosted SaaS. Confirm?~~
**RESOLVED** *(maintainer: DaanHoogland)* — distributed control plane;
**both** a single management-server instance (smaller clouds) and a
clustered deployment are supported topologies. Folded into §2.
**Q2.** Are the SecondaryStorageVM, ConsoleProxyVM, and Virtual Router
treated as trusted-once-enrolled peers (proposed: **yes**, same shape as
agents), or do they get their own trust tier? *(maps to §2, §4 B5)*
**Q2.** ~~Are the SecondaryStorageVM, ConsoleProxyVM, and Virtual Router
treated as trusted-once-enrolled peers, or do they get their own trust
tier?~~ **RESOLVED** *(maintainer)***yes**, same trust tier as agents,
not a separate tier. Folded into §2 caller-roles.
**Q3.** Are external integrations (LDAP, SAML2 IdP, OAuth2 IdP, NSX
controller, Netscaler, Tungsten, S3-compatible storage, backup
@ -837,14 +903,23 @@ providers) modeled as trusted control-plane peers (proposed: **yes**)? If
trusted, that licenses §3 item 2 and §11a trusted-input dispositions.
*(maps to §2, §3, §11a)*
**Q4.** SecondaryStorageVM HTTP download surface — is the URL token
per-template ACL-checked, or is the SSVM URL itself a bearer credential
that any holder can replay? *(maps to §6, §11a)*
**Q4.** ~~SecondaryStorageVM HTTP download surface — is the URL token
per-template ACL-checked, or is the SSVM URL itself a bearer credential?~~
**RESOLVED** *(maintainer: vishesh92, DaanHoogland)* — download links are
UUID-named symlinks served by an Apache httpd with **no auth on the link**;
the UUID format defeats enumeration and the symlink is removed after a
period (timed availability is the mitigation). The PMC noted this should
be re-tested/confirmed in code. Folded into §6, §11a. *(Daan also asked
why static code analysis did not surface this — a note for the scan
agent, not a model gap.)*
**Q5.** Vendored upstream code under `systemvm/agent/noVNC/vendor/pako`
and bundled JaSypt / Bouncy Castle / JSch — is the policy "report
upstream; we pick up fixes on next sync" (proposed)? *(maps to §3 item 8,
§11a)*
**Q5.** ~~Vendored upstream code under `systemvm/agent/noVNC` and bundled
JaSypt / Bouncy Castle / JSch — is the policy "report upstream; we pick up
fixes on next sync"?~~ **RESOLVED** *(maintainer: vishesh92, DaanHoogland)*
`systemvm/agent/noVNC` is a **vendored fork of `github.com/novnc/novnc`**
with CloudStack changes; vendored bugs go upstream. There is **no automated
update procedure today** (dependabot has not produced viable PRs); the PMC
would prefer to establish one. Folded into §3 item 8, §11a.
**Q6.** Is "an operator with `root` on a management-server host, the
JCEKS keystore + encryption keys, the Root CA private key, or MariaDB
@ -857,24 +932,23 @@ KVM/QEMU itself) — out of scope, report upstream (proposed)? *(maps to
### Wave 2 — the two big insecure defaults
**Q12.** **Highest-leverage question in the model.** Two Root-CA defaults:
**Q12.** ~~**Highest-leverage question in the model.** Are
`ca.plugin.root.auth.strictness` and `ca.plugin.root.allow.expired.cert`
shipped insecure-by-default?~~ **RESOLVED** *(maintainer: vishesh92,
DaanHoogland — `https://github.com/apache/cloudstack/pull/2239`)*:
- `ca.plugin.root.auth.strictness` = `false` *(documented:
`RootCAProvider.java` line 132)* — the management server does **not**
require a client cert from peers on `:8250` (agent port) and cluster
ports by default. Is this **(a)** the supported production posture (so
a report of "agent port accepts un-certed peer" is `VALID` against the
default, meaning the default should be flipped), or **(b)** a dev/test
convenience that operators are documented as required to flip per
§10 (so the report is `OUT-OF-MODEL: non-default-build`)?
- `ca.plugin.root.allow.expired.cert` = `true` *(documented: same file
line 138)* — expired client certs are accepted. Same question.
- `ca.plugin.root.auth.strictness` defaults to **`true` on new setups** —
the management server **does** require a Root-CA-signed client cert on
`:8250` and the cluster ports. It is `false` **only** after upgrading
from a version released before Aug 2017 that predates the setting; the
upgrade instructions document turning it on, so a leftover `false` is an
upgrade-hygiene gap, not a shipped insecure default.
- `ca.plugin.root.allow.expired.cert` defaults to `true` as an operational
concession to cert-rotation lag.
This single ruling reshapes §3 item 1, §5a, §7 (the un-certed peer
row), §8 P5, §10, §11 first/penultimate bullets, §11a first two
bullets, and §13. The text of §3 / §10 / §11a in this draft
**assumes the answer is (b)** — operator must flip both per §10.
*(maps to §5a, §10, §11a, §13)*
This resolution reshaped §3 item 1, §5a, §7 (the un-certed peer row),
§8 P5, §9 false-friends, §10, §11, §11a, and §13. The earlier
"assumes operator must flip per §10" framing is withdrawn.
### Wave 3 — adjacent insecure defaults and admin-only surfaces
@ -901,23 +975,29 @@ a code-execution channel into the guest (proposed)? *(maps to §3 item 6,
`simulator` hypervisor plugin. Anything to add or remove? *(maps to §3
item 7)*
**Q17.** `useforwardheader` (`use.forward.header`) default — proposed:
**`false`** by default; operator turns it on only when a reverse proxy
is in front. Confirm, and confirm `proxy.forward.list` is required when
the flag is on. *(maps to §5a, §6, §10)*
**Q17.** Forward-header gating — the **setting names are confirmed**
*(maintainer: vishesh92)*: `proxy.header.verify` (the on/off gate),
`proxy.header.names` (header names to consult), and `proxy.cidr` (CIDRs of
the `Remote_Addr` values for which those headers are honoured). **Still
open:** confirm `proxy.header.verify` is **off by default** and that
`proxy.cidr` must be set for the headers to take effect. *(maps to §5a,
§6, §10)*
**Q18.** 2FA — proposed: off by default, operator turns it on per
domain / per user via `enable.2fa.*`. Confirm; and is "2FA disabled in
production" a §10 violation or a deployment choice? *(maps to §5a,
§10)*
**Q19.** User-authenticator plugins — the repo still ships
`plain-text`, `md5`, `sha256salted`, `pbkdf2`, plus `ldap`, `saml2`,
`oauth2`. Which are supported for new production deployments? Proposed:
`pbkdf2` and `sha256salted` are supported; `md5` and `plain-text` are
legacy-compat for upgrade paths only and a report against them in a
greenfield install is `OUT-OF-MODEL: non-default-build`. Confirm.
*(maps to §5a, §10, §11)*
**Q19.** User-authenticator plugins — encoder selection is governed by
`user.password.encoders.order` (default
`PBKDF2,SHA256SALT,MD5,LDAP,SAML2,PLAINTEXT`) and
`user.password.encoders.exclude` (default `MD5,LDAP,PLAINTEXT`), so PBKDF2
is the effective hashing default and `MD5`/`PLAINTEXT` are retained only
for verifying legacy hashes *(maintainer: vishesh92)*. **Still open:**
confirm that a report against `md5`/`plain-text` being *used to hash new
passwords* in a greenfield install is `OUT-OF-MODEL: non-default-build`
(they are excluded by default), and confirm the supported greenfield
encoder set. *(maps to §5a, §10, §11)*
**Q20.** Integration API port `:8096` — proposed: closed (port-zero) by
default in production packaging, open only when explicitly configured;
@ -1047,13 +1127,13 @@ landing page. The de facto security-policy artifacts are scattered:
| `README.md` "Reporting Security Vulnerabilities" | report to `security@apache.org`; canonical page at `cloudstack.apache.org/security.html` | §1 reporting cross-reference |
| `README.md` "Notice of Cryptographic Software" | JaSypt, Bouncy Castle, JSch, OpenSwan, MySQL native encryption | §5 cryptography assumption, §8 P8 |
| `agent/conf/agent.properties` (`host`, `port`, `ssl.handshake.timeout`, …) | agent ↔ management server transport on `:8250` | §2 component table, §4 B5 |
| `server/src/main/java/com/cloud/api/ApiServer.java` `verifyRequest` (lines ~9801156) | HMAC-SHA1 signature + `expires` enforcement + constant-time compare | §8 P1, §8 P3, §5a "api.signature.version", §11a "SHA1 / constant-time" entries |
| `server/src/main/java/com/cloud/api/ApiServlet.java` `getClientAddress` (lines 700725) | forward-header gating by `proxy.forward.list` | §8 P6, §5a "useforwardheader" row |
| `server/src/main/java/com/cloud/api/ApiServer.java` `verifyRequest` (lines ~9801156) | HMAC-SHA1 signature + `expires` enforcement (`enforce.post.requests.and.timestamps`) + constant-time compare | §8 P1, §8 P3, §5a "enforce.post.requests.and.timestamps" row, §11a "SHA1 / constant-time" entries |
| `server/src/main/java/com/cloud/api/ApiServlet.java` `getClientAddress` (lines 700725) | forward-header gating by `proxy.cidr` / `proxy.header.names` when `proxy.header.verify=true` | §8 P6, §5a "proxy.header.verify" row |
| `server/src/main/java/com/cloud/api/ApiServlet.java` 2FA path (lines 360582) | password + 2FA flow | §8 P2 |
| `framework/ca/.../CAService.java`, `plugins/ca/root-ca/.../RootCAProvider.java` | Root CA generated at first boot; agent enrolment via `SetupKeyStoreCommand` | §4 B5, §8 P5, §5a strictness/allow-expired rows |
| `plugins/ca/root-ca/.../RootCACustomTrustManager.java` | `authStrictness` and `allowExpiredCertificate` semantics | §5a, §8 P5 |
| `plugins/acl/{static,dynamic,project}-role-based` | RBAC backends | §8 P4 |
| `plugins/user-authenticators/{md5,sha256salted,pbkdf2,plain-text,ldap,saml2,oauth2}` | pluggable user auth | §2 caller-roles row, §5a "auth.password.algorithm", §10 item 7 |
| `plugins/user-authenticators/{md5,sha256salted,pbkdf2,plain-text,ldap,saml2,oauth2}` | pluggable user auth; selection via `user.password.encoders.order` / `user.password.encoders.exclude` | §2 caller-roles row, §5a "user.password.encoders.*" rows, §10 item 7 |
| `plugins/user-two-factor-authenticators/{static-pin,totp}` | 2FA backends | §5a "enable.2fa.*", §10 item 8 |
| `framework/security/.../KeysManager.java`, `KeystoreManager.java` | `security.encryption.key`, `security.encryption.iv` (Hidden), application-secret JaSypt encryption | §8 P8, §5a, §10 item 6 |
| `agent/src/main/java/com/cloud/agent/Agent.java` `setupAgentKeystore` (lines ~793916) | agent receives Root CA-signed cert via `SetupKeyStoreCommand` and imports it | §4 B5, §8 P5 |