Skip to content
Merged
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
44 changes: 43 additions & 1 deletion docs/operator-runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,34 @@ The subject must already exist as a principal in the policy store
(typically through `wyctl policy role-grant`). Enrollment does not
create principals — it only attaches a TOTP factor to one.

### Setting defaults via GSettings

Operators who enroll multiple subjects against the same policy store and
KeyProvider can stop repeating `--store` and `--keyprovider` on every
invocation by setting the two GSettings keys once:

```sh
gsettings set org.wyrelog.wyctl default-policy-store /var/lib/wyrelog/policy.sqlite
gsettings set org.wyrelog.wyctl default-keyprovider systemd-creds:wyrelog-policy
```

After this, `sudo wyctl mfa enroll --subject alice` (no `--store`, no
`--keyprovider`) resolves both paths from GSettings. `--subject` is
**not** a GSettings-backed key — it is always passed explicitly per
enrollment, because every enrollment targets exactly one principal.

Precedence is **CLI > GSettings > error**: an explicit `--store` or
`--keyprovider` on the command line still wins over the GSettings value,
and if neither is set the existing per-flag missing diagnostic fires.
The existing kill switch `WYCTL_DISABLE_GSETTINGS=1` (the literal
string `1`) disables the GSettings fallback uniformly across all wyctl
subcommands, including the mfa subcommands, restoring the pre-GSettings
"CLI-or-nothing" behaviour for incident-response or CI runs.

See the *wyctl Configuration and Token-File Safety* section below for
the full key reference and the surrounding precedence/kill-switch
machinery.

### Recovery and Reset

There are no user-side backup codes in v0. The only recovery path is
Expand Down Expand Up @@ -928,7 +956,12 @@ not need to be repeated on every invocation, while bearer-token bytes are
loaded only from a protected on-disk token file. Explicit CLI flags always
override GSettings. The GSettings store records only the *path* to the
token file; the bytes themselves never live in dconf, the keyfile backend,
or any other GSettings backing store.
or any other GSettings backing store. The same path-only / spec-only
discipline applies to the MFA defaults: `default-policy-store` records
the policy-store path, and `default-keyprovider` records the KeyProvider
*spec string* (e.g. `file:/etc/wyrelog/policy.key`,
`systemd-creds:wyrelog-policy`). The KeyProvider key material, the TOTP
seed bytes, and the policy-store contents never live in GSettings.

Daemon defaults live in `/etc/wyrelog/wyrelogd.conf` (see the previous
section) — wyctl and wyrelogd intentionally do **not** share a single
Expand Down Expand Up @@ -962,6 +995,8 @@ honest about which surface acted.
| `default-guard-loc-class` | `s` | `""` | Location class used when `--guard-loc-class` is omitted. |
| `default-guard-risk` | `i` | `-1` | Risk score (0..100) used when `--guard-risk` is omitted. `-1` is the "unset" sentinel because `0` is a real risk score. |
| `default-guard-timestamp-mode` | `s` | `"none"` | Strategy for filling `--guard-timestamp` when omitted. `"none"` preserves the historical "must be supplied" behaviour; `"now"` is reserved for a future commit that fills the current wall-clock time. |
| `default-policy-store` | `s` | `""` | Backs `--store` for `wyctl mfa enroll|reset`. Policy-store path (SQLite file). Empty = "no default; CLI must supply." |
| `default-keyprovider` | `s` | `""` | Backs `--keyprovider` for `wyctl mfa enroll|reset`. KeyProvider spec (e.g. `file:/etc/wyrelog/policy.key`). Empty = "no default; CLI must supply." |

Example: configure the operator workstation once and let every wyctl
invocation pick up the defaults.
Expand All @@ -988,6 +1023,13 @@ of:
3. **Unset** — the existing per-flag "missing" diagnostic fires
(`wyctl: missing daemon URL`, `wyctl: missing --tenant`, etc.).

The `wyctl mfa enroll` and `wyctl mfa reset` subcommands participate in
the same resolver pipeline: `--store` falls back to `default-policy-store`
and `--keyprovider` falls back to `default-keyprovider` under the same
precedence rule (CLI > GSettings > missing-flag diagnostic). The
`WYCTL_DISABLE_GSETTINGS=1` kill switch documented below disables the
fallback uniformly across all wyctl subcommands, mfa included.

### Kill Switch: `WYCTL_DISABLE_GSETTINGS`

Set `WYCTL_DISABLE_GSETTINGS=1` (the **literal string `1`** — `true`,
Expand Down
8 changes: 7 additions & 1 deletion tests/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -312,8 +312,14 @@ if build_client and host_machine.system() != 'windows'
dependencies : [wyrelog_dep, glib_dep, gio_dep, sqlite_dep, sodium_dep],
)

# The MFA harness now exercises the GSettings fallback for --store /
# --keyprovider (issue #333), so it needs the compiled schema on
# GSETTINGS_SCHEMA_DIR and the memory backend, just like the
# wyctl-config / wyctl-basic tests. Per-test XDG-keyfile fixtures
# then layer a real GSettings keyfile on top of that schema dir.
test('wyctl-mfa', test_wyctl_mfa,
depends : [wyctl_exe],
depends : [wyctl_exe, wyctl_gschemas_compiled],
env : wyctl_gsettings_test_env,
timeout : 60,
)

