Skip to content

About

Cloud-init to Butane transpiler

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Flatcar OS Discord Slack Twitter Follow Mastodon Follow Bluesky OpenSSF Best Practices

butane-init: a Cloud-Init to Butane Config Transpiler

Background

Flatcar uses Butane and Ignition for provisioning. Much of the wider ecosystem still produces cloud-config (cloud-init) YAML, which makes it harder to adopt Flatcar in environments where cloud-config is already the norm.

The Project

A transpiler in Go that converts cloud-config YAML into Butane YAML for Flatcar Container Linux. Generated configurations always use variant: flatcar; other distributions, including Fedora CoreOS, are not currently supported. The transpiler implements the minimum functionality needed to support ClusterAPI worker node provisioning, and to replace coreos-cloudinit In practice that limits the transpiler to basic Butane features such as users, groups, certificates, files and systemd units.

Goals and Non-Goals

  • A working Go transpiler covering a documented subset of cloud-config.
  • Test coverage for each supported feature, running in CI.
  • End to end integration with Flatcar and ClusterAPI.
  • Documentation covering supported fields and known limitations.

Usage

Build bt, then pass a cloud-config file or pipe one on standard input:

go build -o bt ./cmd/bt
bt cloud-config.yaml > butane.yaml
# or: cat cloud-config.yaml | bt -o butane.yaml

bt currently converts the Cluster API users, ntp, and plain write_files subsets. A config is either converted completely or rejected; unsupported fields are never silently discarded.

cloud-config stanza Status Butane output
users Supported Cluster API subset passwd, plus generated storage.files
write_files Supported plain-file subset storage.files, including tmpfiles rules for /run
runcmd Unsupported Planned scripts and systemd units
bootcmd Unsupported Planned scripts and systemd units
ntp Supported Cluster API subset A generated timesyncd configuration file
disk_setup Unsupported Planned storage configuration
fs_setup Unsupported Planned storage.filesystems
mounts Unsupported Planned filesystems or systemd mount units
cloud-config field Butane output
name passwd.users[].name
passwd passwd.users[].password_hash
gecos passwd.users[].gecos
homedir passwd.users[].home_dir
shell passwd.users[].shell
ssh_authorized_keys passwd.users[].ssh_authorized_keys
groups passwd.users[].groups and synthesized passwd.groups
primary_group passwd.users[].primary_group and synthesized passwd.groups
inactive false is a no-op; true is rejected
lock_passwd Password locking and a generated sshd drop-in when false
sudo A generated /etc/sudoers.d/<username> file

The new fields accept only the shapes emitted by Cluster API: groups is a comma-separated string, primary_group and sudo are non-empty strings, and inactive and lock_passwd are booleans. Referenced groups are created before users. Empty or duplicate supplementary groups are rejected, and a primary group must not also be listed as a supplementary group. If a referenced group has the same name as a configured user, that user must set primary_group explicitly to avoid colliding with Linux's implicit private-group creation.

Password hashes are locked when lock_passwd is omitted or true. When it is false, the hash remains unlocked and bt enables SSH password authentication for that user with a Flatcar sshd drop-in. An already-locked hash is rejected when lock_passwd is false. Users that request password login or sudo must have names matching ^[a-z_][a-z0-9_-]*$ so names cannot become SSH, sudoers, or path syntax.

Generated sshd and sudoers files are root-owned, mode 0600, and replace any existing file at the generated path. Sudo rules are preserved verbatim and are not checked for valid sudoers syntax. No sshd restart is needed because Ignition writes the drop-in before sshd starts.

Write files

write_files accepts a list of objects with these fields, using the Cluster API v1.11.1 cloud-config vocabulary:

Field Supported values
path Required clean absolute file path: no ./.. components, repeated slashes, trailing slash, or control characters
content Plain string, preserved exactly; omitted or '' creates an empty file; null and binary values are rejected
owner Explicit user:group names, default root:root; no partial forms or numeric IDs
permissions Quoted three- or four-digit octal string, default '0644'; only 0000–0777

