Skip to content

azure: add --generate-cloud-config for user ignition config generation - #4

Open
peytonr18 wants to merge 1 commit into
mainfrom
probertson-generate-ignition-config
Open

azure: add --generate-cloud-config for user ignition config generation #4
peytonr18 wants to merge 1 commit into
mainfrom
probertson-generate-ignition-config

Conversation

@peytonr18

@peytonr18 peytonr18 commented Dec 3, 2025

Copy link
Copy Markdown
Owner

Summary

This PR adds the ability for Azure images to generate an Ignition configuration dynamically from Azure Instance Metadata Service (IMDS) and OVF provisioning data.

Changes

  • Added GenerateCloudConfig function to the Azure provider that:
    • Fetches instance metadata from IMDS (/metadata/instance) with parameters format=json&extended=true
    • Reads OVF environment from the provisioning CD-ROM (ovf-env.xml)
    • Builds an Ignition config with admin user, SSH keys, and optional password

Config Generation (internal/providers/azure/azure.go):

  • Creates admin user from IMDS adminUsername (falls back to OVF UserName)
  • Collects SSH public keys from both IMDS and OVF sources (deduplicated)
  • Handles passwords: detects pre-hashed passwords vs plaintext, hashes plaintext using SHA-512 crypt
  • Generates supplementary files:
    • /etc/sudoers.d/99_wheel_nopasswd - passwordless sudo for wheel group
    • /etc/ssh/sshd_config.d/10-custom.conf - SSH hardening based on OVF settings

Password Utilities (internal/providers/azure/crypt.go):

  • HashPassword() - SHA-512 crypt with random 16-char salt
  • IsPasswordHashed() - Detects common hash prefixes ($6$, $5$, $y$, $2a$, etc.)

Command-line Flag (internal/main.go):

  • Added --generate-cloud-config boolean flag to the ignition binary
  • When set, Ignition generates config from cloud provider metadata instead of fetching user-provided config

Platform Interface (internal/platform/platform.go):

  • Extended the Provider struct with GenerateCloudConfig function

Usage

The --generate-cloud-config flag is triggered by shipping a systemd drop-in:

# /etc/systemd/system/ignition-fetch-offline.service.d/azure-generate-config.conf
[Service]
ExecStart=
ExecStart=/usr/bin/ignition --root=/sysroot --platform=${PLATFORM_ID} --stage=fetch-offline ${IGNITION_ARGS} --generate-cloud-config
# /etc/systemd/system/ignition-fetch.service.d/azure-generate-config.conf
[Service]
ExecStart=
ExecStart=/usr/bin/ignition --root=/sysroot --platform=${PLATFORM_ID} --stage=fetch ${IGNITION_ARGS} --generate-cloud-config

This keeps upstream Ignition unchanged while allowing Azure to enable the feature.

Alternative: Auto-Enable for Azure Platform

If we want --generate-cloud-config to be automatically enabled for all Azure VMs without requiring a separate drop-in, we could modify the ignition-generator to detect the platform and set the flag automatically:

platform_id="$(cmdline_arg ignition.platform.id)"

# Auto-enable cloud config generation for platforms that support it
ignition_args=""
if [[ "${platform_id}" == "azure" ]]; then
    ignition_args="--generate-cloud-config"
fi

echo "PLATFORM_ID=${platform_id}" > /run/ignition.env
echo "IGNITION_ARGS=${ignition_args}" >> /run/ignition.env

Benefits of this approach:

  • No separate drop-in files needed for Azure images
  • Automatic behavior based on platform detection
  • Uses the existing ignition.platform.id parameter (already required for Ignition)
  • Other platforms are unaffected (they get an empty IGNITION_ARGS)
  • Future platforms can be added to the condition as they implement GenerateCloudConfig

Trade-offs:

  • Less explicit opt-in (behavior changes based on platform)
  • Requires upstream to carry platform-specific logic in the generator

Please offer feedback on which approach is preferred!


Additional Context: How Ignition Stages Work

