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.
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.
- 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.
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.yamlbt 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 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 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.
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:
- Contribution Guide – how to get involved and submit patches.
- Repository Codeowners – who's assigned for contribution reviews in this repository.
- Repository Maintainers – who’s responsible for what in this repository.
- Flatcar Project Maintainers – who’s responsible for what at Flatcar.
- Governance – how decisions are made and who the stakeholders are.
- Code of Conduct – our commitment to a welcoming and inclusive community.
- LICENSE - License under which the software is released.
- 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