Expand Down
91 changes: 91 additions & 0 deletions tests/test-wyctl-config.c
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ fresh_settings (void)
"default-guard-loc-class",
"default-guard-risk",
"default-guard-timestamp-mode",
"default-policy-store",
"default-keyprovider",
};
for (gsize i = 0; i < G_N_ELEMENTS (keys); i++)
g_settings_reset (settings, keys[i]);
Expand Down Expand Up @@ -125,6 +127,79 @@ test_resolve_uint_no_settings_returns_null (void)
g_assert_null (resolved);
}

static void
test_resolve_string_policy_store_cli_wins (void)
{
g_autoptr (GSettings) settings = fresh_settings ();
g_settings_set_string (settings, "default-policy-store",
"/var/lib/wyrelog/from-gsettings.sqlite");

g_autofree gchar *resolved =
wyctl_resolve_string_option ("/tmp/from-cli.sqlite", settings,
"default-policy-store");
g_assert_cmpstr (resolved, ==, "/tmp/from-cli.sqlite");
}

static void
test_resolve_string_policy_store_falls_back_to_settings (void)
{
g_autoptr (GSettings) settings = fresh_settings ();
g_settings_set_string (settings, "default-policy-store",
"/var/lib/wyrelog/from-gsettings.sqlite");

g_autofree gchar *resolved = wyctl_resolve_string_option (NULL, settings,
"default-policy-store");
g_assert_cmpstr (resolved, ==, "/var/lib/wyrelog/from-gsettings.sqlite");
}

static void
test_resolve_string_policy_store_empty_settings_is_unset (void)
{
g_autoptr (GSettings) settings = fresh_settings ();
/* Schema default is the empty string. Symmetry with daemon-url:
no CLI value + empty-string in GSettings must surface as NULL so
the caller's "missing --store" diagnostic fires unchanged. */
g_autofree gchar *resolved = wyctl_resolve_string_option (NULL, settings,
"default-policy-store");
g_assert_null (resolved);
}

static void
test_resolve_string_keyprovider_cli_wins (void)
{
g_autoptr (GSettings) settings = fresh_settings ();
g_settings_set_string (settings, "default-keyprovider",
"systemd-creds:wyrelog-policy-from-gsettings");

g_autofree gchar *resolved =
wyctl_resolve_string_option ("file:/etc/wyrelog/keyprovider.key",
settings, "default-keyprovider");
g_assert_cmpstr (resolved, ==, "file:/etc/wyrelog/keyprovider.key");
}

static void
test_resolve_string_keyprovider_falls_back_to_settings (void)
{
g_autoptr (GSettings) settings = fresh_settings ();
g_settings_set_string (settings, "default-keyprovider",
"systemd-creds:wyrelog-policy");

g_autofree gchar *resolved = wyctl_resolve_string_option (NULL, settings,
"default-keyprovider");
g_assert_cmpstr (resolved, ==, "systemd-creds:wyrelog-policy");
}

static void
test_resolve_string_keyprovider_empty_settings_is_unset (void)
{
g_autoptr (GSettings) settings = fresh_settings ();
/* Empty-string symmetry: matches the daemon-url test at line ~78
and the policy-store equivalent above. */
g_autofree gchar *resolved = wyctl_resolve_string_option (NULL, settings,
"default-keyprovider");
g_assert_null (resolved);
}

static void
test_open_settings_respects_kill_switch (void)
{
Expand Down Expand Up @@ -187,6 +262,22 @@ main (int argc, char **argv)
test_resolve_uint_renders_settings_value);
g_test_add_func ("/wyctl/config/resolve-uint/no-settings-returns-null",
test_resolve_uint_no_settings_returns_null);
g_test_add_func ("/wyctl/config/resolve-string/policy-store-cli-wins",
test_resolve_string_policy_store_cli_wins);
g_test_add_func
("/wyctl/config/resolve-string/policy-store-falls-back-to-settings",
test_resolve_string_policy_store_falls_back_to_settings);
g_test_add_func
("/wyctl/config/resolve-string/policy-store-empty-settings-is-unset",
test_resolve_string_policy_store_empty_settings_is_unset);
g_test_add_func ("/wyctl/config/resolve-string/keyprovider-cli-wins",
test_resolve_string_keyprovider_cli_wins);
g_test_add_func
("/wyctl/config/resolve-string/keyprovider-falls-back-to-settings",
test_resolve_string_keyprovider_falls_back_to_settings);
g_test_add_func
("/wyctl/config/resolve-string/keyprovider-empty-settings-is-unset",
test_resolve_string_keyprovider_empty_settings_is_unset);
g_test_add_func ("/wyctl/config/open/respects-kill-switch",
test_open_settings_respects_kill_switch);
g_test_add_func ("/wyctl/config/open/handle-when-schema-present",
Expand Down
4 changes: 4 additions & 0 deletions tests/test-wyctl-gschema.c
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ test_schema_keys_and_types (void)
{"default-guard-loc-class", G_VARIANT_TYPE_STRING},
{"default-guard-risk", G_VARIANT_TYPE_INT32},
{"default-guard-timestamp-mode", G_VARIANT_TYPE_STRING},
{"default-policy-store", G_VARIANT_TYPE_STRING},
{"default-keyprovider", G_VARIANT_TYPE_STRING},
};

for (gsize i = 0; i < G_N_ELEMENTS (expected); i++) {
Expand Down Expand Up @@ -89,6 +91,8 @@ test_schema_defaults_safe (void)
"default-graph",
"access-token-file",
"default-guard-loc-class",
"default-policy-store",
"default-keyprovider",
};

for (gsize i = 0; i < G_N_ELEMENTS (string_keys); i++) {
Expand Down
Loading
Loading