Ignition runs during early boot (in the initramfs) and executes in distinct stages. Each stage is a separate invocation of the ignition binary with a different --stage flag.

Stage 1: Generator (Early Boot)

  • ignition-generator (a systemd generator) runs very early in boot, before any services start
    • It parses kernel cmdline and sets up environment
    • It then creates /run/ignition.env with PLATFORM_ID variable

Stage 2: Fetch (Config Acquisition)

  • ignition-fetch-offline.service (or ignition-fetch.service if network needed) acquires the Ignition configuration.
    • Fetches config from cloud provider metadata, user data, or local source
    • With --generate-cloud-config: italls the platform's GenerateCloudConfig() to synthesize a config from cloud metadata
    • Caches the final config to /run/ignition.json

Stage 3: Disks

  • ignition-disks.service runs to partition and format disks.
    • It reads config from /run/ignition.json and then creates partitions, formats filesystems, etc.

Stage 4: Mount

  • ignition-mount.service` runs to mount the filesystems.
    • It reads config from /run/ignition.json and mounts filesystems to /sysroot as specified in config.

Stage 5: Files

  • ignition-files.serviceruns to create users, write files, configure systemd units
    • It reads config from /run/ignition.json and then does the following:
      • Creates users and groups (useradd, groupadd)
      • Sets password hashes in /etc/shadow
      • Writes SSH authorized keys to user home directories
      • Creates files, directories, and symlinks
      • Installs and enables/disables systemd units

Generated Config Example

{
  "ignition": { "version": "3.4.0" },
  "passwd": {
    "users": [{
      "name": "azureuser",
      "groups": ["wheel"],
      "homeDir": "/home/azureuser",
      "shell": "/bin/bash",
      "passwordHash": "$6$...",
      "sshAuthorizedKeys": ["ssh-rsa AAAA...", "ssh-ed25519 AAAA..."]
    }]
  },
  "storage": {
    "files": [
      {
        "path": "/etc/sudoers.d/99_wheel_nopasswd",
        "mode": 288,
        "contents": { "source": "data:,%25wheel%20ALL%3D..." }
      },
      {
        "path": "/etc/ssh/sshd_config.d/10-custom.conf",
        "mode": 420,
        "contents": { "source": "data:,PasswordAuthentication%20..." }
      }
    ]
  }
}

Add --generate-cloud-config flag to synthesize Ignition configs from
Azure IMDS metadata and OVF provisioning data.

@cadejacobson cadejacobson 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.

This is awesome work! On first glance, the only improvement I see is to just add another imds error code to retry. Excited to test this out!

)

var imdsRetryCodes = []int{
404,

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.

Let's add 500 to this.

user.PasswordHash = cfgutil.StrToPtr(passwordHash)
}

sudoersFile := newDataFile("/etc/sudoers.d/99_wheel_nopasswd", 0440, "%wheel ALL=(ALL) NOPASSWD:ALL\n")

@cjp256 cjp256 Dec 5, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

is this supposed to be unconditional?

Consider naming the file to indicate the owner, and perhaps avoid using the variables set, e.g.:
/etc/sudoers.d/50_ignition_cloud_config

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

or ignition-azure-cloud-config

PermitRootLogin no
AllowUsers %s
`, passwordSetting, username)
sshdFile := newDataFile("/etc/ssh/sshd_config.d/10-custom.conf", 0644, sshConfig)

@cjp256 cjp256 Dec 5, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

/etc/ssh/sshd_config.d/50-ignition-azure-cloud-config.conf

Contents: types.Resource{Source: &encoded},
},
}
} No newline at end of file

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

check your editor for newlines at end of file

// getRawConfig returns the config by mounting the given block device
func getRawConfig(f *resource.Fetcher, devicePath string, fstype string) ([]byte, error) {
logger := f.Logger
mnt, err := os.MkdirTemp("", "ignition-azure")

@cjp256 cjp256 Dec 5, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

avoid refactoring so the diff is more concise, this can be done later

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.

3 participants