Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/moody-schools-yawn.md
Original file line number Diff line number Diff line change
@@ -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.
35 changes: 33 additions & 2 deletions apps/draupnir/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ 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) &&
Expand Down Expand Up @@ -75,6 +81,11 @@ export interface IConfig {
username: string;
password: string;
};
zeroTouchDeploymentSelfLogin: {
enabled: boolean;
username: string;
password: string;
};
Comment on lines +84 to +88
Copy link
Copy Markdown
Member

@Gnuxie Gnuxie May 5, 2026

Choose a reason for hiding this comment

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

In external config can we call this something sensible, like what the fuck is this for? What kind of crazy thing are we working around in MAS with this?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Gnuxie on a less alarmist note we are also compatible with all matrix homeservers that are currently actually used by people with this method unlike trying to go MAS native and have to maintain a bunch of bullshit to be compatible with all homeserver projects.

Though mdad does only have support for Turnkey user creation (the dependency we use in draupnir for ztd stage 2 mdad) for Synapse as far as i know.

dataPath: string;
/**
* If true, Draupnir will only accept invites from users present in managementRoom.
Expand Down Expand Up @@ -186,7 +197,8 @@ export interface IConfig {
isDraupnirConfigOptionUsed: boolean;

isAccessTokenPathOptionUsed: boolean;
isPasswordPathOptionUsed: boolean;
isPantalaimonPasswordOptionUsed: boolean;
isZeroTouchDeploymentSelfLoginPasswordOptionUsed: boolean;
isHttpAntispamAuthorizationPathOptionUsed: boolean;
}
| undefined;
Expand All @@ -201,6 +213,11 @@ const defaultConfig: IConfig = {
username: "",
password: "",
},
zeroTouchDeploymentSelfLogin: {
enabled: false,
username: "",
password: "",
},
dataPath: "/data/storage",
acceptInvitesFromSpace: "!noop:example.org",
autojoinOnlyIfManager: true,
Expand Down Expand Up @@ -315,10 +332,15 @@ function getConfigMeta(): NonNullable<IConfig["configMeta"]> {
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"
Expand Down Expand Up @@ -381,6 +403,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"
Expand All @@ -393,6 +419,11 @@ export function configRead(): IConfig {
explicitPantalaimonPasswordPath
);
}
if (explicitZeroTouchDeploymentSelfLoginPasswordPath) {
config.zeroTouchDeploymentSelfLogin.password = readSecretFromPath(
explicitZeroTouchDeploymentSelfLoginPasswordPath
);
}
if (explicitHttpAntispamAuthorizationPath) {
config.web.synapseHTTPAntispam.authorization = readSecretFromPath(
explicitHttpAntispamAuthorizationPath
Expand Down
8 changes: 8 additions & 0 deletions apps/draupnir/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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(
Expand Down
48 changes: 48 additions & 0 deletions apps/draupnir/src/zeroTouchDeploymentSelfLogin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// SPDX-FileCopyrightText: 2026 Catalan Lover <catalanlover@protonmail.com>
//
// 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<string>;

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<IConfig, "homeserverUrl" | "zeroTouchDeploymentSelfLogin">,
storage: IStorageProvider,
zeroTouchLogin: ZeroTouchLogin = defaultZeroTouchLogin
): Promise<string> {
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;
}
50 changes: 50 additions & 0 deletions apps/draupnir/test/unit/zeroTouchDeploymentSelfLoginTest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// SPDX-FileCopyrightText: 2026 Catalan Lover <catalanlover@protonmail.com>
//
// SPDX-License-Identifier: Apache-2.0

import expect from "expect";
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");
});
});
18 changes: 18 additions & 0 deletions config/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 <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.
Expand Down
Loading