fix(#1185): move matrix-password outside purgeable agent_state_root
The password file was stored at agent_notes_dir/matrix-password which lives inside agent_state_root — wiped by destroy --purge. On re-spawn with the same agent name, the matrix user still exists in the homeserver but the stored password is gone, making re-login impossible. Move password to /var/lib/hyperhive/matrix-creds/<name>-password which is not deleted by purge. On re-spawn, ensure_user_for finds M_USER_IN_USE, reads the preserved password, re-logins, and writes a fresh token. Also: - add one-time migration that moves existing passwords from the old path to the new location on first access after upgrade - remove chown_to_agent on the password file (it is now host-only, not inside the agent bind-mount tree) - fix the error message to give actionable recovery steps instead of suggesting hivectl matrix create-user --password which is rejected for agent accounts
This commit is contained in:
parent
181535650b
commit
47e9c1cc1b
1 changed files with 53 additions and 6 deletions
|
|
@ -45,10 +45,27 @@ fn token_path(name: &str) -> PathBuf {
|
||||||
Coordinator::agent_notes_dir(name).join("matrix-token")
|
Coordinator::agent_notes_dir(name).join("matrix-token")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Password file alongside the token. Persisted so we can fall back to
|
/// Password file for the agent's matrix account. Stored OUTSIDE the
|
||||||
/// `m.login.password` if the token file is deleted but the homeserver
|
/// purgeable `agent_state_root` tree so it survives `destroy --purge`
|
||||||
/// account still exists. Mode 0600, same dir as the token.
|
/// and allows re-login recovery when the same agent name is re-spawned.
|
||||||
|
///
|
||||||
|
/// Path: `/var/lib/hyperhive/matrix-creds/<name>-password`
|
||||||
|
///
|
||||||
|
/// The token file lives inside the agent's bind-mounted state dir (under
|
||||||
|
/// `agent_notes_dir`) so the agent container can read it; the password
|
||||||
|
/// file is host-side only (agents never log in by password — they use
|
||||||
|
/// the access token exclusively) and belongs with other hive-c0re
|
||||||
|
/// credential state, not inside the purgeable per-agent tree.
|
||||||
fn password_path(name: &str) -> PathBuf {
|
fn password_path(name: &str) -> PathBuf {
|
||||||
|
PathBuf::from("/var/lib/hyperhive/matrix-creds").join(format!("{name}-password"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Legacy password path (inside the old purgeable `agent_notes_dir`).
|
||||||
|
/// Used only during the one-time migration in [`ensure_user_for`] to
|
||||||
|
/// move credentials from old deployments to the new location. Safe to
|
||||||
|
/// call after `destroy --purge` — the path will simply not exist and
|
||||||
|
/// the migration is a no-op.
|
||||||
|
fn legacy_password_path(name: &str) -> PathBuf {
|
||||||
Coordinator::agent_notes_dir(name).join("matrix-password")
|
Coordinator::agent_notes_dir(name).join("matrix-password")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -276,6 +293,29 @@ pub async fn ensure_user_for(
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// One-time migration: move the password from the old location inside
|
||||||
|
// agent_notes_dir (purgeable) to the new location outside it.
|
||||||
|
let new_pw_path = password_path(name);
|
||||||
|
let old_pw_path = legacy_password_path(name);
|
||||||
|
if !new_pw_path.exists() && old_pw_path.exists() {
|
||||||
|
if let Some(parent) = new_pw_path.parent() {
|
||||||
|
std::fs::create_dir_all(parent).ok();
|
||||||
|
}
|
||||||
|
if let Err(e) = std::fs::rename(&old_pw_path, &new_pw_path) {
|
||||||
|
// Rename across filesystems or read-only src — copy + delete.
|
||||||
|
if let Ok(content) = std::fs::read(&old_pw_path) {
|
||||||
|
if std::fs::write(&new_pw_path, &content).is_ok() {
|
||||||
|
let _ = std::fs::remove_file(&old_pw_path);
|
||||||
|
tracing::info!(%name, "matrix: migrated password file to non-purgeable location");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tracing::warn!(%name, error = ?e, "matrix: password migration failed (old path stays)");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tracing::info!(%name, "matrix: migrated password file to non-purgeable location");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let password = random_password()?;
|
let password = random_password()?;
|
||||||
let access_token = match register_user(client, name, register_token, &password).await {
|
let access_token = match register_user(client, name, register_token, &password).await {
|
||||||
Ok(token) => {
|
Ok(token) => {
|
||||||
|
|
@ -289,7 +329,6 @@ pub async fn ensure_user_for(
|
||||||
tracing::warn!(%name, error = ?e, "matrix: failed to persist password (token still saved)");
|
tracing::warn!(%name, error = ?e, "matrix: failed to persist password (token still saved)");
|
||||||
} else {
|
} else {
|
||||||
let _ = std::fs::set_permissions(&pw_path, std::fs::Permissions::from_mode(0o600));
|
let _ = std::fs::set_permissions(&pw_path, std::fs::Permissions::from_mode(0o600));
|
||||||
crate::lifecycle::chown_to_agent(name, &pw_path, "matrix");
|
|
||||||
}
|
}
|
||||||
token
|
token
|
||||||
}
|
}
|
||||||
|
|
@ -303,8 +342,16 @@ pub async fn ensure_user_for(
|
||||||
.filter(|s| !s.is_empty())
|
.filter(|s| !s.is_empty())
|
||||||
.with_context(|| {
|
.with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
"matrix: user {name} already exists in homeserver but matrix-password \
|
"matrix: user {name} already exists in homeserver but the stored \
|
||||||
is missing — manual recovery: hivectl matrix create-user {name} --password <pw>"
|
password is missing — manual recovery:\n\
|
||||||
|
1. reset the password via the matrix admin API:\n\
|
||||||
|
curl -X PUT http://localhost:8008/_synapse/admin/v2/users/@{name}:<server> \\\n\
|
||||||
|
-H 'Authorization: Bearer <admin-token>' \\\n\
|
||||||
|
-d '{{\"password\": \"<new-pw>\"}}'\n\
|
||||||
|
2. write the new password to {pw_path}:\n\
|
||||||
|
echo '<new-pw>' > {pw_path} && chmod 600 {pw_path}\n\
|
||||||
|
3. run: hivectl matrix create-user {name}",
|
||||||
|
pw_path = pw_path.display()
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
login_user(client, name, &stored).await.with_context(|| {
|
login_user(client, name, &stored).await.with_context(|| {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue