Skip to content

Userdata APPEND policy concatenates cloud-config as text, causing same-name top-level keys to silently override template-linked userdata #13092

@CodeBleu

Description

@CodeBleu

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

  1. 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
  2. Link this userdata to a template with override policy APPENDONLY.

  3. 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
  4. 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
  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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions