Add draft project security threat-model document#13293
Conversation
Adds a draft project-level security threat-model document (draft-THREAT-MODEL.md) at repo root, improving discoverability for automated security scanners running against this repository. The file follows the rubric format used by several other ASF projects piloting security-model discoverability. The "draft-" prefix signals this is a proposal for the PMC to review, correct, or reject — not a finalised maintainer-blessed model. Every claim carries a provenance tag (documented / inferred / maintainer) so reviewers can see where each claim originates; §14 collects open questions for the maintainers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #13293 +/- ##
============================================
- Coverage 18.10% 18.10% -0.01%
+ Complexity 16752 16749 -3
============================================
Files 6037 6037
Lines 542796 542796
Branches 66456 66456
============================================
- Hits 98291 98267 -24
- Misses 433460 433488 +28
+ Partials 11045 11041 -4
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Markdown / typos / table-shape fixes per the CI lint output. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
There's a lot of details in the draft that needs a better set of eyes, so assigning @DaanHoogland @vishesh92 who're also PMC leads on the work. |
| **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)* |
There was a problem hiding this comment.
@potiuk , not sure if I should answer you but, the control plane could be clustered but for smaller clouds it can be a single instance of the management server. I am not sure if that is relevant for the question and I am not sure this is the proper way to answer the question.
| **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)* |
There was a problem hiding this comment.
yes, for as far as they are pointing inwards. they do have for some purposed outside interfaces which are supposed to be safe.
| one or more management-server instances (clustered behind a load balancer | ||
| in production), 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 |
There was a problem hiding this comment.
| per-hypervisor-host `cloudstack-agent` (for KVM/Hyper-V/baremetal) or | |
| per-hypervisor-host `cloudstack-agent` (for KVM//baremetal) or |
| **Q3.** Are external integrations (LDAP, SAML2 IdP, OAuth2 IdP, NSX | ||
| controller, Netscaler, Tungsten, S3-compatible storage, backup | ||
| 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)* |
| **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)* |
There was a problem hiding this comment.
We have no procedure to implement this yet (dependabot is not producing viable PRs for us) I would prefer we do. Any ideas @vishesh92 ?
There was a problem hiding this comment.
For noVNC, we are cloning the github.com/novnc/novnc and making some changes on top of it for cloudstack requirements.
| | 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 | |
There was a problem hiding this comment.
default value for ca.plugin.root.auth.strictness is true for newer setups but for older setups, it remains false even after upgraded.
There was a problem hiding this comment.
ok, so if it is mentioned in the upgrade instructions it is not a concern.
|
|
||
| **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)* |
There was a problem hiding this comment.
I think the only mitigation to unauthorised use is timed availability of the download(-token) Not sure how to respond to this. We’ll have to make sure/test this. cc @vishesh92.
@potiuk, I wonder why code analysis did not discover this?
There was a problem hiding this comment.
@DaanHoogland AFAIK, we create a symlink to generate a download link to apache http server to allow downloading of files. The symlink is in the UUID format to prevent enumeration attacks. And IIRC, the symlinks are removed after certain period.
There is no auth on the download links generated.
| **Q6.** Is "an operator with `root` on a management-server host, the | ||
| JCEKS keystore + encryption keys, the Root CA private key, or MariaDB | ||
| credentials" out of scope (proposed: **yes**, `OUT-OF-MODEL: | ||
| adversary-not-in-scope`)? *(maps to §3 item 1, §9)* |
|
|
||
| | 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. | |
There was a problem hiding this comment.
Default is true. It's false only when upgrading from really old cloudstack versions.
| **Q7.** Hypervisor bugs (libvirt / vSphere SDK / XenAPI / Hyper-V API / | ||
| KVM/QEMU itself) — out of scope, report upstream (proposed)? *(maps to | ||
| §3 item 3)* |
There was a problem hiding this comment.
yes, should not lead to any changes but deleting this question, right @potiuk ?
| | `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 | |
There was a problem hiding this comment.
This isn't configurable and I didn't find any references for api.signature.version in the code.
| | `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 | |
There was a problem hiding this comment.
Global setting is enforce.post.requests.and.timestamps and not post.requests.and.timestamps.enforced
| | `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 700–725)*. | |
There was a problem hiding this comment.
The name of the global setting is proxy.header.verify.
| | `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 700–725)*. | | ||
| | `proxy.forward.list` | unset *(inferred — §14 Q17)* | required when `useforwardheader=true` | CIDR list of trusted reverse proxies. | |
There was a problem hiding this comment.
proxy.header.names is list of names to check for allowed ipaddresses from a proxy set header.
proxy.cidr is a list of cidrs for which "proxy.header.names" are honoured if the "Remote_Addr" is in this list.
| | `proxy.forward.list` | unset *(inferred — §14 Q17)* | required when `useforwardheader=true` | CIDR list of trusted reverse proxies. | | ||
| | `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 | |
There was a problem hiding this comment.
No mention of either auth.password.algorithm or hash.user.password in code.
Order is defined by user.password.encoders.order. Default is PBKDF2,SHA256SALT,MD5,LDAP,SAML2,PLAINTEXT. And to exclude user.password.encoders.exclude which has the default value - MD5,LDAP,PLAINTEXT.
| | `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 | |
There was a problem hiding this comment.
global setting is ca.framework.cert.management.custom.san
|
Thanks @DaanHoogland @yadvr @vishesh92 — agreed, let's make this (apache/cloudstack) the canonical project-level threat model and have the client/tooling repos inherit from it rather than each carrying a full copy. Concretely, mirroring what we've done for other multi-repo PMCs:
So let's converge here first. None of the satellite PRs are merged, so re-pointing them to reference this model once its shape is settled is cheap — I'll repurpose those into pointer PRs (or close + reopen) once you're happy with the parent. On "the fields we need": that's exactly the §14 "Open questions" section — each is a proposed answer for you to confirm, correct, or strike, grouped into waves so you can take a few at a time. Drop answers inline or here and I'll fold them in and promote the provenance tags. Happy to adjust the section set if CloudStack's shape calls for it. |
…po copy Drop the standalone draft-THREAT-MODEL.md and wire the discoverability chain AGENTS.md -> SECURITY.md -> the project-wide model in apache/cloudstack (apache/cloudstack#13293), so scanners find one canonical model and this repo inherits it rather than duplicating it. Generated-by: Claude Code
…po copy Drop the standalone draft-THREAT-MODEL.md and wire the discoverability chain AGENTS.md -> SECURITY.md -> the project-wide model in apache/cloudstack (apache/cloudstack#13293), so scanners find one canonical model and this repo inherits it rather than duplicating it. Generated-by: Claude Code
…po copy Drop the standalone draft-THREAT-MODEL.md and wire the discoverability chain AGENTS.md -> SECURITY.md -> the project-wide model in apache/cloudstack (apache/cloudstack#13293), so scanners find one canonical model and this repo inherits it rather than duplicating it. Generated-by: Claude Code
…po copy Drop the standalone draft-THREAT-MODEL.md and wire the discoverability chain AGENTS.md -> SECURITY.md -> the project-wide model in apache/cloudstack (apache/cloudstack#13293), so scanners find one canonical model and this repo inherits it rather than duplicating it. Generated-by: Claude Code
| **Q12.** **Highest-leverage question in the model.** Two Root-CA defaults: | ||
|
|
||
| - `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. | ||
|
|
||
| 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)* |
There was a problem hiding this comment.
For new setups, this is true by default. When upgrading from an older version (which didn't have this global setting which are versions released prior to Aug, 2017), this is false.
Ref.: #2239
Summary
This PR adds an initial draft of a project-level security
threat-model document (
draft-THREAT-MODEL.md) so that automatedsecurity scanners running against this repository have a
maintainer-facing reference for which classes of findings are
in-scope vs. out-of-scope for the project.
The document follows the rubric format used by several other ASF
projects piloting improved security-model discoverability for
agentic scanners. Every claim carries a provenance tag:
the project website), cited inline.
knowledge; the PMC has not confirmed.
to this draft. (Zero in this initial draft.)
Draft stats:
§14 is the highest-leverage section: answering each question
either promotes one (inferred) tag to (maintainer) or corrects
the underlying claim.
Why "draft-" prefix?
The file is named
draft-THREAT-MODEL.mdrather thanSECURITY-THREAT-MODEL.mdbecause this is a proposal for thePMC to review — please correct, reject, or discuss as needed.
Once the PMC ratifies (or substantially edits) the content, the
file can be renamed in a follow-up PR and a discoverability
scaffold (
AGENTS.md→SECURITY.md→ the model) added soscanners can mechanically follow the chain.
What this is, and what it is not
This is not a security audit. It is a working triage document
— the reference a triager holds against an inbound report to
decide whether the report is about a CloudStack vulnerability or
about caller misuse / operator misconfiguration / an out-of-scope
concern.
The draft was generated by an automated agentic security scan
being piloted by the ASF Security team; the discoverability work
is independent of any specific scan run.
How to review
replaces the inferred claim with the correct one.
dispositions) — those govern how a vulnerability report would
be triaged.
Reply edits / corrections inline on the PR, or to the original
security@apache.orgthread, whichever fits the PMC's workflow.🤖 Generated with Claude Code