From 9666a3d0781c07c501bfb8b312884aa4494fd46a Mon Sep 17 00:00:00 2001 From: Catalan Lover Date: Mon, 4 May 2026 17:02:33 +0200 Subject: [PATCH 1/4] Zero Touch Deployment Stage 2 Bot Mode initial commit --- apps/draupnir/src/config.ts | 31 ++++++++++- apps/draupnir/src/index.ts | 8 +++ .../src/zeroTouchDeploymentSelfLogin.ts | 48 +++++++++++++++++ .../unit/zeroTouchDeploymentSelfLoginTest.ts | 54 +++++++++++++++++++ config/default.yaml | 18 +++++++ 5 files changed, 157 insertions(+), 2 deletions(-) create mode 100644 apps/draupnir/src/zeroTouchDeploymentSelfLogin.ts create mode 100644 apps/draupnir/test/unit/zeroTouchDeploymentSelfLoginTest.ts diff --git a/apps/draupnir/src/config.ts b/apps/draupnir/src/config.ts index 77a73918..896f8efc 100644 --- a/apps/draupnir/src/config.ts +++ b/apps/draupnir/src/config.ts @@ -47,6 +47,9 @@ export function getNonDefaultConfigProperties( if ("pantalaimon" in nonDefault && isConfigRecord(nonDefault.pantalaimon)) { nonDefault.pantalaimon.password = "REDACTED"; } + if ("zeroTouchDeploymentSelfLogin" in nonDefault && isConfigRecord(nonDefault.zeroTouchDeploymentSelfLogin)) { + nonDefault.zeroTouchDeploymentSelfLogin.password = "REDACTED"; + } if ( "web" in nonDefault && isConfigRecord(nonDefault.web) && @@ -75,6 +78,11 @@ export interface IConfig { username: string; password: string; }; + zeroTouchDeploymentSelfLogin: { + enabled: boolean; + username: string; + password: string; + }; dataPath: string; /** * If true, Draupnir will only accept invites from users present in managementRoom. @@ -186,7 +194,8 @@ export interface IConfig { isDraupnirConfigOptionUsed: boolean; isAccessTokenPathOptionUsed: boolean; - isPasswordPathOptionUsed: boolean; + isPantalaimonPasswordOptionUsed: boolean; + isZeroTouchDeploymentSelfLoginPasswordOptionUsed: boolean; isHttpAntispamAuthorizationPathOptionUsed: boolean; } | undefined; @@ -201,6 +210,11 @@ const defaultConfig: IConfig = { username: "", password: "", }, + zeroTouchDeploymentSelfLogin: { + enabled: false, + username: "", + password: "", + }, dataPath: "/data/storage", acceptInvitesFromSpace: "!noop:example.org", autojoinOnlyIfManager: true, @@ -315,10 +329,14 @@ function getConfigMeta(): NonNullable { process.argv, "--access-token-path" ), - isPasswordPathOptionUsed: isCommandLineOptionPresent( + isPantalaimonPasswordOptionUsed: isCommandLineOptionPresent( process.argv, "--pantalaimon-password-path" ), + isZeroTouchDeploymentSelfLoginPasswordOptionUsed: isCommandLineOptionPresent( + process.argv, + "--zero-touch-deployment-self-login-password-path" + ), isHttpAntispamAuthorizationPathOptionUsed: isCommandLineOptionPresent( process.argv, "--http-antispam-authorization-path" @@ -381,6 +399,10 @@ export function configRead(): IConfig { process.argv, "--pantalaimon-password-path" ); + const explicitZeroTouchDeploymentSelfLoginPasswordPath = getCommandLineOption( + process.argv, + "--zero-touch-deployment-self-login-password-path" + ); const explicitHttpAntispamAuthorizationPath = getCommandLineOption( process.argv, "--http-antispam-authorization-path" @@ -393,6 +415,11 @@ export function configRead(): IConfig { explicitPantalaimonPasswordPath ); } + if (explicitZeroTouchDeploymentSelfLoginPasswordPath) { + config.zeroTouchDeploymentSelfLogin.password = readSecretFromPath( + explicitZeroTouchDeploymentSelfLoginPasswordPath + ); + } if (explicitHttpAntispamAuthorizationPath) { config.web.synapseHTTPAntispam.authorization = readSecretFromPath( explicitHttpAntispamAuthorizationPath diff --git a/apps/draupnir/src/index.ts b/apps/draupnir/src/index.ts index 2e344e5b..741a87f7 100644 --- a/apps/draupnir/src/index.ts +++ b/apps/draupnir/src/index.ts @@ -25,6 +25,7 @@ import { DraupnirBotModeToggle } from "./DraupnirBotMode"; import { SafeMatrixEmitterWrapper } from "matrix-protection-suite-for-matrix-bot-sdk"; import { DefaultEventDecoder } from "matrix-protection-suite"; import { makeTopLevelStores } from "./backingstore/DraupnirStores"; +import { getZeroTouchDeploymentAccessToken } from "./zeroTouchDeploymentSelfLogin"; void (async function () { const config = configRead(); @@ -51,6 +52,13 @@ void (async function () { path.join(storagePath, "bot.json") ); + if (config.zeroTouchDeploymentSelfLogin.enabled) { + config.accessToken = await getZeroTouchDeploymentAccessToken( + config, + storage + ); + } + if (config.pantalaimon.use && !config.experimentalRustCrypto) { const pantalaimon = new PantalaimonClient(config.homeserverUrl, storage); client = await pantalaimon.createClientWithCredentials( diff --git a/apps/draupnir/src/zeroTouchDeploymentSelfLogin.ts b/apps/draupnir/src/zeroTouchDeploymentSelfLogin.ts new file mode 100644 index 00000000..88e05728 --- /dev/null +++ b/apps/draupnir/src/zeroTouchDeploymentSelfLogin.ts @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2026 Catalan Lover +// +// SPDX-License-Identifier: Apache-2.0 + +import { MatrixAuth } from "@vector-im/matrix-bot-sdk"; +import type { IStorageProvider } from "@vector-im/matrix-bot-sdk"; +import type { IConfig } from "./config"; + +const ZERO_TOUCH_ACCESS_TOKEN_KEY = "zero_touch_access_token"; + +type ZeroTouchLogin = ( + homeserverUrl: string, + username: string, + password: string +) => Promise; + +const defaultZeroTouchLogin: ZeroTouchLogin = async ( + homeserverUrl, + username, + password +) => { + const auth = new MatrixAuth(homeserverUrl); + const client = await auth.passwordLogin(username, password); + return client.accessToken; +}; + +export async function getZeroTouchDeploymentAccessToken( + config: Pick, + storage: IStorageProvider, + zeroTouchLogin: ZeroTouchLogin = defaultZeroTouchLogin +): Promise { + const storedToken = await Promise.resolve( + storage.readValue(ZERO_TOUCH_ACCESS_TOKEN_KEY) + ); + if (storedToken) { + return storedToken; + } + + const accessToken = await zeroTouchLogin( + config.homeserverUrl, + config.zeroTouchDeploymentSelfLogin.username, + config.zeroTouchDeploymentSelfLogin.password + ); + await Promise.resolve( + storage.storeValue(ZERO_TOUCH_ACCESS_TOKEN_KEY, accessToken) + ); + return accessToken; +} diff --git a/apps/draupnir/test/unit/zeroTouchDeploymentSelfLoginTest.ts b/apps/draupnir/test/unit/zeroTouchDeploymentSelfLoginTest.ts new file mode 100644 index 00000000..0d6e890d --- /dev/null +++ b/apps/draupnir/test/unit/zeroTouchDeploymentSelfLoginTest.ts @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: 2026 Catalan Lover +// +// SPDX-License-Identifier: Apache-2.0 + +import expect from "expect"; +import { describe, it } from "mocha"; +import type { IStorageProvider } from "@vector-im/matrix-bot-sdk"; +import type { IConfig } from "../../src/config"; +import { getZeroTouchDeploymentAccessToken } from "../../src/zeroTouchDeploymentSelfLogin"; + +describe("zeroTouchDeploymentSelfLogin", function () { + it("boots a client using the configured zero-touch credentials", async function () { + const calls: Array<[string, string, string]> = []; + const config = { + homeserverUrl: "https://homeserver.example", + zeroTouchDeploymentSelfLogin: { + enabled: true, + username: "bot-user", + password: "bot-password", + }, + } satisfies Pick< + IConfig, + "homeserverUrl" | "zeroTouchDeploymentSelfLogin" + >; + const storage: IStorageProvider = { + setSyncToken() {}, + getSyncToken() { + return null; + }, + setFilter() {}, + getFilter() { + return null as never; + }, + readValue() { + return null; + }, + storeValue() {}, + }; + + const result = await getZeroTouchDeploymentAccessToken( + config, + storage, + async (homeserverUrl, username, password) => { + calls.push([homeserverUrl, username, password]); + return "secret-token"; + } + ); + + expect(calls).toEqual([ + ["https://homeserver.example", "bot-user", "bot-password"], + ]); + expect(result).toEqual("secret-token"); + }); +}); diff --git a/config/default.yaml b/config/default.yaml index e4e42064..b46e8ace 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -32,6 +32,24 @@ pantalaimon: # which would allow using secret management systems such as systemd's service credentials. password: your_password +zeroTouchDeploymentSelfLogin: + # Whether or not Draupnir will use zero-touch deployment self-login. + # The purpose of this is to allow Draupnir to login to the homeserver using a username and password on first startup, + # without needing to pre-provision an access token. + # This is especially useful for zero-touch deployments where pre-provisioning an access token may not be practical. + # Its also straight up easier to use for users who don't want to faf around with curl to obtain an access token. + enabled: false + + # The username to login with. + username: draupnir + + # The password Draupnir will login with. + # + # After successfully logging in once, this will be ignored as long as the access token is stored. + # This option can be loaded from a file by passing "--zero-touch-password-path " at the command line, + # which would allow using secret management systems such as systemd's service credentials. + password: your_password + # Experimental usage of the matrix-bot-sdk rust crypto. # This can not be used with Pantalaimon. # Make sure to setup the bot as if you are not using pantalaimon for this. From 2b92fa1866fb97b88b03e698787a4c467b901f1b Mon Sep 17 00:00:00 2001 From: Catalan Lover Date: Mon, 4 May 2026 17:07:41 +0200 Subject: [PATCH 2/4] Add changeset --- .changeset/moody-schools-yawn.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/moody-schools-yawn.md diff --git a/.changeset/moody-schools-yawn.md b/.changeset/moody-schools-yawn.md new file mode 100644 index 00000000..4ae7f907 --- /dev/null +++ b/.changeset/moody-schools-yawn.md @@ -0,0 +1,6 @@ +--- +"draupnir": patch +--- + +Add Zero Touch Deployment Login option to Draupnir Bot mode. So the bot can +fetch its own access tokens. From d340175c5e0fd0d5d1d5b4944548adb1dbdfa135 Mon Sep 17 00:00:00 2001 From: Catalan Lover Date: Mon, 4 May 2026 17:09:26 +0200 Subject: [PATCH 3/4] Please the linter --- apps/draupnir/src/config.ts | 14 +++++++++----- .../test/unit/zeroTouchDeploymentSelfLoginTest.ts | 5 +---- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/apps/draupnir/src/config.ts b/apps/draupnir/src/config.ts index 896f8efc..f42a7ac4 100644 --- a/apps/draupnir/src/config.ts +++ b/apps/draupnir/src/config.ts @@ -47,7 +47,10 @@ export function getNonDefaultConfigProperties( if ("pantalaimon" in nonDefault && isConfigRecord(nonDefault.pantalaimon)) { nonDefault.pantalaimon.password = "REDACTED"; } - if ("zeroTouchDeploymentSelfLogin" in nonDefault && isConfigRecord(nonDefault.zeroTouchDeploymentSelfLogin)) { + if ( + "zeroTouchDeploymentSelfLogin" in nonDefault && + isConfigRecord(nonDefault.zeroTouchDeploymentSelfLogin) + ) { nonDefault.zeroTouchDeploymentSelfLogin.password = "REDACTED"; } if ( @@ -333,10 +336,11 @@ function getConfigMeta(): NonNullable { process.argv, "--pantalaimon-password-path" ), - isZeroTouchDeploymentSelfLoginPasswordOptionUsed: isCommandLineOptionPresent( - process.argv, - "--zero-touch-deployment-self-login-password-path" - ), + isZeroTouchDeploymentSelfLoginPasswordOptionUsed: + isCommandLineOptionPresent( + process.argv, + "--zero-touch-deployment-self-login-password-path" + ), isHttpAntispamAuthorizationPathOptionUsed: isCommandLineOptionPresent( process.argv, "--http-antispam-authorization-path" diff --git a/apps/draupnir/test/unit/zeroTouchDeploymentSelfLoginTest.ts b/apps/draupnir/test/unit/zeroTouchDeploymentSelfLoginTest.ts index 0d6e890d..adf8de31 100644 --- a/apps/draupnir/test/unit/zeroTouchDeploymentSelfLoginTest.ts +++ b/apps/draupnir/test/unit/zeroTouchDeploymentSelfLoginTest.ts @@ -18,10 +18,7 @@ describe("zeroTouchDeploymentSelfLogin", function () { username: "bot-user", password: "bot-password", }, - } satisfies Pick< - IConfig, - "homeserverUrl" | "zeroTouchDeploymentSelfLogin" - >; + } satisfies Pick; const storage: IStorageProvider = { setSyncToken() {}, getSyncToken() { From ecfb23df5ec95fd9a56e61ac95ecfef6113b22ef Mon Sep 17 00:00:00 2001 From: Catalan Lover Date: Mon, 4 May 2026 18:13:40 +0200 Subject: [PATCH 4/4] Fix Unit tests --- apps/draupnir/test/unit/zeroTouchDeploymentSelfLoginTest.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/draupnir/test/unit/zeroTouchDeploymentSelfLoginTest.ts b/apps/draupnir/test/unit/zeroTouchDeploymentSelfLoginTest.ts index adf8de31..2825e765 100644 --- a/apps/draupnir/test/unit/zeroTouchDeploymentSelfLoginTest.ts +++ b/apps/draupnir/test/unit/zeroTouchDeploymentSelfLoginTest.ts @@ -3,7 +3,6 @@ // SPDX-License-Identifier: Apache-2.0 import expect from "expect"; -import { describe, it } from "mocha"; import type { IStorageProvider } from "@vector-im/matrix-bot-sdk"; import type { IConfig } from "../../src/config"; import { getZeroTouchDeploymentAccessToken } from "../../src/zeroTouchDeploymentSelfLogin";