problem
When a template has a linked userdata with policy APPENDONLY/APPEND, and a user deploys a VM with additional cloud-config userdata, CloudStack concatenates the two cloud-config documents as raw text rather than producing a proper multipart MIME message with two distinct parts.
The resulting payload is a single MIME part containing two #cloud-config documents glued together. Because cloud-init parses this as a single YAML document, any duplicate top-level keys (e.g., runcmd, write_files) cause the second occurrence to silently override the first per YAML semantics. The template-linked userdata is therefore not actually enforced — a user who supplies a cloud-config with the same top-level keys as the template silently destroys the template's directives.
This breaks the security guarantee that APPENDONLY is documented to provide. The documentation states:
Don't allow users to override linked UserData but allow users to pass userdata content … which is appended to the linked UserData of the Template.
In practice, override is exactly what occurs for any colliding top-level key.
Expected Behavior
Both the template's directives and the user's directives should execute. With the reproduction steps in the next section, all four files should exist after boot:
/tmp/template-write-files.txt
/tmp/template-runcmd.txt
/tmp/user-write-files.txt
/tmp/user-runcmd.txt
This requires CloudStack to deliver the appended userdata as a proper multipart MIME message with two distinct text/cloud-config parts. cloud-init would then merge them correctly using its standard merge handlers (or via merge_how directives if specified).
Actual Behavior
Only the user-supplied directives execute. The template's directives are silently lost:
| File |
Status |
/tmp/template-write-files.txt |
MISSING |
/tmp/template-runcmd.txt |
MISSING |
/tmp/user-write-files.txt |
exists |
/tmp/user-runcmd.txt |
exists |
The cloud-init metadata files reveal why.
/var/lib/cloud/instance/user-data.txt.i shows:
Content-Type: multipart/mixed; boundary="===============XXXXXXX=="
MIME-Version: 1.0
Number-Attachments: 1
--===============XXXXXXX==
MIME-Version: 1.0
Content-Type: text/cloud-config
Content-Disposition: attachment; filename="part-001"
#cloud-config
write_files:
- path: /tmp/template-write-files.txt
...
runcmd:
- echo "template runcmd ran" > /tmp/template-runcmd.txt
#cloud-config
write_files:
- path: /tmp/user-write-files.txt
...
runcmd:
- echo "user runcmd ran" > /tmp/user-runcmd.txt
--===============XXXXXXX==--
Note Number-Attachments: 1 — the two cloud-configs are concatenated into a single part, not delivered as two distinct parts. The second #cloud-config line is interpreted as a YAML comment by cloud-init's parser, not a part separator.
/var/lib/cloud/instance/cloud-config.txt confirms cloud-init only saw one document:
#cloud-config
# from 1 files
# part-001
---
runcmd:
- echo "user runcmd ran" > /tmp/user-runcmd.txt
write_files:
- path: /tmp/user-write-files.txt
...
The template's runcmd and write_files were silently discarded due to YAML duplicate-key resolution.
Security Impact
This bug means APPENDONLY does not actually enforce its documented guarantee. Operators relying on APPENDONLY to deliver mandatory cloud-init configuration (e.g., security mitigations, logging agents, baseline hardening) cannot trust that those configurations are applied — any user-supplied cloud-config with colliding top-level keys will silently override them, with no error or warning to the operator or the user.
For organizations using APPENDONLY to enforce kernel-vulnerability mitigations or compliance baselines, this is a silent security regression.
versions
- CloudStack: 4.19.3.0
- Hypervisor: KVM
- Userdata datasource: CloudStack (virtual router)
- Cloud-init (guest): 25.2
- Guest OS reproduced on: Ubuntu 24.04
Note: this issue is in CloudStack's userdata merge logic on the management server side, not OS- or hypervisor-specific.
The steps to reproduce the bug
-
Register a userdata in CloudStack with the following content:
#cloud-config
write_files:
- path: /tmp/template-write-files.txt
content: "template write_files ran\n"
runcmd:
- echo "template runcmd ran" > /tmp/template-runcmd.txt
-
Link this userdata to a template with override policy APPENDONLY.
-
Deploy a VM from this template, supplying additional manual userdata:
#cloud-config
write_files:
- path: /tmp/user-write-files.txt
content: "user write_files ran\n"
runcmd:
- echo "user runcmd ran" > /tmp/user-runcmd.txt
-
After the VM boots, inspect:
ls -la /tmp/template-* /tmp/user-*
cat /var/lib/cloud/instance/user-data.txt
cat /var/lib/cloud/instance/user-data.txt.i
cat /var/lib/cloud/instance/cloud-config.txt
-
Observe that /tmp/template-write-files.txt and /tmp/template-runcmd.txt do not exist, while /tmp/user-write-files.txt and /tmp/user-runcmd.txt do. The .i metadata file shows Number-Attachments: 1, confirming CloudStack delivered both cloud-configs concatenated into a single MIME part rather than as two distinct parts.
What to do about it?
Suggested Fix
CloudStack's userdata append logic should produce a proper multipart MIME message with two distinct parts, each tagged with the appropriate Content-Type (e.g., text/cloud-config), rather than concatenating raw content. cloud-init's existing handlers will then merge the parts correctly, and operators can use merge_how directives in the template userdata to control merge behavior per-key (replace, append, recurse).
The relevant code path is the userdata combination logic invoked when policy is APPENDONLY and the user supplies additional userdata via userdata= or userdataid= on deployVirtualMachine.
Workarounds Attempted (none sufficient)
merge_how directives in the template userdata — do not work, because cloud-init's merge handlers operate across separate MIME parts, not within a single concatenated document.
- Wrapping the template userdata in a multipart MIME envelope when registering — does not help; CloudStack still concatenates the result.
- Using non-colliding keys (e.g.,
bootcmd in user-supplied userdata when template uses runcmd) — works, but requires every user to know which keys to avoid, which defeats the security purpose of APPENDONLY.
Related History
Issue #7918 ("Joint UserData must have a header") and PR #9575 ("Fix userdata append header restrictions", merged Aug 2024, included in 4.19.2+) addressed a different aspect of joint userdata handling. The header-restriction fix is present in 4.19.3.0 and is not the issue reported here. This is a separate problem with the append mechanism producing a single MIME part instead of multiple distinct parts.
problem
When a template has a linked userdata with policy
APPENDONLY/APPEND, and a user deploys a VM with additional cloud-config userdata, CloudStack concatenates the two cloud-config documents as raw text rather than producing a proper multipart MIME message with two distinct parts.The resulting payload is a single MIME part containing two
#cloud-configdocuments glued together. Because cloud-init parses this as a single YAML document, any duplicate top-level keys (e.g.,runcmd,write_files) cause the second occurrence to silently override the first per YAML semantics. The template-linked userdata is therefore not actually enforced — a user who supplies a cloud-config with the same top-level keys as the template silently destroys the template's directives.This breaks the security guarantee that
APPENDONLYis documented to provide. The documentation states:In practice, override is exactly what occurs for any colliding top-level key.
Expected Behavior
Both the template's directives and the user's directives should execute. With the reproduction steps in the next section, all four files should exist after boot:
/tmp/template-write-files.txt/tmp/template-runcmd.txt/tmp/user-write-files.txt/tmp/user-runcmd.txtThis requires CloudStack to deliver the appended userdata as a proper multipart MIME message with two distinct
text/cloud-configparts. cloud-init would then merge them correctly using its standard merge handlers (or viamerge_howdirectives if specified).Actual Behavior
Only the user-supplied directives execute. The template's directives are silently lost:
/tmp/template-write-files.txt/tmp/template-runcmd.txt/tmp/user-write-files.txt/tmp/user-runcmd.txtThe cloud-init metadata files reveal why.
/var/lib/cloud/instance/user-data.txt.ishows:Note
Number-Attachments: 1— the two cloud-configs are concatenated into a single part, not delivered as two distinct parts. The second#cloud-configline is interpreted as a YAML comment by cloud-init's parser, not a part separator./var/lib/cloud/instance/cloud-config.txtconfirms cloud-init only saw one document:The template's
runcmdandwrite_fileswere silently discarded due to YAML duplicate-key resolution.Security Impact
This bug means
APPENDONLYdoes not actually enforce its documented guarantee. Operators relying onAPPENDONLYto deliver mandatory cloud-init configuration (e.g., security mitigations, logging agents, baseline hardening) cannot trust that those configurations are applied — any user-supplied cloud-config with colliding top-level keys will silently override them, with no error or warning to the operator or the user.For organizations using
APPENDONLYto enforce kernel-vulnerability mitigations or compliance baselines, this is a silent security regression.versions
Note: this issue is in CloudStack's userdata merge logic on the management server side, not OS- or hypervisor-specific.
The steps to reproduce the bug
Register a userdata in CloudStack with the following content:
Link this userdata to a template with override policy
APPENDONLY.Deploy a VM from this template, supplying additional manual userdata:
After the VM boots, inspect:
Observe that
/tmp/template-write-files.txtand/tmp/template-runcmd.txtdo not exist, while/tmp/user-write-files.txtand/tmp/user-runcmd.txtdo. The.imetadata file showsNumber-Attachments: 1, confirming CloudStack delivered both cloud-configs concatenated into a single MIME part rather than as two distinct parts.What to do about it?
Suggested Fix
CloudStack's userdata append logic should produce a proper multipart MIME message with two distinct parts, each tagged with the appropriate
Content-Type(e.g.,text/cloud-config), rather than concatenating raw content. cloud-init's existing handlers will then merge the parts correctly, and operators can usemerge_howdirectives in the template userdata to control merge behavior per-key (replace, append, recurse).The relevant code path is the userdata combination logic invoked when policy is
APPENDONLYand the user supplies additional userdata viauserdata=oruserdataid=ondeployVirtualMachine.Workarounds Attempted (none sufficient)
merge_howdirectives in the template userdata — do not work, because cloud-init's merge handlers operate across separate MIME parts, not within a single concatenated document.bootcmdin user-supplied userdata when template usesruncmd) — works, but requires every user to know which keys to avoid, which defeats the security purpose ofAPPENDONLY.Related History
Issue #7918 ("Joint UserData must have a header") and PR #9575 ("Fix userdata append header restrictions", merged Aug 2024, included in 4.19.2+) addressed a different aspect of joint userdata handling. The header-restriction fix is present in 4.19.3.0 and is not the issue reported here. This is a separate problem with the append mechanism producing a single MIME part instead of multiple distinct parts.