Startup scripts
os.startup_scripts creates bounded systemd one-shot work for the resulting image. Each entry declares a shell command, required packages, execution user, and ordering unit.
Startup scripts are root-capable code unless run_as says otherwise. They must receive the same review as any installation script.
Canonical shape
{
"os": {
"startup_scripts": [
{
"name": "write-build-marker",
"description": "Create a local readiness marker after networking is available.",
"command": "set -Eeuo pipefail\ninstall -d -m 0755 /var/lib/example\nprintf '%s\\n' ready > /var/lib/example/build-ready",
"packages": [],
"run_as": "root",
"after": "network-online.target"
}
]
}
}The fields are description and command, not the legacy script field. run_as and after also use snake_case. after is one systemd unit string, not an array.
The schema accepts at most 32 entries and a bounded command size. Validation rejects empty commands and NUL bytes, but it does not make shell content safe or idempotent.
Design for retries and partial failure
A boot can be interrupted after some side effects have occurred. Write scripts so another invocation either completes safely or exits with a clear, inspectable state.
Good patterns include:
- write to a temporary file, verify it, then rename atomically;
- check whether users, directories, or configuration entries already exist;
- use
installfor explicit owner and mode; - apply
set -Eeuo pipefailand handle expected nonzero results deliberately; - use bounded network timeouts and a finite retry count; and
- write a readiness marker only after all required steps succeed.
Do not rely on sleep as a readiness check. Probe the actual dependency.
External downloads
Avoid curl ... | sh. If first boot must fetch an artifact:
- use HTTPS with certificate verification;
- pin the expected artifact or source version;
- verify a cryptographic digest or approved signature before execution;
- set connect and total timeouts;
- fail closed if verification fails; and
- avoid logging credentials or signed URLs.
For truly offline or reproducible behavior, include reviewed content in the image or an approved package repository instead of downloading on first boot.
Secrets
Never embed plaintext credentials in the recipe, command, URL, or generated marker. Recipe JSON and build logs are retained evidence and can be visible to operators. Use an approved deployment-time enrollment or secret-delivery mechanism and scope the resulting credential to the target.
Execution identity
Prefer an unprivileged service account. If root is required, reduce the command to the smallest privileged step and set explicit file ownership. Confirm that run_as names an account created before the unit starts.
Verification
Test outcomes rather than only the unit’s exit status:
{
"type": "file_contains",
"description": "The startup unit wrote its readiness marker.",
"params": {
"path": "/var/lib/example/build-ready",
"content": "ready"
}
}Also test a second boot, an unavailable dependency, and recovery from an interrupted first run. Inspect systemctl status and the unit journal on failure.