Skip to content

feat(azure): ensure CustomData is base64-decodable - #6958

Open
cadejacobson wants to merge 11 commits into
canonical:mainfrom
cadejacobson:custom-data-base64
Open

feat(azure): ensure CustomData is base64-decodable#6958
cadejacobson wants to merge 11 commits into
canonical:mainfrom
cadejacobson:custom-data-base64

Conversation

@cadejacobson

@cadejacobson cadejacobson commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Proposed Commit Message

feat(azure): ensure CustomData is base64-decodable

Azure's CustomData for Linux VMs is base64 encoded. 
Future developments in Azure for the custom data will result
in this field reaching cloud-init encrypted in some scenarios.
If the decryption process is ever bypassed or failed, the encrypted
string will not be able to be base64 decoded and will result in an
unhandled error. If non-base64 CustomData reaches the parser,
cloud-init should report an error and fail.

Fixes GH-6959

Test Steps

This can be validated by using a custom cloud-init version that passes a non-base64 CustomData to cloud-init. The following was done on an Azure VM with the following not_base64 string:

image

This forced CustomData value resulted in the correct error propagating to Azure:

[    9.856048] cloud-init[689]: 2026-07-29 13:57:49,418 - azure.py[ERROR]: Azure datasource failure occurred: result=error|reason=failure to decode ovf-env.xml field=CustomData|agent=Cloud-Init|field=CustomData|exception=Invalid base64-encoded string: number of data characters (9) cannot be 1 more than a multiple of 4|length=10|timestamp=2026-08-05T18:36:46.845442+00:00|documentation_url=https://aka.ms/linuxprovisioningerror

My test changes for this can be found on my personal fork at the linked branch.

Merge type

  • Squash merge using "Proposed Commit Message"

Fixes #6959

@cadejacobson cadejacobson changed the title feat(azure): ensure custom data is base64-decodable feat(azure): ensure CustomData is base64-decodable Jul 28, 2026
@cadejacobson
cadejacobson marked this pull request as ready for review July 29, 2026 12:36
@cadejacobson
cadejacobson marked this pull request as draft July 29, 2026 15:17
@cadejacobson
cadejacobson marked this pull request as ready for review July 29, 2026 16:12

@bpryan99 bpryan99 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, nice work.

@blackboxsw blackboxsw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cadejacobson thank you for this. I'd like to move the scope of the try/except closer to where we actually expect that error to be raised instead of further up the stack. The other comment about message failure details is non-blocking.


class ReportableErrorOvfInvalidBase64(ReportableError):
def __init__(self, field: str) -> None:
super().__init__(f"failure to decode ovf-env.xml field={field}")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems we are only reporting the field as supporting data but not the representation of the specific cause of the error.

binascii.Error has more detail that could aid in the debug of this issue.
str(error) in helpers/azure.py gives us Invalid base64-encoded string: number of data characters (5) cannot be 1 more than a multiple of 4 Do we want to include this detail in the raised ReportableError somehow?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps this is non-blocking, and your intent is to provide a simple readable string to represent all classes of non-base64 encoded data without details. Just wanted to raise this for your thoughts before we proceed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for raising this! This message was initially supposed to be rather general in order to allow for some of our internal queries on deployment statistics to be able to gather the InvalidBase64 deployments into one group. This may be achievable while still providing the additional message in that error output.

Let me work with @cjp256 to figure out the best way to go from here and I will get back to you.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I agree it would be good to include some additional details in supporting data like length and maybe name of error type caught. We just need to be careful to include safe strings so we don't accidentally leak data contents if contained in the thrown exception. I don't expect this to be the case but we would need to validate.

@cadejacobson cadejacobson Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to include a bit more data in the exception string. It appears no secrets will be reported in the failure message. Here is an example:

[    9.856048] cloud-init[689]: 2026-07-29 13:57:49,418 - azure.py[ERROR]: Azure datasource failure occurred: result=error|reason=failure to decode ovf-env.xml field=CustomData|agent=Cloud-Init|field=CustomData|'exception=Error(''Invalid base64-encoded string: number of data characters (9) cannot be 1 more than a multiple of 4'')'|length=10|vm_id={redacted}|timestamp=2026-08-04T13:35:14.359589+00:00|documentation_url=https://aka.ms/linuxprovisioningerror.

The length being different is actually valuable, as I included a non-data character (_) in my mocked bad-base64. This difference helps to show that there are non-data characters getting stripped out of the base64 while decoding, and how many there are.

Comment thread cloudinit/sources/helpers/azure.py Outdated
decode_base64=True,
required=False,
)
except binascii.Error as error:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since this error can only be raised when decode_base64=True, can we instead move this try/except scope down into _parse_property:around the single line 1087 instead of up in _parse_linux_configuration_set_section?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is much cleaner and is better practice. Thanks for noticing this! This should be fixed in the most recent commit.

@blackboxsw blackboxsw self-assigned this Jul 31, 2026
Comment thread tests/unittests/sources/test_azure_helper.py Outdated
),
(
"not_base64", # bad character stripped away
10,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIL that validate=False ignores any invalid b64 character, not just whitespace.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

given this, I agree these are the only two cases that throw exceptions. Incorrect badding or invalid number of data characters. This test coverage provides additional confidence that it can't/won't leak the data contents in the error. I reviewed the binascii/base64 implementations and these are the only two error cases I see with altchars=None and validate=False.

Comment thread cloudinit/sources/helpers/azure.py Outdated

parsed = azure_helper.OvfEnvXml.parse_text(ovf)

assert parsed.custom_data is None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

interesting that custom_data is None instead of ""?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that is due to ElementTree's parsing of empty tags, or beginning and closing tags one after the other: https://docs.python.org/3.12/library/xml.etree.elementtree.html#element-objects

Since the CustomData XML tag appears as <CustomData></CustomData>, the text is not ever updated from the default value of None. A similar thing happens at the following test that was merged last week:
https://github.com/canonical/cloud-init/pull/6948/changes#diff-6e31b416f1464a8d16e2519ac4b4ce66946b54100de33dc5d40b27265f44871dR1447

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[enhancement]: validate Azure's CustomData is base64-decodable

4 participants