From f0c8c370228676313d57b00505c589f1832ec0d4 Mon Sep 17 00:00:00 2001 From: Alex Gonzalez Date: Fri, 12 May 2023 16:02:32 +0200 Subject: [PATCH] os configure, config generate: Add '--secureBoot' option to opt-in secure boot Allow to generate a config file with `installer.secureboot` set so that a secure boot and disk encrypted system can be installed. Change-type: minor Signed-off-by: Alex Gonzalez --- docs/balena-cli.md | 15 ++++++++ lib/commands/config/generate.ts | 21 ++++++++++- lib/commands/os/configure.ts | 21 ++++++++++- lib/utils/common-flags.ts | 6 +++ lib/utils/config.ts | 66 +++++++++++++++++++++++++++++++++ lib/utils/messages.ts | 4 ++ 6 files changed, 131 insertions(+), 2 deletions(-) diff --git a/docs/balena-cli.md b/docs/balena-cli.md index 3711bd46..65ebcd7b 100644 --- a/docs/balena-cli.md +++ b/docs/balena-cli.md @@ -2160,6 +2160,9 @@ confuse the balenaOS "development mode" with a device's "local mode", the latter being a supervisor feature that allows the "balena push" command to push a user's application directly to a device in the local network. +The '--secureBoot' option is used to configure a balenaOS installer image to opt-in +secure boot and disk encryption. + The --system-connection (-c) option is used to inject NetworkManager connection profiles for additional network interfaces, such as cellular/GSM or additional WiFi or ethernet connections. This option may be passed multiple times in case there @@ -2230,6 +2233,10 @@ WiFi SSID (network name) (non-interactive configuration) Configure balenaOS to operate in development mode +#### --secureBoot + +Configure balenaOS installer to opt-in secure boot and disk encryption + #### -d, --device DEVICE device UUID @@ -2314,6 +2321,9 @@ confuse the balenaOS "development mode" with a device's "local mode", the latter being a supervisor feature that allows the "balena push" command to push a user's application directly to a device in the local network. +The '--secureBoot' option is used to configure a balenaOS installer image to opt-in +secure boot and disk encryption. + To configure an image for a fleet of mixed device types, use the --fleet option alongside the --deviceType option to specify the target device type. @@ -2337,6 +2347,7 @@ Examples: $ balena config generate --device 7cf02a6 --version 2.12.7 --deviceApiKey $ balena config generate --device 7cf02a6 --version 2.12.7 --output config.json $ balena config generate --fleet myorg/fleet --version 2.12.7 --dev + $ balena config generate --fleet myorg/fleet --version 2.12.7 --secureBoot $ balena config generate --fleet myorg/fleet --version 2.12.7 --deviceType fincm3 $ balena config generate --fleet myorg/fleet --version 2.12.7 --output config.json $ balena config generate --fleet myorg/fleet --version 2.12.7 --network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 15 @@ -2355,6 +2366,10 @@ fleet name or slug (preferred) Configure balenaOS to operate in development mode +#### --secureBoot + +Configure balenaOS installer to opt-in secure boot and disk encryption + #### -d, --device DEVICE device UUID diff --git a/lib/commands/config/generate.ts b/lib/commands/config/generate.ts index 2806a26e..cf8524c7 100644 --- a/lib/commands/config/generate.ts +++ b/lib/commands/config/generate.ts @@ -19,13 +19,18 @@ import { flags } from '@oclif/command'; import Command from '../../command'; import * as cf from '../../utils/common-flags'; import { getBalenaSdk, getCliForm, stripIndent } from '../../utils/lazy'; -import { applicationIdInfo, devModeInfo } from '../../utils/messages'; +import { + applicationIdInfo, + devModeInfo, + secureBootInfo, +} from '../../utils/messages'; import type { PineDeferred } from 'balena-sdk'; interface FlagsDef { version: string; // OS version fleet?: string; dev?: boolean; // balenaOS development variant + secureBoot?: boolean; device?: string; deviceApiKey?: string; deviceType?: string; @@ -51,6 +56,8 @@ export default class ConfigGenerateCmd extends Command { ${devModeInfo.split('\n').join('\n\t\t')} + ${secureBootInfo.split('\n').join('\n\t\t')} + To configure an image for a fleet of mixed device types, use the --fleet option alongside the --deviceType option to specify the target device type. @@ -66,6 +73,7 @@ export default class ConfigGenerateCmd extends Command { '$ balena config generate --device 7cf02a6 --version 2.12.7 --deviceApiKey ', '$ balena config generate --device 7cf02a6 --version 2.12.7 --output config.json', '$ balena config generate --fleet myorg/fleet --version 2.12.7 --dev', + '$ balena config generate --fleet myorg/fleet --version 2.12.7 --secureBoot', '$ balena config generate --fleet myorg/fleet --version 2.12.7 --deviceType fincm3', '$ balena config generate --fleet myorg/fleet --version 2.12.7 --output config.json', '$ balena config generate --fleet myorg/fleet --version 2.12.7 --network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 15', @@ -80,6 +88,7 @@ export default class ConfigGenerateCmd extends Command { }), fleet: { ...cf.fleet, exclusive: ['device'] }, dev: cf.dev, + secureBoot: cf.secureBoot, device: { ...cf.device, exclusive: [ @@ -195,6 +204,15 @@ export default class ConfigGenerateCmd extends Command { deviceType, ); + const { validateSecureBootOptionAndWarn } = await import( + '../../utils/config' + ); + await validateSecureBootOptionAndWarn( + options.secureBoot, + deviceType, + options.version, + ); + // Prompt for values // Pass params as an override: if there is any param with exactly the same name as a // required option, that value is used (and the corresponding question is not asked) @@ -203,6 +221,7 @@ export default class ConfigGenerateCmd extends Command { }); answers.version = options.version; answers.developmentMode = options.dev; + answers.secureBoot = options.secureBoot; answers.provisioningKeyName = options['provisioning-key-name']; answers.provisioningKeyExpiryDate = options['provisioning-key-expiry-date']; diff --git a/lib/commands/os/configure.ts b/lib/commands/os/configure.ts index e177f968..e1073858 100644 --- a/lib/commands/os/configure.ts +++ b/lib/commands/os/configure.ts @@ -23,7 +23,11 @@ import Command from '../../command'; import { ExpectedError } from '../../errors'; import * as cf from '../../utils/common-flags'; import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy'; -import { applicationIdInfo, devModeInfo } from '../../utils/messages'; +import { + applicationIdInfo, + devModeInfo, + secureBootInfo, +} from '../../utils/messages'; const CONNECTIONS_FOLDER = '/system-connections'; @@ -36,6 +40,7 @@ interface FlagsDef { 'config-wifi-key'?: string; 'config-wifi-ssid'?: string; dev?: boolean; // balenaOS development variant + secureBoot?: boolean; device?: string; // device UUID 'device-type'?: string; help?: void; @@ -53,6 +58,7 @@ interface ArgsDef { interface Answers { appUpdatePollInterval: number; // in minutes developmentMode?: boolean; // balenaOS development variant + secureBoot?: boolean; deviceType: string; // e.g. "raspberrypi3" network: 'ethernet' | 'wifi'; version: string; // e.g. "2.32.0+rev1" @@ -80,6 +86,8 @@ export default class OsConfigureCmd extends Command { ${devModeInfo.split('\n').join('\n\t\t')} + ${secureBootInfo.split('\n').join('\n\t\t')} + The --system-connection (-c) option is used to inject NetworkManager connection profiles for additional network interfaces, such as cellular/GSM or additional WiFi or ethernet connections. This option may be passed multiple times in case there @@ -140,6 +148,7 @@ export default class OsConfigureCmd extends Command { description: 'WiFi SSID (network name) (non-interactive configuration)', }), dev: cf.dev, + secureBoot: cf.secureBoot, device: { ...cf.device, exclusive: [ @@ -238,6 +247,15 @@ export default class OsConfigureCmd extends Command { const { validateDevOptionAndWarn } = await import('../../utils/config'); await validateDevOptionAndWarn(options.dev, osVersion); + const { validateSecureBootOptionAndWarn } = await import( + '../../utils/config' + ); + await validateSecureBootOptionAndWarn( + options.secureBoot, + deviceTypeSlug, + osVersion, + ); + const answers: Answers = await askQuestionsForDeviceType( deviceTypeManifest, options, @@ -248,6 +266,7 @@ export default class OsConfigureCmd extends Command { } answers.version = osVersion; answers.developmentMode = options.dev; + answers.secureBoot = options.secureBoot; answers.provisioningKeyName = options['provisioning-key-name']; answers.provisioningKeyExpiryDate = options['provisioning-key-expiry-date']; diff --git a/lib/utils/common-flags.ts b/lib/utils/common-flags.ts index 37f0a734..818b03b2 100644 --- a/lib/utils/common-flags.ts +++ b/lib/utils/common-flags.ts @@ -74,6 +74,12 @@ export const dev: IBooleanFlag = flags.boolean({ default: false, }); +export const secureBoot: IBooleanFlag = flags.boolean({ + description: + 'Configure balenaOS installer to opt-in secure boot and disk encryption', + default: false, +}); + export const drive = flags.string({ char: 'd', description: stripIndent` diff --git a/lib/utils/config.ts b/lib/utils/config.ts index 61231afa..cce020f9 100644 --- a/lib/utils/config.ts +++ b/lib/utils/config.ts @@ -52,6 +52,10 @@ export interface ImgConfig { os?: { sshKeys?: string[]; }; + + installer?: { + secureboot?: boolean; + }; } export async function generateApplicationConfig( @@ -63,6 +67,7 @@ export async function generateApplicationConfig( os?: { sshKeys?: string[]; }; + secureBoot?: boolean; }, ): Promise { options = { @@ -84,6 +89,12 @@ export async function generateApplicationConfig( : options.os.sshKeys; } + // configure installer secure boot opt-in if specified + if (options.secureBoot) { + config.installer ??= {}; + config.installer.secureboot = options.secureBoot; + } + return config; } @@ -165,3 +176,58 @@ export async function validateDevOptionAndWarn( and exposes network ports such as 2375 that allows unencrypted access to balenaEngine. Therefore, development mode should only be used in private, trusted local networks.`); } + +/** + * Chech whether the `--secureBoot` option of commands related to OS configuration + * such as `os configure` and `config generate` is compatible with a given + * OS release, and print a warning regarding the consequences of using that + * option. + */ +export async function validateSecureBootOptionAndWarn( + secureBoot?: boolean, + slug?: string, + version?: string, + logger?: import('./logger'), +) { + if (!secureBoot) { + return; + } + const { ExpectedError } = await import('../errors'); + if (!version) { + throw new ExpectedError(`Error: No version provided`); + } + if (!slug) { + throw new ExpectedError(`Error: No device type provided`); + } + const sdk = getBalenaSdk(); + const [OsRelease]: BalenaSdk.OsVersion[] = + await sdk.models.os.getAllOsVersions(`${slug}`, { + $select: 'id', + $filter: { raw_version: `${version.replace(/^v/, '')}` }, + }); + if (!OsRelease) { + throw new ExpectedError(`Error: No ${version} release for ${slug}`); + } + const OsContract = await sdk.models.release.get(OsRelease.id, { + $select: 'contract', + }); + const yaml = require('js-yaml'); + const contract = yaml.load(OsContract['contract']); + if ( + contract.provides.some((entry: Dictionary) => { + return entry.type === 'sw.feature' && entry.slug === 'secureboot'; + }) + ) { + if (!logger) { + const Logger = await import('./logger'); + logger = Logger.getLogger(); + } + logger.logInfo(stripIndent` + The '--secureBoot' option is being used to configure a balenaOS installer image + into secure boot and full disk encryption.`); + } else { + throw new ExpectedError( + `Error: The '--secureBoot' option is not supported for ${slug} in ${version}`, + ); + } +} diff --git a/lib/utils/messages.ts b/lib/utils/messages.ts index ec9f21b8..530bc553 100644 --- a/lib/utils/messages.ts +++ b/lib/utils/messages.ts @@ -169,6 +169,10 @@ confuse the balenaOS "development mode" with a device's "local mode", the latter being a supervisor feature that allows the "balena push" command to push a user's application directly to a device in the local network.`; +export const secureBootInfo = `\ +The '--secureBoot' option is used to configure a balenaOS installer image to opt-in +secure boot and disk encryption.`; + export const jsonInfo = `\ The --json option is recommended when scripting the output of this command, because field names are less likely to change in JSON format and because it