CodeBleu opened a new issue, #13092:
URL: https://github.com/apache/cloudstack/issues/13092

   ### 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:
   
   ```yaml
   #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
   
   1. Register a userdata in CloudStack with the following content:
   
      ```yaml
      #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
      ```
   
   2. Link this userdata to a template with override policy `APPENDONLY`.
   
   3. Deploy a VM from this template, supplying additional manual userdata:
   
      ```yaml
      #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
      ```
   
   4. After the VM boots, inspect:
   
      ```bash
      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
      ```
   
   5. 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.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to