Owner names must match [a-zA-Z_][a-zA-Z0-9_.-]* with an optional trailing $. Ownership does not create accounts or groups: they must exist on the target image or be declared in users. encoding, append, defer, source, and all other fields are rejected, even when they specify a no-op. An omitted stanza or empty list generates nothing; a null stanza is rejected.

Persistent files use Ignition's overwrite behavior. Targets must be regular files or absent, with writable parent directories on the target image; following existing destination symlinks is not part of this subset. Parent-directory ownership is left to Ignition/systemd defaults, not taken from owner. /tmp, /dev, /proc, /sys, /media, and /var/lock paths are unsupported (use /run/lock/... instead of /var/lock/...). Duplicate paths, file/directory conflicts, and collisions with files generated by users or ntp are rejected. /etc/tmpfiles.d/60-butane-init-write-files.conf is reserved.

Files beneath /run (including the /var/run/ alias) instead generate f+!~ rules in that reserved tmpfiles configuration. The rules create or truncate files after /run is mounted, preserve literal content using base64, and apply the requested mode and ownership. Each complete encoded rule must be smaller than 1 MiB, including its escaped path and metadata; oversized runtime content is rejected before producing output. This internal encoding does not enable the input encoding field. The rule file is root-owned, mode 0600, and retains the content on persistent storage: base64 is not encryption, and bootstrap credentials are not automatically removed.

Runtime destinations and their parents must be trusted. Tmpfiles truncates an existing regular file in place, writing before it applies the requested mode and ownership. Do not use it to replace an untrusted or world-readable file with sensitive content: existing readers could see that content during replacement.

Unlike cloud-init's once-per-instance write_files, these /run files are recreated on every boot. ! restricts rules to tmpfiles invocations using --boot, not just the first boot. Such invocations also replace existing content. Consumers must run after systemd-tmpfiles-setup.service; conflicting tmpfiles rules installed outside this cloud-config remain the operator's responsibility. Creating a kubeadm configuration does not execute kubeadm join; runcmd is still unsupported. Boot and reboot behavior is verified on Flatcar 4757.2.1.

NTP

ntp accepts only enabled (a boolean) and servers (a list of ASCII hostnames, IPv4 addresses, or bare IPv6 addresses without zone identifiers or ports). Server names are validated without DNS lookups; whitespace, URLs, and configuration syntax are rejected. Unsupported fields such as pools, ntp_client, and config are rejected even when enabled: false.

With enabled omitted or true, non-empty servers produce a root-owned, mode 0644 /etc/systemd/timesyncd.conf.d/60-butane-init.conf file, replacing any file at that path. It resets earlier global NTP= settings before assigning the supplied list; later drop-ins can still override it. DHCP-provided sources remain allowed. enabled: false skips NTP configuration after validation; it does not disable time synchronization. Omitted, null, or empty server lists, an absent ntp stanza, and ntp: {} retain Flatcar defaults without generating files or units.

This relies on Flatcar's native timesyncd service, does not switch clients on images using another daemon, and does not make kubeadm wait for synchronization. Fedora CoreOS uses chrony, so this timesyncd drop-in would not configure its default NTP client. Supporting it would require both the appropriate Butane variant and chrony configuration, not just a change to the variant. All stanzas other than users, ntp, and write_files remain unsupported. Jinja templates are not evaluated and are rejected; render them before passing cloud-config to bt.

A Flatcar Container Linux project

Flatcar Container Linux is a fully open source, minimal-footprint, secure by default and always up-to-date Linux distribution for running containers at scale.

This repository is maintained by the Flatcar community and contributors. Development and design decisions are guided by the principles of transparency, modularity, and collaboration.

Please find information on:


Community & Project Documentation

  • Contributing Guidelines — How to contribute, find issues, and submit pull requests
  • Code of Conduct — Standards for respectful and inclusive community participation
  • Security Policy — How to report vulnerabilities and security-related information
  • Maintainers — Current project maintainers and their responsibilities
  • Governance — Project governance model, decision-making process, and roles

About

Cloud-init to Butane transpiler

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages