From 1b64da475a78f5cfc32437e6ac6ddfed2fa9b500 Mon Sep 17 00:00:00 2001 From: usimd <11619247+usimd@users.noreply.github.com> Date: Tue, 14 May 2024 21:19:31 +0200 Subject: [PATCH] Configure PUBKEY_SSH_FIRST_USER --- .github/workflows/integration-test.yml | 3 +++ README.md | 11 ++++++++++ __test__/pi-gen-config.test.ts | 26 ++++++++++++++++++++++ action.yml | 12 +++++++++++ jest.config.js | 4 ++-- package-lock.json | 8 +++---- package.json | 4 ++-- src/configure.ts | 3 +++ src/host-dependencies.ts | 3 ++- src/pi-gen-config.ts | 30 ++++++++++++++++++++++++++ 10 files changed, 94 insertions(+), 10 deletions(-) diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 029e6cc..45cbbd6 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -20,6 +20,7 @@ env: CONFIG_LOCALE: de_DE.UTF-8 CONFIG_USERNAME: pi-gen CONFIG_HOSTNAME: pi-gen-test + CONFIG_PUBLIC_KEY: ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBAoK4bf7Tj47S67Mf3aCsRPOcYU2F91xLBJ4U4n9jqgsAf75NWFX5UfoRQhWsGVsCYfA84ZTYIrIHMw57CLA2gM= a@b.org jobs: @@ -57,6 +58,7 @@ jobs: wpa-essid: foo wpa-password: '1234567890' timezone: ${{ env.CONFIG_TIMEZONE }} + pubkey-ssh-first-user: ${{ env.CONFIG_PUBLIC_KEY }} - name: List working directory run: tree @@ -77,6 +79,7 @@ jobs: source ${ROOTFS_DIR}/etc/default/locale test "$LANG" = "$CONFIG_LOCALE" test "$(cat ${ROOTFS_DIR}/etc/timezone)" = "$CONFIG_TIMEZONE" + test "$(sudo cat ${ROOTFS_DIR}/home/${CONFIG_USERNAME}/.ssh/authorized_keys)" = "$CONFIG_PUBLIC_KEY" - name: Remove test label from PR (if set) uses: actions-ecosystem/action-remove-labels@v1 diff --git a/README.md b/README.md index 9609a88..029a189 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,17 @@ tries to make sure the stage is respected and its changes are included in the fi # the pi-gen repository. pi-gen-version: arm64 + # Setting to `1` will disable password authentication for SSH and enable public + # key authentication. Note that if SSH is not enabled this will take effect when + # SSH becomes enabled. + pubkey-only-ssh: 0 + + # Setting this to a value will make that value the contents of the + # FIRST_USER_NAME's ~/.ssh/authorized_keys. Obviously the value should therefore + # be a valid authorized_keys file. Note that this does not automatically enable + # SSH. + pubkey-ssh-first-user: '' + # The release version to build images against. Valid values are jessie, stretch, # buster, bullseye, bookworm, and testing. release: bookworm diff --git a/__test__/pi-gen-config.test.ts b/__test__/pi-gen-config.test.ts index e2a7731..7bcb4d9 100644 --- a/__test__/pi-gen-config.test.ts +++ b/__test__/pi-gen-config.test.ts @@ -126,6 +126,21 @@ describe('PiGenConfig', () => { 'exportLastStageOnly', 'no', 'export-last-stage-only must be either set to "true" or "false", was: no' + ], + [ + 'enableSsh', + 'yes', + 'enable-ssh must be set to either "0" or "1" but was: yes' + ], + [ + 'pubkeyOnlySsh', + 'yes', + 'pubkey-only-ssh must be set to either "0" or "1" but was: yes' + ], + [ + 'pubkeySshFirstUser', + 'ssh-foo vnqf493rn34xzrm234yru13ß48rnz1x034ztn== foo@bar.com', + 'pubkey-ssh-first-user does not seem to be a valid list of public key according to "ssh-keygen -l", here\'s its output' ] ])( 'rejects %s with invalid value %s', @@ -161,4 +176,15 @@ describe('PiGenConfig', () => { ) } }) + + it('should accept valid public key file', async () => { + const piGenConfig = { + ...DEFAULT_CONFIG, + stageList: ['stage0', 'stage1', 'stage2'], + pubkeySshFirstUser: `ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAF71eRtNA2CqGiKYQLI6ozVyW1XKJUXOqkH1r3ZWIruDvckuwxBUZYMvB5si4PkteJqKJnFsO74LesgxTvacxNELgHvxXCJ4XmuT0O7XujwrFCO6dARYyf+RUO5XKt0LegmbqMq3faE7SMmVJnl39quLWojGZ8kUUeS6rg089l7X9LxBA== foo@bar.com + ssh-dss AAAAB3NzaC1kc3MAAAEBAI95Ndm5qum/q+2Ies9JUbbzLsWeO683GOjqxJYfPv02BudDUanEGDM5uAnnwq4cU5unR1uF0BGtuLR5h3VJhGlcrA6PFLM2CCiiL/onEQo9YqmTRTQJoP5pbEZY+EvdIIGcNwmgEFexla3NACM9ulSEtikfnWSO+INEhneXnOwEtDSmrC516Zhd4j2wKS/BEYyf+p2BgeczjbeStzDXueNJWS9oCZhyFTkV6j1ri0ZTxjNFj4A7MqTC4PJykCVuTj+KOwg4ocRQ5OGMGimjfd9eoUPeS2b/BJA+1c8WI+FY1IfGCOl/IRzYHcojy244B2X4IuNCvkhMBXY5OWAc1mcAAAAdALr2lqaFePff3uf6Z8l3x4XvMrIzuuWAwLzVaV0AAAEAFqZcWCBIUHBOdQKjl1cEDTTaOjR4wVTU5KXALSQu4E+W5h5L0JBKvayPN+6x4J8xgtI8kEPLZC+IAEFg7fnKCbMgdqecMqYn8kc+kYebosTnRL0ggVRMtVuALDaNH6g+1InpTg+gaI4yQopceMR4xo0FJ7ccmjq7CwvhLERoljnn08502xAaZaorh/ZMaCbbPscvS1WZg0u07bAvfJDppJbTpV1TW+v8RdT2GfY/Pe27hzklwvIk4HcxKW2oh+weR0j4fvtf3rdUhDFrIjLe5VPdrwIRKw0fAtowlzIk/ieu2oudSyki2bqL457Z4QOmPFKBC8aIt+LtQxbh7xfb3gAAAQAG2DjHpzzWGYtVLzMRfXwRFmVNwOO1Rg7ZmLjcy0hWy2b2JzeYJJSj+mRa/GC/Si3e16b0nBJGWU6FcTGSzPOdU3xrMJGLqtIlnUyqS5UJf75xs7zJamuSJ/QMLsvzqglaFBygL5Iuc5KF8lluXFK9h4ggCYxJp2UhgpsX6QMAl7ITeTHFdGWs/nwBHafEwxY3DViTmrj7wXuz8QBzzh65+lIrbOnibg3gliBg77bFLfVEFdylu3f5R18c8sZ9U0c4DOA+ZGlmAqSHZOQtyf8p8T+yKbMBJaQ/R+y0qG/R5Ai1d/aBcGZ1W5b/BU9Z+6yb7ITGowGzObBhU3L4g22e test@example.org` + } + + expect(await validateConfig(piGenConfig)).toBeUndefined() + }) }) diff --git a/action.yml b/action.yml index f6a0b57..acc692f 100644 --- a/action.yml +++ b/action.yml @@ -84,6 +84,18 @@ inputs: description: Enable SSH access to Pi. required: false default: 0 + pubkey-ssh-first-user: + description: | + Setting this to a value will make that value the contents of the FIRST_USER_NAME's ~/.ssh/authorized_keys. + Obviously the value should therefore be a valid authorized_keys file. Note that this does not automatically + enable SSH. + required: false + pubkey-only-ssh: + description: | + Setting to `1` will disable password authentication for SSH and enable public key authentication. Note that if + SSH is not enabled this will take effect when SSH becomes enabled. + required: false + default: 0 docker-opts: description: Additional options to include in PIGEN_DOCKER_OPTS required: false diff --git a/jest.config.js b/jest.config.js index 9986c04..55a2e30 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,8 +3,8 @@ module.exports = { collectCoverageFrom: ['src/**/*.ts', '!src/misc/update-readme.ts'], coverageThreshold: { global: { - statements: 96, - branches: 91, + statements: 97, + branches: 92, functions: 96, lines: 97 } diff --git a/package-lock.json b/package-lock.json index da0f85d..23b2f73 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,8 @@ "@actions/glob": "^0.4.0", "@actions/io": "^1.1.3", "ansi-colors": "^4.1.3", - "json-colorizer": "^3.0.1" + "json-colorizer": "^3.0.1", + "tmp": "0.2.3" }, "devDependencies": { "@types/jest": "29.5.12", @@ -38,7 +39,6 @@ "markdown-replace-section": "0.4.0", "prettier": "3.2.5", "semver": "7.6.0", - "tmp": "0.2.3", "ts-jest": "29.1.2", "ts-node": "10.9.2", "typescript": "5.4.5", @@ -9561,7 +9561,6 @@ "version": "0.2.3", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", - "dev": true, "engines": { "node": ">=14.14" } @@ -17946,8 +17945,7 @@ "tmp": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", - "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", - "dev": true + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==" }, "tmpl": { "version": "1.0.5", diff --git a/package.json b/package.json index b464624..5c96c31 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,8 @@ "@actions/glob": "^0.4.0", "@actions/io": "^1.1.3", "ansi-colors": "^4.1.3", - "json-colorizer": "^3.0.1" + "json-colorizer": "^3.0.1", + "tmp": "0.2.3" }, "devDependencies": { "@types/jest": "29.5.12", @@ -58,7 +59,6 @@ "markdown-replace-section": "0.4.0", "prettier": "3.2.5", "semver": "7.6.0", - "tmp": "0.2.3", "ts-jest": "29.1.2", "ts-node": "10.9.2", "typescript": "5.4.5", diff --git a/src/configure.ts b/src/configure.ts index dc661a3..6ecd63e 100644 --- a/src/configure.ts +++ b/src/configure.ts @@ -43,6 +43,9 @@ export async function configure(): Promise { core.getInput('wpa-country') || DEFAULT_CONFIG.wpaCountry userConfig.enableSsh = core.getInput('enable-ssh') || DEFAULT_CONFIG.enableSsh + userConfig.pubkeySshFirstUser = core.getInput('pubkey-ssh-first-user') + userConfig.pubkeyOnlySsh = + core.getInput('pubkey-only-ssh') || DEFAULT_CONFIG.pubkeyOnlySsh userConfig.enableNoobs = core.getBooleanInput('enable-noobs')?.toString() || DEFAULT_CONFIG.enableNoobs diff --git a/src/host-dependencies.ts b/src/host-dependencies.ts index 70051a2..d0f2bf1 100644 --- a/src/host-dependencies.ts +++ b/src/host-dependencies.ts @@ -4,7 +4,8 @@ export const hostDependencies = { 'qemu-user-static', 'qemu-utils', 'nbd-server', - 'nbd-client' + 'nbd-client', + 'openssh-client' ], modules: ['binfmt_misc', 'nbd'] } diff --git a/src/pi-gen-config.ts b/src/pi-gen-config.ts index 54e1b89..bc4e04a 100644 --- a/src/pi-gen-config.ts +++ b/src/pi-gen-config.ts @@ -1,4 +1,5 @@ import * as fs from 'fs/promises' +import * as tmp from 'tmp' import {PiGenStages} from './pi-gen-stages' import * as core from '@actions/core' import * as exec from '@actions/exec' @@ -172,6 +173,35 @@ export async function validateConfig(config: PiGenConfig): Promise { ) } + if (!/^[01]$/.test(config.enableSsh)) { + throw new Error( + `enable-ssh must be set to either "0" or "1" but was: ${config.enableSsh}` + ) + } + + if (!/^[01]$/.test(config.pubkeyOnlySsh)) { + throw new Error( + `pubkey-only-ssh must be set to either "0" or "1" but was: ${config.pubkeyOnlySsh}` + ) + } + + if (config.pubkeySshFirstUser) { + const tempAuthorizedKeysFile = tmp.fileSync() + await fs.writeFile(tempAuthorizedKeysFile.name, config.pubkeySshFirstUser) + + const sshKeygenCmd = await io.which('ssh-keygen', true) + const sshKeygenOutput = await exec.getExecOutput( + sshKeygenCmd, + ['-l', '-f', tempAuthorizedKeysFile.name], + {silent: true, ignoreReturnCode: true} + ) + if (sshKeygenOutput.exitCode !== 0) { + throw new Error( + `pubkey-ssh-first-user does not seem to be a valid list of public key according to "ssh-keygen -l", here's its output:\n${sshKeygenOutput.stderr}` + ) + } + } + if (!config.stageList || config.stageList.length === 0) { throw new Error('stage-list must not be empty') }