From 814a1c2994297489aa9a65f09d5274717459fd4f Mon Sep 17 00:00:00 2001
From: David Runge <dvzrv@archlinux.org>
Date: Tue, 18 Mar 2025 20:41:27 +0100
Subject: [PATCH 1/6] feat: Derive `Debug` for `Credentials`

The `Passphrase` type redacts its contents from debug prints, so it is
safe to implement `Debug` for `Credentials`.

Signed-off-by: David Runge <dvzrv@archlinux.org>
---
 nethsm/src/user.rs | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/nethsm/src/user.rs b/nethsm/src/user.rs
index 2ff8caf6..487f23a9 100644
--- a/nethsm/src/user.rs
+++ b/nethsm/src/user.rs
@@ -118,7 +118,7 @@ impl FromStr for NamespaceId {
 
 impl Display for NamespaceId {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        self.0.fmt(f)
+        write!(f, "{}", self.0)
     }
 }
 
@@ -357,7 +357,7 @@ impl FromStr for UserId {
 impl Display for UserId {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         match self {
-            UserId::SystemWide(user_id) => user_id.fmt(f),
+            UserId::SystemWide(user_id) => write!(f, "{user_id}"),
             UserId::Namespace(namespace, name) => write!(f, "{namespace}~{name}"),
         }
     }
@@ -396,6 +396,7 @@ impl TryFrom<String> for UserId {
 /// Credentials for a [`NetHsm`][`crate::NetHsm`]
 ///
 /// Holds a user ID and an accompanying [`Passphrase`].
+#[derive(Debug)]
 pub struct Credentials {
     pub user_id: UserId,
     pub passphrase: Option<Passphrase>,
-- 
GitLab


From c8b7fe335911504d0336153764f7650c0a7f0d37 Mon Sep 17 00:00:00 2001
From: David Runge <dvzrv@archlinux.org>
Date: Thu, 20 Mar 2025 13:55:28 +0100
Subject: [PATCH 2/6] feat: Derive `Clone` for `Credentials`

In some situations it is useful/ needed to be able to clone
`Credentials` when using them in consumers.

Signed-off-by: David Runge <dvzrv@archlinux.org>
---
 nethsm/src/user.rs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/nethsm/src/user.rs b/nethsm/src/user.rs
index 487f23a9..76e2f9df 100644
--- a/nethsm/src/user.rs
+++ b/nethsm/src/user.rs
@@ -396,7 +396,7 @@ impl TryFrom<String> for UserId {
 /// Credentials for a [`NetHsm`][`crate::NetHsm`]
 ///
 /// Holds a user ID and an accompanying [`Passphrase`].
-#[derive(Debug)]
+#[derive(Clone, Debug)]
 pub struct Credentials {
     pub user_id: UserId,
     pub passphrase: Option<Passphrase>,
-- 
GitLab


From 4da4a39cd5e78872feb0f04171464fb53922557c Mon Sep 17 00:00:00 2001
From: David Runge <dvzrv@archlinux.org>
Date: Thu, 6 Feb 2025 20:59:02 +0100
Subject: [PATCH 3/6] feat(justfile): Add recipes for running integration tests
 in containers

Add `containerized-integration-test` recipe to run each test made
available through the `_containerized-integration-test` feature in a
separate Arch Linux container.

Signed-off-by: David Runge <dvzrv@archlinux.org>
---
 .containers/Containerfile.integration-test |  5 ++
 REUSE.toml                                 |  1 +
 justfile                                   | 53 ++++++++++++++++++++++
 3 files changed, 59 insertions(+)
 create mode 100644 .containers/Containerfile.integration-test

diff --git a/.containers/Containerfile.integration-test b/.containers/Containerfile.integration-test
new file mode 100644
index 00000000..70b22062
--- /dev/null
+++ b/.containers/Containerfile.integration-test
@@ -0,0 +1,5 @@
+FROM archlinux
+
+WORKDIR /test
+
+RUN pacman-key --init && pacman -Sy --needed --noconfirm archlinux-keyring && cat .env && source /test/.env && pacman -Syu --needed --noconfirm cargo cargo-nextest
diff --git a/REUSE.toml b/REUSE.toml
index 7b272c7f..11a33249 100644
--- a/REUSE.toml
+++ b/REUSE.toml
@@ -18,6 +18,7 @@ path = [
     "*.json",
     "Cargo.lock",
     ".codespellrc",
+    ".containers/Containerfile.integration-test",
     ".env",
     ".gitignore",
     ".gitlab-ci.yml",
diff --git a/justfile b/justfile
index 2ee731f6..1c3c9f58 100755
--- a/justfile
+++ b/justfile
@@ -608,3 +608,56 @@ watch-book:
     just ensure-command watchexec
     watchexec --exts md,toml,js --delay-run 5s :w
     just build-book
+
+# Returns the target directory for cargo.
+get-cargo-target-dir:
+    just ensure-command cargo jq
+    cargo metadata --format-version 1 | jq -r '.target_directory'
+
+# Returns all names of tests by project and features (the names are cargo-nextest compatible)
+get-tests-by-features project features:
+    just ensure-command cargo cargo-nextest jq
+
+    cargo nextest list --package {{ project }} --no-default-features --features {{ features }} --message-format json | jq -r '."rust-suites"[] | ."binary-id" as $x | ."testcases" | to_entries[] | if (.value."filter-match"."status") == "matches" then (.key) else null end | select(. != null) as $y | "\($x) \($y)"'
+
+# Builds a container image that enables running dedicated integration tests of the project in containers
+build-container-integration-test-image:
+    just ensure-command podman
+
+    podman build --volume "$PWD:/test" --tag arch-signstar-integration-test --file .containers/Containerfile.integration-test .
+
+# Runs each test of a project that is made available with the "_containerized-integration-test" feature in a separate container
+containerized-integration-test project:
+    #!/usr/bin/env bash
+    set -euo pipefail
+
+    just ensure-command cargo-nextest podman
+
+    readonly features=_containerized-integration-test
+    project={{ project }}
+    raw_tests="$(just get-tests-by-features "$project" "$features")"
+    target_dir="$(just get-cargo-target-dir)"
+    readarray -t tests <<< "$raw_tests"
+
+    tmpdir="$(mktemp --dry-run --directory)"
+    readonly test_tmpdir="$tmpdir"
+    mkdir -p "$test_tmpdir"
+
+    # remove temporary dir on exit
+    cleanup() (
+      if [[ -n "${test_tmpdir:-}" ]]; then
+        rm -rf "${test_tmpdir}"
+      fi
+    )
+
+    # create an archive with the special containerized tests
+    cargo nextest archive --archive-file "$test_tmpdir/tests.tar.zst" --features _containerized-integration-test --package "$project"
+
+    for test in "${tests[@]}"; do
+        printf "Running test %s\n" "$test"
+        podman run --rm --interactive --tty --volume "$test_tmpdir:/mnt" --volume "$PWD:/test" --volume "$target_dir/debug/examples:/usr/local/bin" arch-signstar-integration-test cargo nextest run --archive-file "/mnt/tests.tar.zst" --workspace-remap "/test" "${test/ */}" "${test/* /}"
+    done
+
+# Runs the tests that are made available with the "_containerized-integration-test" feature of all configured projects in a separate container
+containerized-integration-tests:
+    just containerized-integration-test signstar-config
-- 
GitLab


From 4905633ee48f8e5b9df0b747309c02bae1fe95fa Mon Sep 17 00:00:00 2001
From: David Runge <dvzrv@archlinux.org>
Date: Mon, 24 Mar 2025 12:56:12 +0100
Subject: [PATCH 4/6] feat: Deny missing docs on the workspace level when
 linting

Signed-off-by: David Runge <dvzrv@archlinux.org>
---
 Cargo.toml | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/Cargo.toml b/Cargo.toml
index 6b970a36..a561d08c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -43,6 +43,9 @@ ureq = "2.12.1"
 uuid = { version = "1.11.0", features = ["v7"] }
 zeroize = { version = "1.8.1", features = ["zeroize_derive", "serde"] }
 
+[workspace.lints.rust]
+missing_docs = "deny"
+
 [workspace.package]
 authors = [
   "David Runge <dvzrv@archlinux.org>",
-- 
GitLab


From f13a0a4113705204652497a60c2948451daa01a4 Mon Sep 17 00:00:00 2001
From: David Runge <dvzrv@archlinux.org>
Date: Fri, 7 Feb 2025 18:15:41 +0100
Subject: [PATCH 5/6] feat: Add `signstar-config` crate to handle Signstar host
 configs

Signed-off-by: David Runge <dvzrv@archlinux.org>
---
 .lycheeignore                                 |   2 +
 Cargo.lock                                    |  65 +-
 Cargo.toml                                    |   1 +
 signstar-config/Cargo.toml                    |  35 +
 signstar-config/README.md                     | 123 +++
 .../examples/get-nethsm-credentials.rs        |  61 ++
 signstar-config/src/admin_credentials.rs      | 567 +++++++++++++
 signstar-config/src/config.rs                 |  41 +
 signstar-config/src/error.rs                  | 396 +++++++++
 signstar-config/src/lib.rs                    |  18 +
 signstar-config/src/non_admin_credentials.rs  | 765 ++++++++++++++++++
 signstar-config/src/utils.rs                  | 198 +++++
 .../tests/admin_credentials/mod.rs            | 182 +++++
 signstar-config/tests/config/mod.rs           |  64 ++
 .../tests/fixtures/admin-creds-simple.toml    |  11 +
 .../tests/fixtures/signstar-config-full.toml  | 129 +++
 .../fixtures/signstar-config-plaintext.toml   | 129 +++
 signstar-config/tests/integration.rs          |  12 +
 .../tests/non_admin_credentials/mod.rs        | 383 +++++++++
 signstar-config/tests/utils/mod.rs            | 424 ++++++++++
 20 files changed, 3597 insertions(+), 9 deletions(-)
 create mode 100644 signstar-config/Cargo.toml
 create mode 100644 signstar-config/README.md
 create mode 100644 signstar-config/examples/get-nethsm-credentials.rs
 create mode 100644 signstar-config/src/admin_credentials.rs
 create mode 100644 signstar-config/src/config.rs
 create mode 100644 signstar-config/src/error.rs
 create mode 100644 signstar-config/src/lib.rs
 create mode 100644 signstar-config/src/non_admin_credentials.rs
 create mode 100644 signstar-config/src/utils.rs
 create mode 100644 signstar-config/tests/admin_credentials/mod.rs
 create mode 100644 signstar-config/tests/config/mod.rs
 create mode 100644 signstar-config/tests/fixtures/admin-creds-simple.toml
 create mode 100644 signstar-config/tests/fixtures/signstar-config-full.toml
 create mode 100644 signstar-config/tests/fixtures/signstar-config-plaintext.toml
 create mode 100644 signstar-config/tests/integration.rs
 create mode 100644 signstar-config/tests/non_admin_credentials/mod.rs
 create mode 100644 signstar-config/tests/utils/mod.rs

diff --git a/.lycheeignore b/.lycheeignore
index 75cbd132..542c1f91 100644
--- a/.lycheeignore
+++ b/.lycheeignore
@@ -7,3 +7,5 @@ https://raw.githubusercontent.com/Nitrokey/nethsm-sdk-py/main/tests/%7B%7D
 # URLs that only become available after release of a component
 https://signstar.archlinux.page/rustdoc/signstar_common/
 https://docs.rs/signstar_common/latest/signstar_common/
+https://signstar.archlinux.page/rustdoc/signstar_config/
+https://docs.rs/signstar_config/latest/signstar_config/
diff --git a/Cargo.lock b/Cargo.lock
index 8a00f64c..c403318a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -247,7 +247,7 @@ dependencies = [
  "rustc-hash",
  "shlex",
  "syn",
- "which",
+ "which 4.4.2",
 ]
 
 [[package]]
@@ -1017,6 +1017,12 @@ dependencies = [
  "cfg-if",
 ]
 
+[[package]]
+name = "env_home"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe"
+
 [[package]]
 name = "equivalent"
 version = "1.0.1"
@@ -3141,6 +3147,28 @@ dependencies = [
  "thiserror 2.0.11",
 ]
 
+[[package]]
+name = "signstar-config"
+version = "0.1.0"
+dependencies = [
+ "confy",
+ "nethsm",
+ "nethsm-config",
+ "nix",
+ "num_enum",
+ "rand",
+ "rstest",
+ "serde",
+ "signstar-common",
+ "strum 0.27.1",
+ "tempfile",
+ "testresult",
+ "thiserror 2.0.11",
+ "toml",
+ "which 7.0.2",
+ "zeroize",
+]
+
 [[package]]
 name = "signstar-configure-build"
 version = "0.1.2"
@@ -3392,12 +3420,13 @@ dependencies = [
 
 [[package]]
 name = "tempfile"
-version = "3.14.0"
+version = "3.16.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c"
+checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91"
 dependencies = [
  "cfg-if",
  "fastrand",
+ "getrandom 0.3.1",
  "once_cell",
  "rustix",
  "windows-sys 0.59.0",
@@ -3537,9 +3566,9 @@ dependencies = [
 
 [[package]]
 name = "toml"
-version = "0.8.19"
+version = "0.8.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
+checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148"
 dependencies = [
  "serde",
  "serde_spanned",
@@ -3558,9 +3587,9 @@ dependencies = [
 
 [[package]]
 name = "toml_edit"
-version = "0.22.22"
+version = "0.22.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
+checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
 dependencies = [
  "indexmap",
  "serde",
@@ -3899,6 +3928,18 @@ dependencies = [
  "rustix",
 ]
 
+[[package]]
+name = "which"
+version = "7.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2774c861e1f072b3aadc02f8ba886c26ad6321567ecc294c935434cad06f1283"
+dependencies = [
+ "either",
+ "env_home",
+ "rustix",
+ "winsafe",
+]
+
 [[package]]
 name = "whoami"
 version = "1.5.2"
@@ -4174,13 +4215,19 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
 
 [[package]]
 name = "winnow"
-version = "0.6.20"
+version = "0.7.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b"
+checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1"
 dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "winsafe"
+version = "0.0.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"
+
 [[package]]
 name = "wit-bindgen-rt"
 version = "0.33.0"
diff --git a/Cargo.toml b/Cargo.toml
index a561d08c..c26dc3c6 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -8,6 +8,7 @@ members = [
   "nethsm-tests",
   "signstar-configure-build",
   "signstar-common",
+  "signstar-config",
   "signstar-request-signature",
 ]
 
diff --git a/signstar-config/Cargo.toml b/signstar-config/Cargo.toml
new file mode 100644
index 00000000..eb86f7b2
--- /dev/null
+++ b/signstar-config/Cargo.toml
@@ -0,0 +1,35 @@
+[package]
+authors.workspace = true
+edition.workspace = true
+homepage.workspace = true
+license.workspace = true
+name = "signstar-config"
+repository.workspace = true
+version = "0.1.0"
+
+[dependencies]
+confy.workspace = true
+nethsm.workspace = true
+nethsm-config.workspace = true
+nix = { version = "0.29.0", features = ["user"] }
+num_enum = "0.7.3"
+rand.workspace = true
+serde.workspace = true
+signstar-common.workspace = true
+strum.workspace = true
+thiserror.workspace = true
+toml = "0.8.20"
+which = "7.0.2"
+zeroize.workspace = true
+
+[dev-dependencies]
+rstest.workspace = true
+tempfile = "3.16.0"
+testresult.workspace = true
+
+[features]
+default = []
+_containerized-integration-test = []
+
+[lints]
+workspace = true
diff --git a/signstar-config/README.md b/signstar-config/README.md
new file mode 100644
index 00000000..b6181abd
--- /dev/null
+++ b/signstar-config/README.md
@@ -0,0 +1,123 @@
+# Signstar config
+
+Configuration file handling for Signstar hosts.
+
+## Documentation
+
+- <https://signstar.archlinux.page/rustdoc/signstar_config/> for development version of the crate
+- <https://docs.rs/signstar_config/latest/signstar_config/> for released versions of the crate
+
+## Examples
+
+### Administrative credentials
+
+Administrative credentials on a Signstar host describe all required secrets to unlock, backup, restore and fully provision a NetHSM backend.
+They can be used from plaintext and [`systemd-creds`] encrypted files.
+Functions for interacting with configurations in default locations must be called by root.
+
+```rust no_run
+use nethsm_config::AdministrativeSecretHandling;
+use signstar_config::AdminCredentials;
+
+# fn main() -> testresult::TestResult {
+// Load from plaintext file in default location
+let creds = AdminCredentials::load(AdministrativeSecretHandling::Plaintext)?;
+
+// Load from systemd-creds encrypted file in default location
+let creds = AdminCredentials::load(AdministrativeSecretHandling::SystemdCreds)?;
+
+// Store in plaintext file in default location
+creds.store(AdministrativeSecretHandling::Plaintext)?;
+
+// Store in systemd-creds encrypted file in default location
+creds.store(AdministrativeSecretHandling::SystemdCreds)?;
+# Ok(())
+# }
+```
+
+### Creating secrets for non-administrative credentials
+
+Non-administrative credentials on a Signstar host provide access to non-administrative users on a NetHSM backend.
+They can be used in plaintext and [`systemd-creds`] encrypted files.
+
+Assuming, that a Signstar configuration is present on the host, it is possible to create secrets for each backend user assigned to any of the configured system users.
+Functions for the creation of secrets must be called by root.
+
+```rust no_run
+use nethsm_config::{
+    AdministrativeSecretHandling,
+    ConfigInteractivity,
+    ConfigSettings,
+    ExtendedUserMapping,
+    HermeticParallelConfig,
+};
+use signstar_common::config::get_default_config_file_path;
+use signstar_config::{AdminCredentials, SecretsWriter};
+
+# fn main() -> testresult::TestResult {
+// Load Signstar config from default location
+let config = HermeticParallelConfig::new_from_file(
+    ConfigSettings::new(
+        "my_app".to_string(),
+        ConfigInteractivity::NonInteractive,
+        None,
+    ),
+    Some(&get_default_config_file_path()),
+)?;
+
+// Get extended user mappings for all users
+let creds_mapping: Vec<ExtendedUserMapping> = config.into();
+
+// Create secrets for each system user and their backend users
+for mapping in &creds_mapping {
+    mapping.create_secrets_dir()?;
+    mapping.create_non_administrative_secrets()?;
+}
+# Ok(())
+# }
+```
+
+---
+
+NOTE: For the creation of system users based on a Signstar config refer to [signstar-configure-build].
+
+---
+
+### Loading secrets for non-administrative users
+
+Depending on user mapping in the Signstar config, a system user may have one or more NetHSM backend users assigned to it.
+The credentials for each NetHSM backend user can be loaded by each configured system user.
+Functions for the loading of secrets must be called by the system user that is assigned that particular secret.
+
+```rust no_run
+use signstar_config::CredentialsLoading;
+
+# fn main() -> testresult::TestResult {
+// Load all credentials for the current system user
+let credentials_loading = CredentialsLoading::from_system_user()?;
+
+// Assuming the current system user is a signing user, get the credentials for its assigned user in the NetHSM backend
+let credentials = credentials_loading.credentials_for_signing_user()?;
+# Ok(())
+# }
+```
+
+## Features
+
+- `_containerized-integration-test` enables tests that require to be run in a separate, ephemeral container each.
+
+## Contributing
+
+Please refer to the [contributing guidelines] to learn how to contribute to this project.
+
+## License
+
+This project may be used under the terms of the [Apache-2.0] or [MIT] license.
+
+Changes to this project - unless stated otherwise - automatically fall under the terms of both of the aforementioned licenses.
+
+[Apache-2.0]: https://www.apache.org/licenses/LICENSE-2.0
+[MIT]: https://opensource.org/licenses/MIT
+[contributing guidelines]: ../CONTRIBUTING.md
+[signstar-configure-build]: https://signstar.archlinux.page/signstar-configure-build/index.html
+[`systemd-creds`]: https://man.archlinux.org/man/systemd-creds.1
diff --git a/signstar-config/examples/get-nethsm-credentials.rs b/signstar-config/examples/get-nethsm-credentials.rs
new file mode 100644
index 00000000..43264748
--- /dev/null
+++ b/signstar-config/examples/get-nethsm-credentials.rs
@@ -0,0 +1,61 @@
+//! Example for reading all backend user credentials associated with the current system user.
+use core::panic;
+use std::process::ExitCode;
+
+#[cfg(doc)]
+use nethsm::NetHsm;
+use signstar_config::{CredentialsLoading, Error, ErrorExitCode};
+
+/// Loads the Signstar credentials associated with the current system user.
+///
+/// The system must have a valid Signstar configuration file available in one of the understood
+/// configuration file locations, which provides an entry for the current system user.
+///
+/// # Errors
+///
+/// Returns an error if
+/// - no credentials could be found that are associated with the current user,
+/// - or one or more [`UserId`]s associated with the current system user triggered an error while
+///   trying to retrieve their credentials.
+fn load_user_credentials() -> Result<(), Error> {
+    let credentials_loading = CredentialsLoading::from_system_user()?;
+
+    // Generally fail if errors occurred while getting credentials (for whichever type of user)
+    if credentials_loading.has_userid_errors() {
+        return Err(Error::NonAdminSecretHandling(
+            signstar_config::non_admin_credentials::Error::CredentialsLoading {
+                system_user: credentials_loading.get_system_user_id()?.clone(),
+                errors: credentials_loading.get_userid_errors(),
+            },
+        ));
+    }
+
+    eprintln!("{credentials_loading:#?}");
+
+    // Get credentials for a signing user in the backend if the current system user is associated
+    // with one
+    if credentials_loading.has_signing_user() {
+        eprintln!("Credentials for signing user in the backend:");
+        let credentials = credentials_loading.credentials_for_signing_user()?;
+        let Some(ref passphrase) = credentials.passphrase else {
+            panic!("There should be a passphrase");
+        };
+        eprintln!(
+            "user: {}\npassphrase: {}",
+            credentials.user_id,
+            passphrase.expose_owned()
+        )
+    }
+
+    Ok(())
+}
+
+/// Retrieves all [`NetHsm`] credentials for the current system user.
+fn main() -> ExitCode {
+    if let Err(error) = load_user_credentials() {
+        eprintln!("{error}");
+        ExitCode::from(ErrorExitCode::from(error))
+    } else {
+        ExitCode::SUCCESS
+    }
+}
diff --git a/signstar-config/src/admin_credentials.rs b/signstar-config/src/admin_credentials.rs
new file mode 100644
index 00000000..67becd13
--- /dev/null
+++ b/signstar-config/src/admin_credentials.rs
@@ -0,0 +1,567 @@
+//! Administrative credentials handling for a NetHSM backend.
+
+use std::{
+    fmt::Display,
+    fs::{File, Permissions, set_permissions},
+    io::Write,
+    os::unix::fs::{PermissionsExt, chown},
+    path::{Path, PathBuf},
+    process::{Command, Stdio},
+};
+
+use nethsm::UserId;
+use nethsm_config::AdministrativeSecretHandling;
+use serde::{Deserialize, Serialize};
+use signstar_common::{
+    admin_credentials::{
+        create_credentials_dir,
+        get_plaintext_credentials_file,
+        get_systemd_creds_credentials_file,
+    },
+    common::SECRET_FILE_MODE,
+};
+use zeroize::Zeroize;
+
+use crate::utils::{fail_if_not_root, get_command, get_current_system_user};
+
+/// An error that may occur when handling administrative credentials for a NetHSM backend.
+#[derive(Debug, thiserror::Error)]
+pub enum Error {
+    /// Deserializing administrative secrets from a TOML string failed.
+    #[error("Deserializing administrative secrets in {path} as TOML string failed:\n{source}")]
+    ConfigFromToml {
+        /// The path to a config file that can not be deserialization as TOML string.
+        path: PathBuf,
+        /// The boxed source error.
+        source: Box<toml::de::Error>,
+    },
+
+    /// Administrative secrets can not be loaded.
+    #[error("Unable to load administrative secrets from {path}:\n{source}")]
+    ConfigLoad {
+        /// The path to a config file from which administrative secrets can not be loaded.
+        path: PathBuf,
+        /// The boxed source error.
+        source: Box<confy::ConfyError>,
+    },
+
+    /// Administrative secrets can not be stored to file.
+    #[error("Unable to store administrative secrets in {path}:\n{source}")]
+    ConfigStore {
+        /// The path to a config file in which administrative secrets can not be stored.
+        path: PathBuf,
+        /// The source error.
+        source: confy::ConfyError,
+    },
+
+    /// Serializing a Signstar config as TOML string failed.
+    #[error("Serializing administrative secrets as TOML string failed:\n{0}")]
+    ConfigToToml(#[source] toml::ser::Error),
+
+    /// A credentials file can not be created.
+    #[error("The credentials file {path} can not be created:\n{source}")]
+    CredsFileCreate {
+        /// The path to a credentials file administrative secrets can not be stored.
+        path: PathBuf,
+        /// The source error.
+        source: std::io::Error,
+    },
+
+    /// A credentials file does not exist.
+    #[error("The credentials file {path} does not exist")]
+    CredsFileMissing {
+        /// The path to a missing credentials file.
+        path: PathBuf,
+    },
+
+    /// A credentials file is not a file.
+    #[error("The credentials file {path} is not a file")]
+    CredsFileNotAFile {
+        /// The path to a credentials file that is not a file.
+        path: PathBuf,
+    },
+
+    /// A credentials file can not be written to.
+    #[error("The credentials file {path} can not be written to:\n{source}")]
+    CredsFileWrite {
+        /// The path to a credentials file that can not be written to.
+        path: PathBuf,
+        /// The source error
+        source: std::io::Error,
+    },
+}
+
+/// User data for [`AdminCredentials`].
+#[derive(Clone, Debug, Deserialize, Serialize, Zeroize)]
+pub struct User {
+    #[zeroize(skip)]
+    name: UserId,
+    passphrase: String,
+}
+
+impl User {
+    /// Creates a new [`User`] instance.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use signstar_config::admin_credentials::User;
+    ///
+    /// # fn main() -> testresult::TestResult {
+    /// let mut user = User::new(
+    ///         "ns1~admin".parse()?,
+    ///         "ns1-admin-passphrase".to_string(),
+    ///     );
+    ///
+    /// assert_eq!(user.to_string(), user.get_name().to_string());
+    /// assert_eq!(user.get_passphrase(), "ns1-admin-passphrase");
+    ///
+    /// user.set_passphrase("new-passphrase".to_string());
+    /// assert_eq!(user.get_passphrase(), "new-passphrase");
+    /// # Ok(())
+    /// # }
+    pub fn new(name: UserId, passphrase: String) -> Self {
+        Self { name, passphrase }
+    }
+
+    /// Returns the name of the [`User`].
+    pub fn get_name(&self) -> UserId {
+        self.name.clone()
+    }
+
+    /// Returns the passphrase of the [`User`].
+    pub fn get_passphrase(&self) -> &str {
+        &self.passphrase
+    }
+
+    /// Sets the passphrase of the [`User`].
+    pub fn set_passphrase(&mut self, passphrase: String) {
+        self.passphrase = passphrase
+    }
+}
+
+impl Display for User {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}", self.name)
+    }
+}
+
+/// Administrative credentials.
+#[derive(Clone, Debug, Default, Deserialize, Serialize, Zeroize)]
+pub struct AdminCredentials {
+    #[zeroize(skip)]
+    iteration: u32,
+    backup_passphrase: String,
+    unlock_passphrase: String,
+    administrators: Vec<User>,
+    namespace_administrators: Vec<User>,
+}
+
+impl AdminCredentials {
+    /// Creates a new [`AdminCredentials`] instance.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use signstar_config::admin_credentials::{AdminCredentials, User};
+    ///
+    /// # fn main() -> testresult::TestResult {
+    /// let creds = AdminCredentials::new(
+    ///     1,
+    ///     "backup-passphrase".to_string(),
+    ///     "unlock-passphrase".to_string(),
+    ///     vec![User::new("admin".parse()?, "admin-passphrase".to_string())],
+    ///     vec![User::new(
+    ///         "ns1~admin".parse()?,
+    ///         "ns1-admin-passphrase".to_string(),
+    ///     )],
+    /// );
+    /// # Ok(())
+    /// # }
+    /// ```
+    pub fn new(
+        iteration: u32,
+        backup_passphrase: String,
+        unlock_passphrase: String,
+        administrators: Vec<User>,
+        namespace_administrators: Vec<User>,
+    ) -> Self {
+        Self {
+            iteration,
+            backup_passphrase,
+            unlock_passphrase,
+            administrators,
+            namespace_administrators,
+        }
+    }
+
+    /// Loads an [`AdminCredentials`] from the default file location.
+    ///
+    /// Depending on `secrets_handling`, the file path and contents differ:
+    ///
+    /// - [`AdministrativeSecretHandling::Plaintext`]: the file path is defined by
+    ///   [`get_plaintext_credentials_file`] and the contents are plaintext,
+    /// - [`AdministrativeSecretHandling::SystemdCreds`]: the file path is defined by
+    ///   [`get_systemd_creds_credentials_file`] and the contents are [systemd-creds] encrypted.
+    ///
+    /// Delegates to [`AdminCredentials::load_from_file`], providing the specific file path and the
+    /// selected `secrets_handling`.
+    ///
+    /// # Examples
+    ///
+    /// ```no_run
+    /// use nethsm_config::AdministrativeSecretHandling;
+    /// use signstar_config::admin_credentials::AdminCredentials;
+    ///
+    /// # fn main() -> testresult::TestResult {
+    /// // load plaintext credentials from default location
+    /// let plaintext_admin_creds = AdminCredentials::load(AdministrativeSecretHandling::Plaintext)?;
+    ///
+    /// // load systemd-creds encrypted credentials from default location
+    /// let systemd_creds_admin_creds =
+    ///     AdminCredentials::load(AdministrativeSecretHandling::SystemdCreds)?;
+    ///
+    /// # Ok(())
+    /// # }
+    /// ```
+    ///
+    /// # Errors
+    ///
+    /// Returns an error if [`AdminCredentials::load_from_file`] fails.
+    ///
+    /// # Panics
+    ///
+    /// This function panics when providing [`AdministrativeSecretHandling::ShamirsSecretSharing`]
+    /// as `secrets_handling`.
+    ///
+    /// [systemd-creds]: https://man.archlinux.org/man/systemd-creds.1
+    pub fn load(secrets_handling: AdministrativeSecretHandling) -> Result<Self, crate::Error> {
+        Self::load_from_file(
+            match secrets_handling {
+                AdministrativeSecretHandling::Plaintext => get_plaintext_credentials_file(),
+                AdministrativeSecretHandling::SystemdCreds => get_systemd_creds_credentials_file(),
+                AdministrativeSecretHandling::ShamirsSecretSharing => {
+                    unimplemented!("Shamir's Secret Sharing is not yet supported")
+                }
+            },
+            secrets_handling,
+        )
+    }
+
+    /// Loads an [`AdminCredentials`] instance from file.
+    ///
+    /// Depending on `path` and `secrets_handling`, the behavior of this function differs:
+    ///
+    /// - If `secrets_handling` is set to [`AdministrativeSecretHandling::Plaintext`] the contents
+    ///   at `path` are considered to be plaintext.
+    /// - If `secrets_handling` is set to [`AdministrativeSecretHandling::SystemdCreds`] the
+    ///   contents at `path` are considered to be [systemd-creds] encrypted.
+    ///
+    /// # Examples
+    ///
+    /// ```no_run
+    /// use std::io::Write;
+    ///
+    /// use nethsm_config::AdministrativeSecretHandling;
+    /// use signstar_config::admin_credentials::{AdminCredentials, User};
+    ///
+    /// # fn main() -> testresult::TestResult {
+    /// let admin_creds = r#"iteration = 1
+    /// backup_passphrase = "backup-passphrase"
+    /// unlock_passphrase = "unlock-passphrase"
+    ///
+    /// [[administrators]]
+    /// name = "admin"
+    /// passphrase = "admin-passphrase"
+    ///
+    /// [[namespace_administrators]]
+    /// name = "ns1~admin"
+    /// passphrase = "ns1-admin-passphrase"
+    /// "#;
+    /// let mut tempfile = tempfile::NamedTempFile::new()?;
+    /// write!(tempfile.as_file_mut(), "{admin_creds}");
+    ///
+    /// assert!(
+    ///     AdminCredentials::load_from_file(tempfile.path(), AdministrativeSecretHandling::Plaintext)
+    ///         .is_ok()
+    /// );
+    /// # Ok(())
+    /// # }
+    /// ```
+    ///
+    /// # Errors
+    ///
+    /// Returns an error if
+    /// - the function is called by a system user that is not root,
+    /// - the file at `path` does not exist,
+    /// - the file at `path` is not a file,
+    /// - the file at `path` is considered as plaintext but can not be loaded,
+    /// - the file at `path` is considered as [systemd-creds] encrypted but can not be decrypted,
+    /// - or the file at `path` is considered as [systemd-creds] encrypted but can not be loaded
+    ///   after decryption.
+    ///
+    /// # Panics
+    ///
+    /// This function panics when providing [`AdministrativeSecretHandling::ShamirsSecretSharing`]
+    /// as `secrets_handling`.
+    ///
+    /// [systemd-creds]: https://man.archlinux.org/man/systemd-creds.1
+    pub fn load_from_file(
+        path: impl AsRef<Path>,
+        secrets_handling: AdministrativeSecretHandling,
+    ) -> Result<Self, crate::Error> {
+        // fail if not running as root
+        fail_if_not_root(&get_current_system_user()?)?;
+
+        let path = path.as_ref();
+        if !path.exists() {
+            return Err(crate::Error::AdminSecretHandling(Error::CredsFileMissing {
+                path: path.to_path_buf(),
+            }));
+        }
+        if !path.is_file() {
+            return Err(crate::Error::AdminSecretHandling(
+                Error::CredsFileNotAFile {
+                    path: path.to_path_buf(),
+                },
+            ));
+        }
+
+        match secrets_handling {
+            AdministrativeSecretHandling::Plaintext => confy::load_path(path).map_err(|source| {
+                crate::Error::AdminSecretHandling(Error::ConfigLoad {
+                    path: path.to_path_buf(),
+                    source: Box::new(source),
+                })
+            }),
+            AdministrativeSecretHandling::SystemdCreds => {
+                // Decrypt the credentials using systemd-creds.
+                let creds_command = get_command("systemd-creds")?;
+                let mut command = Command::new(creds_command);
+                let command = command.arg("decrypt").arg(path).arg("-");
+                let command_output =
+                    command
+                        .output()
+                        .map_err(|source| crate::Error::CommandExec {
+                            command: format!("{command:?}"),
+                            source,
+                        })?;
+                if !command_output.status.success() {
+                    return Err(crate::Error::CommandNonZero {
+                        command: format!("{command:?}"),
+                        exit_status: command_output.status,
+                        stderr: String::from_utf8_lossy(&command_output.stderr).into_owned(),
+                    });
+                }
+
+                // Read the resulting TOML string from stdout and construct an AdminCredentials from
+                // it.
+                let config_str = String::from_utf8(command_output.stdout).map_err(|source| {
+                    crate::Error::Utf8String {
+                        path: path.to_path_buf(),
+                        context: "after decrypting".to_string(),
+                        source,
+                    }
+                })?;
+                toml::from_str(&config_str).map_err(|source| {
+                    crate::Error::AdminSecretHandling(Error::ConfigFromToml {
+                        path: path.to_path_buf(),
+                        source: Box::new(source),
+                    })
+                })
+            }
+            AdministrativeSecretHandling::ShamirsSecretSharing => {
+                unimplemented!("Shamir's Secret Sharing is not yet supported")
+            }
+        }
+    }
+
+    /// Stores the [`AdminCredentials`] as a file in the default location.
+    ///
+    /// Depending on `secrets_handling`, the file path and contents differ:
+    ///
+    /// - [`AdministrativeSecretHandling::Plaintext`]: the file path is defined by
+    ///   [`get_plaintext_credentials_file`] and the contents are plaintext,
+    /// - [`AdministrativeSecretHandling::SystemdCreds`]: the file path is defined by
+    ///   [`get_systemd_creds_credentials_file`] and the contents are [systemd-creds] encrypted.
+    ///
+    /// Automatically creates the directory in which the administrative credentials are created.
+    /// After storing the [`AdminCredentials`] as file, its file permissions and ownership are
+    /// adjusted so that it is only accessible by root.
+    ///
+    /// # Examples
+    ///
+    /// ```no_run
+    /// use nethsm_config::AdministrativeSecretHandling;
+    /// use signstar_config::admin_credentials::{AdminCredentials, User};
+    ///
+    /// # fn main() -> testresult::TestResult {
+    /// let creds = AdminCredentials::new(
+    ///     1,
+    ///     "backup-passphrase".to_string(),
+    ///     "unlock-passphrase".to_string(),
+    ///     vec![User::new("admin".parse()?, "admin-passphrase".to_string())],
+    ///     vec![User::new(
+    ///         "ns1~admin".parse()?,
+    ///         "ns1-admin-passphrase".to_string(),
+    ///     )],
+    /// );
+    ///
+    /// // store as plaintext file
+    /// creds.store(AdministrativeSecretHandling::Plaintext)?;
+    ///
+    /// // store as systemd-creds encrypted file
+    /// creds.store(AdministrativeSecretHandling::SystemdCreds)?;
+    /// # Ok(())
+    /// # }
+    /// ```
+    ///
+    /// # Errors
+    ///
+    /// Returns an error if
+    /// - the function is called by a system user that is not root,
+    /// - the directory for administrative credentials cannot be created,
+    /// - `self` cannot be turned into its TOML representation,
+    /// - the [systemd-creds] command is not found,
+    /// - [systemd-creds] fails to encrypt the TOML representation of `self`,
+    /// - the target file can not be created,
+    /// - the plaintext or [systemd-creds] encrypted data can not be written to file,
+    /// - or the ownership or permissions of the target file can not be adjusted.
+    ///
+    /// # Panics
+    ///
+    /// This function panics when providing [`AdministrativeSecretHandling::ShamirsSecretSharing`]
+    /// as `secrets_handling`.
+    ///
+    /// [systemd-creds]: https://man.archlinux.org/man/systemd-creds.1
+    pub fn store(
+        &self,
+        secrets_handling: AdministrativeSecretHandling,
+    ) -> Result<(), crate::Error> {
+        // fail if not running as root
+        fail_if_not_root(&get_current_system_user()?)?;
+
+        create_credentials_dir()?;
+
+        let (config_data, path) = {
+            // Get the TOML string representation of self.
+            let config_data = toml::to_string_pretty(self)
+                .map_err(|source| crate::Error::AdminSecretHandling(Error::ConfigToToml(source)))?;
+            match secrets_handling {
+                AdministrativeSecretHandling::Plaintext => (
+                    config_data.as_bytes().to_vec(),
+                    get_plaintext_credentials_file(),
+                ),
+                AdministrativeSecretHandling::SystemdCreds => {
+                    // Encrypt self as systemd-creds encrypted TOML file.
+                    let creds_command = get_command("systemd-creds")?;
+                    let mut command = Command::new(creds_command);
+                    let command = command.args(["encrypt", "-", "-"]);
+
+                    let mut command_child = command
+                        .stdin(Stdio::piped())
+                        .stdout(Stdio::piped())
+                        .spawn()
+                        .map_err(|source| crate::Error::CommandBackground {
+                            command: format!("{command:?}"),
+                            source,
+                        })?;
+                    let Some(mut stdin) = command_child.stdin.take() else {
+                        return Err(crate::Error::CommandAttachToStdin {
+                            command: format!("{command:?}"),
+                        })?;
+                    };
+
+                    let handle = std::thread::spawn(move || {
+                        stdin.write_all(config_data.as_bytes()).map_err(|source| {
+                            crate::Error::CommandWriteToStdin {
+                                command: "systemd-creds encrypt - -".to_string(),
+                                source,
+                            }
+                        })
+                    });
+
+                    let _handle_result = handle.join().map_err(|source| crate::Error::Thread {
+                        context: format!(
+                            "storing systemd-creds encrypted administrative credentials: {source:?}"
+                        ),
+                    })?;
+
+                    let command_output = command_child.wait_with_output().map_err(|source| {
+                        crate::Error::CommandExec {
+                            command: format!("{command:?}"),
+                            source,
+                        }
+                    })?;
+                    if !command_output.status.success() {
+                        return Err(crate::Error::CommandNonZero {
+                            command: format!("{command:?}"),
+                            exit_status: command_output.status,
+                            stderr: String::from_utf8_lossy(&command_output.stderr).into_owned(),
+                        });
+                    }
+                    (command_output.stdout, get_systemd_creds_credentials_file())
+                }
+                AdministrativeSecretHandling::ShamirsSecretSharing => {
+                    unimplemented!("Shamir's Secret Sharing is not yet supported")
+                }
+            }
+        };
+
+        // Write administrative credentials to file and adjust permission and ownership
+        // of file
+        {
+            let mut file = File::create(path.as_path()).map_err(|source| {
+                crate::Error::AdminSecretHandling(Error::CredsFileCreate {
+                    path: path.clone(),
+                    source,
+                })
+            })?;
+            file.write_all(&config_data).map_err(|source| {
+                crate::Error::AdminSecretHandling(Error::CredsFileWrite {
+                    path: path.to_path_buf(),
+                    source,
+                })
+            })?;
+        }
+        chown(&path, Some(0), Some(0)).map_err(|source| crate::Error::Chown {
+            path: path.clone(),
+            user: "root".to_string(),
+            source,
+        })?;
+        set_permissions(path.as_path(), Permissions::from_mode(SECRET_FILE_MODE)).map_err(
+            |source| crate::Error::ApplyPermissions {
+                path: path.clone(),
+                mode: SECRET_FILE_MODE,
+                source,
+            },
+        )?;
+
+        Ok(())
+    }
+
+    /// Returns the iteration.
+    pub fn get_iteration(&self) -> u32 {
+        self.iteration
+    }
+
+    /// Returns the backup passphrase.
+    pub fn get_backup_passphrase(&self) -> &str {
+        &self.backup_passphrase
+    }
+
+    /// Returns the unlock passphrase.
+    pub fn get_unlock_passphrase(&self) -> &str {
+        &self.unlock_passphrase
+    }
+
+    /// Returns the list of administrators.
+    pub fn get_administrators(&self) -> &[User] {
+        &self.administrators
+    }
+
+    /// Returns the list of namespace administrators.
+    pub fn get_namespace_administrators(&self) -> &[User] {
+        &self.namespace_administrators
+    }
+}
diff --git a/signstar-config/src/config.rs b/signstar-config/src/config.rs
new file mode 100644
index 00000000..648bd8c6
--- /dev/null
+++ b/signstar-config/src/config.rs
@@ -0,0 +1,41 @@
+//! Configuration file handling for a NetHSM backend.
+
+use nethsm_config::{ConfigSettings, HermeticParallelConfig};
+use signstar_common::config::{get_config_file, get_config_file_paths};
+
+/// The error that may occur when handling a configuration file for a NetHSM backend.
+#[derive(Debug, thiserror::Error)]
+pub enum Error {
+    /// The Signstar configuration is missing.
+    #[error("No configuration file found in {}.", get_config_file_paths().iter().map(|path| path.display().to_string()).collect::<Vec<String>>().join(", "))]
+    ConfigMissing,
+
+    /// An error specific to NetHsm config handling.
+    #[error("NetHSM config error:\n{0}")]
+    NetHsmConfig(#[from] nethsm_config::Error),
+}
+
+/// Loads a [`HermeticParallelConfig`].
+///
+/// Gets a configuration file from the default locations using [`get_config_file`] and returns it as
+/// [`HermeticParallelConfig`].
+///
+/// # Errors
+///
+/// Returns an error if no config file is found or if the [`HermeticParallelConfig`] can not be
+/// created.
+pub fn load_config() -> Result<HermeticParallelConfig, crate::Error> {
+    let Some(config_path) = get_config_file() else {
+        return Err(crate::Error::Config(Error::ConfigMissing));
+    };
+
+    HermeticParallelConfig::new_from_file(
+        ConfigSettings::new(
+            "signstar".to_string(),
+            nethsm_config::ConfigInteractivity::NonInteractive,
+            None,
+        ),
+        Some(&config_path),
+    )
+    .map_err(|source| crate::Error::Config(Error::NetHsmConfig(source)))
+}
diff --git a/signstar-config/src/error.rs b/signstar-config/src/error.rs
new file mode 100644
index 00000000..a03506c4
--- /dev/null
+++ b/signstar-config/src/error.rs
@@ -0,0 +1,396 @@
+//! Common, top-level error type for all components of signstar-config.
+
+use std::{
+    path::PathBuf,
+    process::{ExitCode, ExitStatus},
+    string::FromUtf8Error,
+};
+
+/// An error that may occur when using Signstar config.
+#[derive(Debug, thiserror::Error)]
+pub enum Error {
+    /// An error specific to administrative secret handling.
+    #[error("Error with administrative secret handling:\n{0}")]
+    AdminSecretHandling(#[from] crate::admin_credentials::Error),
+
+    /// An error specific to Signstar config handling.
+    /// Applying permissions to a file or directory failed.
+    #[error("Unable to apply permissions from mode {mode} to {path}:\n{source}")]
+    ApplyPermissions {
+        /// The path to a file for which permissions can not be applied.
+        path: PathBuf,
+        /// The file mode that should be applied for `path`.
+        mode: u32,
+        /// The source error.
+        source: std::io::Error,
+    },
+
+    /// The ownership of a path can not be changed.
+    #[error("Changing ownership of {path} to user {user} failed:\n{source}")]
+    Chown {
+        /// The path to a file for which ownership can not be changed.
+        path: PathBuf,
+        /// The system user that should be the new owner of `path`.
+        user: String,
+        /// The source error.
+        source: std::io::Error,
+    },
+
+    /// Unable to attach to stdin of a command.
+    #[error("Unable to attach to stdin of command \"{command}\"")]
+    CommandAttachToStdin {
+        /// The command for which attaching to stdin failed.
+        command: String,
+    },
+
+    /// A command exited unsuccessfully.
+    #[error("The command \"{command}\" could not be started in the background:\n{source}")]
+    CommandBackground {
+        /// The command that could not be started in the background.
+        command: String,
+        /// The source error.
+        source: std::io::Error,
+    },
+
+    /// A command could not be executed.
+    #[error("The command \"{command}\" could not be executed:\n{source}")]
+    CommandExec {
+        /// The command that could not be executed.
+        command: String,
+        /// The source error.
+        source: std::io::Error,
+    },
+
+    /// A command exited unsuccessfully.
+    #[error(
+        "The command \"{command}\" exited with non-zero status code \"{exit_status}\":\nstderr:\n{stderr}"
+    )]
+    CommandNonZero {
+        /// The command that exited with a non-zero exit code.
+        command: String,
+        /// The exit status of `command`.
+        exit_status: ExitStatus,
+        /// The stderr of `command`.
+        stderr: String,
+    },
+
+    /// Unable to write to stdin of a command.
+    #[error("Unable to write to stdin of command \"{command}\"")]
+    CommandWriteToStdin {
+        /// The command for which writing to stdin failed.
+        command: String,
+        /// The source error.
+        source: std::io::Error,
+    },
+
+    /// Configuration errors.
+    #[error("Signstar config error:\n{0}")]
+    Config(#[from] crate::config::Error),
+
+    /// An error specific to non-administrative secret handling.
+    #[error("Error with non-administrative secret handling:\n{0}")]
+    NonAdminSecretHandling(#[from] crate::non_admin_credentials::Error),
+
+    /// Low-level administrative credentials handling in signstar-common failed.
+    #[error("Handling of administrative credentials failed:\n{0}")]
+    SignstarCommonAdminCreds(#[from] signstar_common::admin_credentials::Error),
+
+    /// Joining a thread returned an error.
+    #[error("Thread error while {context}")]
+    Thread {
+        /// The context in which the failed thread ran.
+        ///
+        /// Should complete the sentence "Thread error while ".
+        context: String,
+    },
+
+    /// A UTF-8 error occurred when trying to convert a byte vector to a string.
+    #[error("Converting contents of {path} to string failed while {context}:\n{source}")]
+    Utf8String {
+        /// The path to a file for which conversion to UTF-8 string failed.
+        path: PathBuf,
+        /// The context in which the error occurred.
+        ///
+        /// Should complete the sentence "Converting contents of `path` to string failed while "
+        context: String,
+        /// The source error.
+        source: FromUtf8Error,
+    },
+
+    /// A utility function returned an error.
+    #[error("Utility function error: {0}")]
+    Utils(#[from] crate::utils::Error),
+}
+
+/// Mapping for relevant [`Error`] variants to an [`ExitCode`].
+#[derive(Clone, Copy, Debug, num_enum::IntoPrimitive, Eq, Ord, PartialEq, PartialOrd)]
+#[repr(u8)]
+pub enum ErrorExitCode {
+    /// Mapping for [`crate::admin_credentials::Error::ConfigFromToml`] wrapped in
+    /// [`Error::AdminSecretHandling`].
+    AdminCredentialsConfigFromToml = 100,
+
+    /// Mapping for [`crate::admin_credentials::Error::ConfigLoad`] wrapped in
+    /// [`Error::AdminSecretHandling`].
+    AdminCredentialsConfigLoad = 101,
+
+    /// Mapping for [`crate::admin_credentials::Error::ConfigStore`] wrapped in
+    /// [`Error::AdminSecretHandling`].
+    AdminCredentialsConfigStore = 102,
+
+    /// Mapping for [`crate::admin_credentials::Error::ConfigToToml`] wrapped in
+    /// [`Error::AdminSecretHandling`].
+    AdminCredentialsConfigToToml = 103,
+
+    /// Mapping for [`crate::admin_credentials::Error::CredsFileCreate`] wrapped in
+    /// [`Error::AdminSecretHandling`].
+    AdminCredentialsCredsFileCreate = 104,
+
+    /// Mapping for [`crate::admin_credentials::Error::CredsFileMissing`] wrapped in
+    /// [`Error::AdminSecretHandling`].
+    AdminCredentialsCredsFileMissing = 105,
+
+    /// Mapping for [`crate::admin_credentials::Error::CredsFileNotAFile`] wrapped in
+    /// [`Error::AdminSecretHandling`].
+    AdminCredentialsCredsFileNotAFile = 106,
+
+    /// Mapping for [`crate::admin_credentials::Error::CredsFileWrite`] wrapped in
+    /// [`Error::AdminSecretHandling`].
+    AdminCredentialsCredsFileWrite = 107,
+
+    /// Mapping for [`Error::ApplyPermissions`].
+    ApplyPermissions = 10,
+
+    /// Mapping for [`Error::Chown`].
+    Chown = 11,
+
+    /// Mapping for [`Error::CommandAttachToStdin`].
+    CommandAttachToStdin = 12,
+
+    /// Mapping for [`Error::CommandBackground`].
+    CommandBackground = 13,
+
+    /// Mapping for [`Error::CommandExec`].
+    CommandExec = 14,
+
+    /// Mapping for [`Error::CommandNonZero`].
+    CommandNonZero = 15,
+
+    /// Mapping for [`Error::CommandWriteToStdin`].
+    CommandWriteToStdin = 16,
+
+    /// Mapping for [`crate::config::Error::ConfigMissing`] wrapped in [`Error::Config`].
+    ConfigConfigMissing = 120,
+
+    /// Mapping for [`crate::config::Error::NetHsmConfig`] wrapped in [`Error::Config`].
+    ConfigNetHsmConfig = 121,
+
+    /// Mapping for [`crate::non_admin_credentials::Error::CredentialsLoading`] wrapped in
+    /// [`Error::NonAdminSecretHandling`].
+    NonAdminCredentialsCredentialsLoading = 140,
+
+    /// Mapping for [`crate::non_admin_credentials::Error::CredentialsMissing`] wrapped in
+    /// [`Error::NonAdminSecretHandling`].
+    NonAdminCredentialsCredentialsMissing = 141,
+
+    /// Mapping for [`crate::non_admin_credentials::Error::NoSystemUser`] wrapped in
+    /// [`Error::NonAdminSecretHandling`].
+    NonAdminCredentialsNoSystemUser = 142,
+
+    /// Mapping for [`crate::non_admin_credentials::Error::NotSigningUser`] wrapped in
+    /// [`Error::NonAdminSecretHandling`].
+    NonAdminCredentialsNotSigningUser = 143,
+
+    /// Mapping for [`crate::non_admin_credentials::Error::SecretsDirCreate`] wrapped in
+    /// [`Error::NonAdminSecretHandling`].
+    NonAdminCredentialsSecretsDirCreate = 144,
+
+    /// Mapping for [`crate::non_admin_credentials::Error::SecretsFileCreate`] wrapped in
+    /// [`Error::NonAdminSecretHandling`].
+    NonAdminCredentialsSecretsFileCreate = 145,
+
+    /// Mapping for [`crate::non_admin_credentials::Error::SecretsFileMetadata`] wrapped in
+    /// [`Error::NonAdminSecretHandling`].
+    NonAdminCredentialsSecretsFileMetadata = 146,
+
+    /// Mapping for [`crate::non_admin_credentials::Error::SecretsFileMissing`] wrapped in
+    /// [`Error::NonAdminSecretHandling`].
+    NonAdminCredentialsSecretsFileMissing = 147,
+
+    /// Mapping for [`crate::non_admin_credentials::Error::SecretsFileNotAFile`] wrapped in
+    /// [`Error::NonAdminSecretHandling`].
+    NonAdminCredentialsSecretsFileNotAFile = 148,
+
+    /// Mapping for [`crate::non_admin_credentials::Error::SecretsFilePermissions`] wrapped in
+    /// [`Error::NonAdminSecretHandling`].
+    NonAdminCredentialsSecretsFilePermissions = 149,
+
+    /// Mapping for [`crate::non_admin_credentials::Error::SecretsFileRead`] wrapped in
+    /// [`Error::NonAdminSecretHandling`].
+    NonAdminCredentialsSecretsFileRead = 150,
+
+    /// Mapping for [`crate::non_admin_credentials::Error::SecretsFileWrite`] wrapped in
+    /// [`Error::NonAdminSecretHandling`].
+    NonAdminCredentialsSecretsFileWrite = 151,
+
+    /// Mapping for [`signstar_common::admin_credentials::Error::ApplyPermissions`] wrapped in
+    /// [`Error::SignstarCommonAdminCreds`].
+    SignstarCommonAdminCredsApplyPermissions = 170,
+
+    /// Mapping for [`signstar_common::admin_credentials::Error::CreateDirectory`] wrapped in
+    /// [`Error::SignstarCommonAdminCreds`].
+    SignstarCommonAdminCredsCreateDirectory = 171,
+
+    /// Mapping for [`signstar_common::admin_credentials::Error::DirChangeOwner`] wrapped in
+    /// [`Error::SignstarCommonAdminCreds`].
+    SignstarCommonAdminCredsDirChangeOwner = 172,
+
+    /// Mapping for [`Error::Thread`].
+    Thread = 17,
+
+    /// Mapping for [`Error::Utf8String`].
+    Utf8String = 18,
+
+    /// Mapping for [`crate::utils::Error::ExecutableNotFound`] wrapped in [`Error::Utils`].
+    UtilsExecutableNotFound = 190,
+
+    /// Mapping for [`crate::utils::Error::MappingSystemUserGet`] wrapped in [`Error::Utils`].
+    UtilsMappingSystemUserGet = 191,
+
+    /// Mapping for [`crate::utils::Error::SystemUserData`] wrapped in [`Error::Utils`].
+    UtilsSystemUserData = 192,
+
+    /// Mapping for [`crate::utils::Error::SystemUserLookup`] wrapped in [`Error::Utils`].
+    UtilsSystemUserLookup = 193,
+
+    /// Mapping for [`crate::utils::Error::SystemUserMismatch`] wrapped in [`Error::Utils`].
+    UtilsSystemUserMismatch = 194,
+
+    /// Mapping for [`crate::utils::Error::SystemUserNotRoot`] wrapped in [`Error::Utils`].
+    UtilsSystemUserNotRoot = 195,
+
+    /// Mapping for [`crate::utils::Error::SystemUserRoot`] wrapped in [`Error::Utils`].
+    UtilsSystemUserRoot = 196,
+}
+
+impl From<Error> for ErrorExitCode {
+    fn from(value: Error) -> Self {
+        match value {
+            // admin credentials related errors and their exit codes
+            Error::AdminSecretHandling(error) => match error {
+                crate::admin_credentials::Error::ConfigFromToml { .. } => {
+                    Self::AdminCredentialsConfigFromToml
+                }
+                crate::admin_credentials::Error::ConfigLoad { .. } => {
+                    Self::AdminCredentialsConfigLoad
+                }
+                crate::admin_credentials::Error::ConfigStore { .. } => {
+                    Self::AdminCredentialsConfigStore
+                }
+                crate::admin_credentials::Error::ConfigToToml(_) => {
+                    Self::AdminCredentialsConfigToToml
+                }
+                crate::admin_credentials::Error::CredsFileCreate { .. } => {
+                    Self::AdminCredentialsCredsFileCreate
+                }
+                crate::admin_credentials::Error::CredsFileMissing { .. } => {
+                    Self::AdminCredentialsCredsFileMissing
+                }
+                crate::admin_credentials::Error::CredsFileNotAFile { .. } => {
+                    Self::AdminCredentialsCredsFileNotAFile
+                }
+                crate::admin_credentials::Error::CredsFileWrite { .. } => {
+                    Self::AdminCredentialsCredsFileWrite
+                }
+            },
+            // config related errors
+            Error::Config(error) => match error {
+                crate::config::Error::ConfigMissing => Self::ConfigConfigMissing,
+                crate::config::Error::NetHsmConfig(_) => Self::ConfigNetHsmConfig,
+            },
+            // non-admin credentials related errors and their exit codes
+            Error::NonAdminSecretHandling(error) => match error {
+                crate::non_admin_credentials::Error::CredentialsLoading { .. } => {
+                    Self::NonAdminCredentialsCredentialsLoading
+                }
+                crate::non_admin_credentials::Error::CredentialsMissing { .. } => {
+                    Self::NonAdminCredentialsCredentialsMissing
+                }
+                crate::non_admin_credentials::Error::NoSystemUser => {
+                    Self::NonAdminCredentialsNoSystemUser
+                }
+                crate::non_admin_credentials::Error::NotSigningUser => {
+                    Self::NonAdminCredentialsNotSigningUser
+                }
+                crate::non_admin_credentials::Error::SecretsDirCreate { .. } => {
+                    Self::NonAdminCredentialsSecretsDirCreate
+                }
+                crate::non_admin_credentials::Error::SecretsFileCreate { .. } => {
+                    Self::NonAdminCredentialsSecretsFileCreate
+                }
+                crate::non_admin_credentials::Error::SecretsFileMetadata { .. } => {
+                    Self::NonAdminCredentialsSecretsFileMetadata
+                }
+                crate::non_admin_credentials::Error::SecretsFileMissing { .. } => {
+                    Self::NonAdminCredentialsSecretsFileMissing
+                }
+                crate::non_admin_credentials::Error::SecretsFileNotAFile { .. } => {
+                    Self::NonAdminCredentialsSecretsFileNotAFile
+                }
+                crate::non_admin_credentials::Error::SecretsFilePermissions { .. } => {
+                    Self::NonAdminCredentialsSecretsFilePermissions
+                }
+                crate::non_admin_credentials::Error::SecretsFileRead { .. } => {
+                    Self::NonAdminCredentialsSecretsFileRead
+                }
+                crate::non_admin_credentials::Error::SecretsFileWrite { .. } => {
+                    Self::NonAdminCredentialsSecretsFileWrite
+                }
+            },
+            // signstar-common admin credentials related errors
+            Error::SignstarCommonAdminCreds(error) => match error {
+                signstar_common::admin_credentials::Error::ApplyPermissions { .. } => {
+                    Self::SignstarCommonAdminCredsApplyPermissions
+                }
+                signstar_common::admin_credentials::Error::CreateDirectory { .. } => {
+                    Self::SignstarCommonAdminCredsCreateDirectory
+                }
+                signstar_common::admin_credentials::Error::DirChangeOwner { .. } => {
+                    Self::SignstarCommonAdminCredsDirChangeOwner
+                }
+            },
+            // utils related errors
+            Error::Utils(error) => match error {
+                crate::utils::Error::ExecutableNotFound { .. } => Self::UtilsExecutableNotFound,
+                crate::utils::Error::MappingSystemUserGet(_) => Self::UtilsMappingSystemUserGet,
+                crate::utils::Error::SystemUserData { .. } => Self::UtilsSystemUserData,
+                crate::utils::Error::SystemUserLookup { .. } => Self::UtilsSystemUserLookup,
+                crate::utils::Error::SystemUserMismatch { .. } => Self::UtilsSystemUserMismatch,
+                crate::utils::Error::SystemUserNotRoot { .. } => Self::UtilsSystemUserNotRoot,
+                crate::utils::Error::SystemUserRoot => Self::UtilsSystemUserRoot,
+            },
+            // top-level errors and their exit codes
+            Error::ApplyPermissions { .. } => Self::ApplyPermissions,
+            Error::CommandAttachToStdin { .. } => Self::CommandAttachToStdin,
+            Error::Chown { .. } => Self::Chown,
+            Error::CommandBackground { .. } => Self::CommandBackground,
+            Error::CommandExec { .. } => Self::CommandExec,
+            Error::CommandNonZero { .. } => Self::CommandNonZero,
+            Error::Thread { .. } => Self::Thread,
+            Error::Utf8String { .. } => Self::Utf8String,
+            Error::CommandWriteToStdin { .. } => Self::CommandWriteToStdin,
+        }
+    }
+}
+
+impl From<ErrorExitCode> for ExitCode {
+    fn from(value: ErrorExitCode) -> Self {
+        Self::from(std::convert::Into::<u8>::into(value))
+    }
+}
+
+impl From<ErrorExitCode> for i32 {
+    fn from(value: ErrorExitCode) -> Self {
+        Self::from(std::convert::Into::<u8>::into(value))
+    }
+}
diff --git a/signstar-config/src/lib.rs b/signstar-config/src/lib.rs
new file mode 100644
index 00000000..baf555d3
--- /dev/null
+++ b/signstar-config/src/lib.rs
@@ -0,0 +1,18 @@
+#![doc = include_str!("../README.md")]
+
+pub mod admin_credentials;
+pub mod config;
+pub mod error;
+pub mod non_admin_credentials;
+pub mod utils;
+
+pub use admin_credentials::{AdminCredentials, User};
+pub use config::load_config;
+pub use error::{Error, ErrorExitCode};
+pub use non_admin_credentials::{
+    CredentialsLoading,
+    CredentialsLoadingError,
+    CredentialsLoadingErrors,
+    SecretsReader,
+    SecretsWriter,
+};
diff --git a/signstar-config/src/non_admin_credentials.rs b/signstar-config/src/non_admin_credentials.rs
new file mode 100644
index 00000000..5e054e95
--- /dev/null
+++ b/signstar-config/src/non_admin_credentials.rs
@@ -0,0 +1,765 @@
+//! Non-administrative credentials handling for a NetHSM backend.
+use std::{
+    fmt::{Debug, Display},
+    fs::{File, Permissions, create_dir_all, read_to_string, set_permissions},
+    io::Write,
+    os::unix::fs::{PermissionsExt, chown},
+    path::{Path, PathBuf},
+    process::{Command, Stdio},
+};
+
+#[cfg(doc)]
+use nethsm::NetHsm;
+use nethsm::{Credentials, Passphrase, UserId};
+#[cfg(doc)]
+use nethsm_config::HermeticParallelConfig;
+use nethsm_config::{ExtendedUserMapping, NonAdministrativeSecretHandling, SystemUserId};
+use rand::{Rng, distributions::Alphanumeric, thread_rng};
+use signstar_common::{
+    common::SECRET_FILE_MODE,
+    system_user::{
+        get_home_base_dir_path,
+        get_plaintext_secret_file,
+        get_systemd_creds_secret_file,
+        get_user_secrets_dir,
+    },
+};
+
+use crate::{
+    config::load_config,
+    utils::{
+        fail_if_not_root,
+        fail_if_root,
+        get_command,
+        get_current_system_user,
+        get_system_user_pair,
+        match_current_system_user,
+    },
+};
+
+/// An error that may occur when handling non-administrative credentials for a NetHSM backend.
+#[derive(Debug, thiserror::Error)]
+pub enum Error {
+    /// There are one or more errors when loading credentials for a specific system user.
+    #[error("Errors occurred when loading credentials for system user {system_user}:\n{errors}")]
+    CredentialsLoading {
+        /// The system user for which loading of backend user credentials led to errors.
+        system_user: SystemUserId,
+        /// The errors that occurred during loading of backend user credentials for `system_user`.
+        errors: CredentialsLoadingErrors,
+    },
+
+    /// There are no credentials for a specific system user.
+    #[error("There are no credentials for system user {system_user}")]
+    CredentialsMissing {
+        /// The system user for which credentials are missing.
+        system_user: SystemUserId,
+    },
+
+    /// A mapping does not offer a system user.
+    #[error("There is no system user in the mapping.")]
+    NoSystemUser,
+
+    /// A user is not a signing user for the NetHSM backend.
+    #[error("The user is not an operator user in the NetHSM backend used for signing.")]
+    NotSigningUser,
+
+    /// A passphrase directory can not be created.
+    #[error("Passphrase directory {path} for user {system_user} can not be created:\n{source}")]
+    SecretsDirCreate {
+        /// The path to a secrets directory that could not be created.
+        path: PathBuf,
+        /// The system user in whose home directory `path` could not be created.
+        system_user: SystemUserId,
+        /// The source error.
+        source: std::io::Error,
+    },
+
+    /// A secrets file can not be created.
+    #[error("The secrets file {path} can not be created for user {system_user}:\n{source}")]
+    SecretsFileCreate {
+        /// The path to a secrets file that could not be created.
+        path: PathBuf,
+        /// The system user in whose home directory `path` could not be created.
+        system_user: SystemUserId,
+        /// The source error.
+        source: std::io::Error,
+    },
+
+    /// The file metadata of a secrets file cannot be retrieved.
+    #[error("File metadata of secrets file {path} cannot be retrieved")]
+    SecretsFileMetadata {
+        /// The path to a secrets file for which metadata could not be retrieved.
+        path: PathBuf,
+        /// The source error.
+        source: std::io::Error,
+    },
+
+    /// A secrets file does not exist.
+    #[error("Secrets file not found: {path}")]
+    SecretsFileMissing {
+        /// The path to a secrets file that is missing.
+        path: PathBuf,
+    },
+
+    /// A secrets file is not a file.
+    #[error("Secrets file is not a file: {path}")]
+    SecretsFileNotAFile {
+        /// The path to a secrets file that is not a file.
+        path: PathBuf,
+    },
+
+    /// A secrets file does not have the correct permissions.
+    #[error("Secrets file {path} has permissions {mode}, but {SECRET_FILE_MODE} is required")]
+    SecretsFilePermissions {
+        /// The path to a secrets file for which permissions could not be set.
+        path: PathBuf,
+        /// The file mode that should be applied to the file at `path`.
+        mode: u32,
+    },
+
+    /// A secrets file cannot be read.
+    #[error("Failed reading secrets file {path}:\n{source}")]
+    SecretsFileRead {
+        /// The path to a secrets file that could not be read.
+        path: PathBuf,
+        /// The source error.
+        source: std::io::Error,
+    },
+
+    /// A secrets file can not be written to.
+    #[error("The secrets file {path} can not be written to for user {system_user}: {source}")]
+    SecretsFileWrite {
+        /// The path to a secrets file that could not be written to.
+        path: PathBuf,
+        /// The system user in whose home directory `path` resides.
+        system_user: SystemUserId,
+        /// The source error.
+        source: std::io::Error,
+    },
+}
+
+/// An error that may occur when loading credentials for a [`SystemUserId`].
+///
+/// Alongside an [`Error`][`crate::Error`] contains a target [`UserId`] for which the error
+/// occurred.
+#[derive(Debug)]
+pub struct CredentialsLoadingError {
+    user_id: UserId,
+    error: crate::Error,
+}
+
+impl CredentialsLoadingError {
+    /// Creates a new [`CredentialsLoadingError`].
+    pub fn new(user_id: UserId, error: crate::Error) -> Self {
+        Self { user_id, error }
+    }
+
+    /// Returns a reference to the [`UserId`].
+    pub fn get_user_id(&self) -> &UserId {
+        &self.user_id
+    }
+
+    /// Returns a reference to the [`Error`][crate::Error].
+    pub fn get_error(&self) -> &crate::Error {
+        &self.error
+    }
+}
+
+impl Display for CredentialsLoadingError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}: {}", self.user_id, self.error)
+    }
+}
+
+/// A wrapper for a list of [`CredentialsLoadingError`]s.
+#[derive(Debug)]
+pub struct CredentialsLoadingErrors {
+    errors: Vec<CredentialsLoadingError>,
+}
+
+impl CredentialsLoadingErrors {
+    /// Creates a new [`CredentialsLoadingError`].
+    pub fn new(errors: Vec<CredentialsLoadingError>) -> Self {
+        Self { errors }
+    }
+
+    /// Returns a reference to the list of [`CredentialsLoadingError`]s.
+    pub fn get_errors(&self) -> &[CredentialsLoadingError] {
+        &self.errors
+    }
+}
+
+impl Display for CredentialsLoadingErrors {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "{}",
+            self.errors
+                .iter()
+                .map(|error| error.to_string())
+                .collect::<Vec<String>>()
+                .join("\n")
+        )
+    }
+}
+
+/// A collection of credentials and credential loading errors for a system user.
+///
+/// Tracks a [`SystemUserId`], zero or more [`Credentials`] mapped to it, as well as zero or more
+/// errors related to loading the passphrase for a [`UserId`].
+#[derive(Debug)]
+pub struct CredentialsLoading {
+    mapping: ExtendedUserMapping,
+    credentials: Vec<Credentials>,
+    errors: CredentialsLoadingErrors,
+}
+
+impl CredentialsLoading {
+    /// Creates a new [`CredentialsLoading`].
+    pub fn new(
+        mapping: ExtendedUserMapping,
+        credentials: Vec<Credentials>,
+        errors: CredentialsLoadingErrors,
+    ) -> Self {
+        Self {
+            mapping,
+            credentials,
+            errors,
+        }
+    }
+
+    /// Creates a [`CredentialsLoading`] for the calling system user.
+    ///
+    /// Uses the data of the calling system user to derive the specific mapping for it from the
+    /// Signstar configuration (a [`HermeticParallelConfig`]).
+    /// Then continues to retrieve the credentials for all associated [`NetHsm`] users of the
+    /// mapping.
+    ///
+    /// # Errors
+    ///
+    /// Returns an error if
+    /// - it is not possible to derive user data from the calling process,
+    /// - if there is no user data for the calling process,
+    /// - the Signstar configuration file does not exist,
+    /// - it is not possible to load the Signstar configuration,
+    /// - not exactly one user mapping exists for the calling system user,
+    /// - or if credentials loading fails due to a severe error.
+    pub fn from_system_user() -> Result<Self, crate::Error> {
+        let user = get_current_system_user()?;
+
+        let system_config = load_config()?;
+
+        let mapping = system_config
+            .get_extended_mapping_for_user(&user.name)
+            .map_err(|source| crate::Error::Config(crate::config::Error::NetHsmConfig(source)))?;
+
+        // get all credentials for the mapping
+        let credentials_loading = mapping.load_credentials()?;
+
+        Ok(credentials_loading)
+    }
+
+    /// Returns the [`ExtendedUserMapping`].
+    pub fn get_mapping(&self) -> &ExtendedUserMapping {
+        &self.mapping
+    }
+
+    /// Returns all [`Credentials`].
+    pub fn get_credentials(&self) -> &[Credentials] {
+        &self.credentials
+    }
+
+    /// Returns a reference to a [`SystemUserId`].
+    ///
+    /// # Errors
+    ///
+    /// Returns an error if there is no system user in the tracked mapping.
+    pub fn get_system_user_id(&self) -> Result<&SystemUserId, crate::Error> {
+        match self.mapping.get_user_mapping().get_system_user() {
+            Some(system_user) => Ok(system_user),
+            None => Err(crate::Error::NonAdminSecretHandling(Error::NoSystemUser)),
+        }
+    }
+
+    /// Indicates whether there are any errors with [`UserId`]s.
+    ///
+    /// Returns `true` if there are errors, `false` otherwise.
+    pub fn has_userid_errors(&self) -> bool {
+        !self.errors.get_errors().is_empty()
+    }
+
+    /// Returns the collected errors for [`UserId`]s.
+    pub fn get_userid_errors(self) -> CredentialsLoadingErrors {
+        self.errors
+    }
+
+    /// Indicates whether the contained [`ExtendedUserMapping`] is that of a signing user.
+    pub fn has_signing_user(&self) -> bool {
+        matches!(
+            self.mapping.get_user_mapping(),
+            nethsm_config::UserMapping::SystemNetHsmOperatorSigning {
+                nethsm_user: _,
+                nethsm_key_setup: _,
+                ssh_authorized_key: _,
+                system_user: _,
+                tag: _,
+            }
+        )
+    }
+
+    /// Returns the credentials for a signing user.
+    ///
+    /// # Errors
+    ///
+    /// Returns an error if
+    /// - the tracked user is not a signing user
+    /// - errors occurred when loading the system user's credentials
+    /// - or there are no credentials for the system user.
+    pub fn credentials_for_signing_user(self) -> Result<Credentials, crate::Error> {
+        if !self.has_signing_user() {
+            return Err(crate::Error::NonAdminSecretHandling(Error::NotSigningUser));
+        }
+
+        if !self.errors.get_errors().is_empty() {
+            return Err(crate::Error::NonAdminSecretHandling(
+                Error::CredentialsLoading {
+                    system_user: self.get_system_user_id()?.clone(),
+                    errors: self.errors,
+                },
+            ));
+        }
+
+        if let Some(credentials) = self.credentials.first() {
+            Ok(credentials.clone())
+        } else {
+            return Err(crate::Error::NonAdminSecretHandling(
+                Error::CredentialsMissing {
+                    system_user: self.get_system_user_id()?.clone(),
+                },
+            ));
+        }
+    }
+}
+
+/// A trait to implement loading of credentials, which includes reading of secrets.
+pub trait SecretsReader {
+    /// Loads credentials.
+    fn load_credentials(self) -> Result<CredentialsLoading, crate::Error>;
+}
+
+/// Checks the accessibility of a secrets file.
+///
+/// Checks whether file at `path`
+/// - exists,
+/// - is a file,
+/// - has accessible metadata,
+/// - and has the file mode [`SECRET_FILE_MODE`].
+///
+/// # Errors
+///
+/// Returns an error, if the file at `path`
+/// - does not exist,
+/// - is not a file,
+/// - does not have accessible metadata,
+/// - or has a file mode other than [`SECRET_FILE_MODE`].
+fn check_secrets_file(path: &Path) -> Result<(), crate::Error> {
+    // check if a path exists
+    if !path.exists() {
+        return Err(crate::Error::NonAdminSecretHandling(
+            Error::SecretsFileMissing {
+                path: path.to_path_buf(),
+            },
+        ));
+    }
+
+    // check if this is a file
+    if !path.is_file() {
+        return Err(crate::Error::NonAdminSecretHandling(
+            Error::SecretsFileNotAFile {
+                path: path.to_path_buf(),
+            },
+        ));
+    }
+
+    // check for correct permissions
+    match path.metadata() {
+        Ok(metadata) => {
+            let mode = metadata.permissions().mode();
+            if mode != SECRET_FILE_MODE {
+                return Err(crate::Error::NonAdminSecretHandling(
+                    Error::SecretsFilePermissions {
+                        path: path.to_path_buf(),
+                        mode,
+                    },
+                ));
+            }
+        }
+        Err(source) => {
+            return Err(crate::Error::NonAdminSecretHandling(
+                Error::SecretsFileMetadata {
+                    path: path.to_path_buf(),
+                    source,
+                },
+            ));
+        }
+    }
+
+    Ok(())
+}
+
+impl SecretsReader for ExtendedUserMapping {
+    /// Loads credentials for each [`UserId`] associated with a [`SystemUserId`].
+    ///
+    /// The [`SystemUserId`] of the mapping must be equal to the current system user calling this
+    /// function.
+    /// Relies on [`get_plaintext_secret_file`] and [`get_systemd_creds_secret_file`] to retrieve
+    /// the specific path to a secret file for each [`UserId`] mapped to a [`SystemUserId`].
+    ///
+    /// Returns a [`CredentialsLoading`], which may contain critical errors related to loading a
+    /// passphrase for each available [`UserId`].
+    /// The caller is expected to handle any errors tracked in the returned object based on context.
+    ///
+    /// # Errors
+    ///
+    /// Returns an error if
+    /// - the [`ExtendedUserMapping`] provides no [`SystemUserId`],
+    /// - no system user equal to the [`SystemUserId`] exists,
+    /// - the [`SystemUserId`] is not equal to the currently calling system user,
+    /// - or the [systemd-creds] command is not available when trying to decrypt secrets.
+    ///
+    /// [systemd-creds]: https://man.archlinux.org/man/systemd-creds.1
+    fn load_credentials(self) -> Result<CredentialsLoading, crate::Error> {
+        // Retrieve required SystemUserId and User and compare with current User.
+        let (system_user, user) = get_system_user_pair(&self)?;
+        let current_system_user = get_current_system_user()?;
+
+        // fail if running as root
+        fail_if_root(&current_system_user)?;
+        match_current_system_user(&current_system_user, &user)?;
+
+        let secret_handling = self.get_non_admin_secret_handling();
+        let mut credentials = Vec::new();
+        let mut errors = Vec::new();
+
+        for user_id in self.get_user_mapping().get_nethsm_users() {
+            let secrets_file = match secret_handling {
+                NonAdministrativeSecretHandling::Plaintext => {
+                    get_plaintext_secret_file(system_user.as_ref(), &user_id.to_string())
+                }
+                NonAdministrativeSecretHandling::SystemdCreds => {
+                    get_systemd_creds_secret_file(system_user.as_ref(), &user_id.to_string())
+                }
+            };
+            // Ensure the secrets file has correct ownership and permissions.
+            if let Err(error) = check_secrets_file(secrets_file.as_path()) {
+                errors.push(CredentialsLoadingError::new(user_id, error));
+                continue;
+            };
+
+            match secret_handling {
+                // Read from plaintext secrets file.
+                NonAdministrativeSecretHandling::Plaintext => {
+                    // get passphrase or error
+                    match read_to_string(&secrets_file)
+                        .map_err(|source| Error::SecretsFileRead {
+                            path: secrets_file,
+                            source,
+                        })
+                        .map_err(crate::Error::NonAdminSecretHandling)
+                    {
+                        Ok(passphrase) => credentials
+                            .push(Credentials::new(user_id, Some(Passphrase::new(passphrase)))),
+                        Err(error) => {
+                            errors.push(CredentialsLoadingError::new(user_id, error));
+                            continue;
+                        }
+                    }
+                }
+                // Read from systemd-creds encrypted secrets file.
+                NonAdministrativeSecretHandling::SystemdCreds => {
+                    // Decrypt secret using systemd-creds.
+                    let creds_command = get_command("systemd-creds")?;
+                    let mut command = Command::new(creds_command);
+                    let command = command
+                        .arg("--user")
+                        .arg("decrypt")
+                        .arg(&secrets_file)
+                        .arg("-");
+                    match command
+                        .output()
+                        .map_err(|source| crate::Error::CommandExec {
+                            command: format!("{command:?}"),
+                            source,
+                        }) {
+                        Ok(command_output) => {
+                            // fail if decryption did not result in a successful status code
+                            if !command_output.status.success() {
+                                errors.push(CredentialsLoadingError::new(
+                                    user_id,
+                                    crate::Error::CommandNonZero {
+                                        command: format!("{command:?}"),
+                                        exit_status: command_output.status,
+                                        stderr: String::from_utf8_lossy(&command_output.stderr)
+                                            .into_owned(),
+                                    },
+                                ));
+                                continue;
+                            }
+
+                            let creds = match String::from_utf8(command_output.stdout) {
+                                Ok(creds) => creds,
+                                Err(source) => {
+                                    errors.push(CredentialsLoadingError::new(
+                                        user_id.clone(),
+                                        crate::Error::Utf8String {
+                                            path: secrets_file,
+                                            context: format!(
+                                                "converting stdout of {command:?} to string"
+                                            ),
+                                            source,
+                                        },
+                                    ));
+                                    continue;
+                                }
+                            };
+
+                            credentials
+                                .push(Credentials::new(user_id, Some(Passphrase::new(creds))));
+                        }
+                        Err(error) => {
+                            errors.push(CredentialsLoadingError::new(user_id, error));
+                            continue;
+                        }
+                    }
+                }
+            }
+        }
+
+        Ok(CredentialsLoading::new(
+            self,
+            credentials,
+            CredentialsLoadingErrors { errors },
+        ))
+    }
+}
+
+/// A trait to create non-administrative secrets and accompanying directories.
+pub trait SecretsWriter {
+    /// Creates secrets directories for all non-administrative mappings.
+    fn create_secrets_dir(&self) -> Result<(), crate::Error>;
+
+    /// Creates non-administrative secrets for all mappings of system users to backend users.
+    fn create_non_administrative_secrets(&self) -> Result<(), crate::Error>;
+}
+
+impl SecretsWriter for ExtendedUserMapping {
+    /// Creates secrets directories for all non-administrative mappings.
+    ///
+    /// Matches the [`SystemUserId`] in a mapping with an actual user on the system.
+    /// Creates the passphrase directory for the user and ensures correct ownership of it and all
+    /// parent directories up until the user's home directory.
+    ///
+    /// # Errors
+    ///
+    /// Returns an error if
+    /// - no system user is available in the mapping,
+    /// - the system user of the mapping is not available on the system,
+    /// - the directory could not be created,
+    /// - the ownership of any directory between the user's home and the passphrase directory can
+    ///   not be changed.
+    fn create_secrets_dir(&self) -> Result<(), crate::Error> {
+        // Retrieve required SystemUserId and User and compare with current User.
+        let (system_user, user) = get_system_user_pair(self)?;
+
+        // fail if not running as root
+        fail_if_not_root(&get_current_system_user()?)?;
+
+        // get and create the user's passphrase directory
+        let secrets_dir = get_user_secrets_dir(system_user.as_ref());
+        create_dir_all(&secrets_dir).map_err(|source| Error::SecretsDirCreate {
+            path: secrets_dir.clone(),
+            system_user: system_user.clone(),
+            source,
+        })?;
+
+        // Recursively chown all directories to the user and group, until `HOME_BASE_DIR` is
+        // reached.
+        let home_dir = get_home_base_dir_path().join(PathBuf::from(system_user.as_ref()));
+        let mut chown_dir = secrets_dir.clone();
+        while chown_dir != home_dir {
+            chown(&chown_dir, Some(user.uid.as_raw()), Some(user.gid.as_raw())).map_err(
+                |source| crate::Error::Chown {
+                    path: chown_dir.to_path_buf(),
+                    user: system_user.to_string(),
+                    source,
+                },
+            )?;
+            if let Some(parent) = &chown_dir.parent() {
+                chown_dir = parent.to_path_buf()
+            } else {
+                break;
+            }
+        }
+
+        Ok(())
+    }
+
+    /// Creates passphrases for all non-administrative mappings.
+    ///
+    /// Creates a random alphanumeric, 30-char long passphrase for each backend user of each
+    /// non-administrative user mapping.
+    ///
+    /// - If `self` is configured to use [`NonAdministrativeSecretHandling::Plaintext`], the
+    ///   passphrase is stored in a secrets file, defined by [`get_plaintext_secret_file`].
+    /// - If `self` is configured to use [`NonAdministrativeSecretHandling::SystemdCreds`], the
+    ///   passphrase is encrypted using [systemd-creds] and stored in a secrets file, defined by
+    ///   [`get_systemd_creds_secret_file`].
+    ///
+    /// # Errors
+    ///
+    /// Returns an error if
+    /// - the targeted system user does not exist in the mapping or on the system,
+    /// - the function is called using a non-root user,
+    /// - the [systemd-creds] command is not available when trying to encrypt the passphrase,
+    /// - the encryption of the passphrase using [systemd-creds] fails,
+    /// - the secrets file can not be created,
+    /// - the secrets file can not be written to,
+    /// - or the ownership and permissions of the secrets file can not be changed.
+    ///
+    /// [systemd-creds]: https://man.archlinux.org/man/systemd-creds.1
+    fn create_non_administrative_secrets(&self) -> Result<(), crate::Error> {
+        // Retrieve required SystemUserId and User.
+        let (system_user, user) = get_system_user_pair(self)?;
+
+        // fail if not running as root
+        fail_if_not_root(&get_current_system_user()?)?;
+
+        let secret_handling = self.get_non_admin_secret_handling();
+
+        // add a secret for each NetHSM user
+        for user_id in self.get_user_mapping().get_nethsm_users() {
+            let secrets_file = match secret_handling {
+                NonAdministrativeSecretHandling::Plaintext => {
+                    get_plaintext_secret_file(system_user.as_ref(), &user_id.to_string())
+                }
+                NonAdministrativeSecretHandling::SystemdCreds => {
+                    get_systemd_creds_secret_file(system_user.as_ref(), &user_id.to_string())
+                }
+            };
+            println!(
+                "Create secret for system user {system_user} and backend user {user_id} in file: {secrets_file:?}"
+            );
+            let secret = {
+                // create initial (unencrypted) secret
+                let initial_secret: String = thread_rng()
+                    .sample_iter(&Alphanumeric)
+                    .take(30)
+                    .map(char::from)
+                    .collect();
+                // Create credentials files depending on secret handling
+                match secret_handling {
+                    NonAdministrativeSecretHandling::Plaintext => {
+                        initial_secret.as_bytes().to_vec()
+                    }
+                    NonAdministrativeSecretHandling::SystemdCreds => {
+                        // Create systemd-creds encrypted secret.
+                        let creds_command = get_command("systemd-creds")?;
+                        let mut command = Command::new(creds_command);
+                        let command = command
+                            .arg("--user")
+                            .arg("--name=")
+                            .arg("--uid")
+                            .arg(system_user.as_ref())
+                            .arg("encrypt")
+                            .arg("-")
+                            .arg("-");
+                        let mut command_child = command
+                            .stdin(Stdio::piped())
+                            .stdout(Stdio::piped())
+                            .spawn()
+                            .map_err(|source| crate::Error::CommandBackground {
+                                command: format!("{command:?}"),
+                                source,
+                            })?;
+                        let Some(mut stdin) = command_child.stdin.take() else {
+                            return Err(crate::Error::CommandAttachToStdin {
+                                command: format!("{command:?}"),
+                            })?;
+                        };
+
+                        let system_user_thread = system_user.clone();
+                        let handle = std::thread::spawn(move || {
+                            stdin
+                                .write_all(initial_secret.as_bytes())
+                                .map_err(|source| crate::Error::CommandWriteToStdin {
+                                    command:
+                                        format!("systemd-creds --user --name= --uid {system_user_thread} encrypt - -"),
+                                    source,
+                                })
+                        });
+
+                        let _handle_result = handle.join().map_err(|source| crate::Error::Thread {
+                            context: format!(
+                                "storing systemd-creds encrypted non-administrative secrets: {source:?}"
+                            ),
+                        })?;
+
+                        let command_output =
+                            command_child.wait_with_output().map_err(|source| {
+                                crate::Error::CommandExec {
+                                    command: format!("{command:?}"),
+                                    source,
+                                }
+                            })?;
+
+                        if !command_output.status.success() {
+                            return Err(crate::Error::CommandNonZero {
+                                command: format!("{command:?}"),
+                                exit_status: command_output.status,
+                                stderr: String::from_utf8_lossy(&command_output.stderr)
+                                    .into_owned(),
+                            });
+                        }
+                        command_output.stdout
+                    }
+                }
+            };
+
+            // Write secret to file and adjust permission and ownership of file.
+            let mut file = File::create(secrets_file.as_path()).map_err(|source| {
+                Error::SecretsFileCreate {
+                    path: secrets_file.clone(),
+                    system_user: system_user.clone(),
+                    source,
+                }
+            })?;
+            file.write_all(&secret)
+                .map_err(|source| Error::SecretsFileWrite {
+                    path: secrets_file.clone(),
+                    system_user: system_user.clone(),
+                    source,
+                })?;
+            chown(
+                &secrets_file,
+                Some(user.uid.as_raw()),
+                Some(user.gid.as_raw()),
+            )
+            .map_err(|source| crate::Error::Chown {
+                path: secrets_file.clone(),
+                user: system_user.to_string(),
+                source,
+            })?;
+            set_permissions(
+                secrets_file.as_path(),
+                Permissions::from_mode(SECRET_FILE_MODE),
+            )
+            .map_err(|source| crate::Error::ApplyPermissions {
+                path: secrets_file.clone(),
+                mode: SECRET_FILE_MODE,
+                source,
+            })?;
+        }
+        Ok(())
+    }
+}
diff --git a/signstar-config/src/utils.rs b/signstar-config/src/utils.rs
new file mode 100644
index 00000000..1ab8050f
--- /dev/null
+++ b/signstar-config/src/utils.rs
@@ -0,0 +1,198 @@
+//! Utilities for signstar-config.
+use std::path::PathBuf;
+
+use nethsm_config::{ExtendedUserMapping, SystemUserId};
+use nix::unistd::{User, geteuid};
+use which::which;
+
+/// An error that may occur when using signstar-config utils.
+#[derive(Debug, thiserror::Error)]
+pub enum Error {
+    /// An executable that is supposed to be called, is not found.
+    #[error("Unable to to find executable \"{command}\"")]
+    ExecutableNotFound {
+        /// The executable that could not be found.
+        command: String,
+        /// The source error.
+        source: which::Error,
+    },
+
+    /// An [`ExtendedUserMapping`] does not provide a system user.
+    #[error("The user mapping does not provide a system user:\n{0}")]
+    MappingSystemUserGet(String),
+
+    /// There is no data about a system user.
+    #[error("Data for system user {user} is missing")]
+    SystemUserData {
+        /// The user identifier for which data is missing.
+        user: NameOrUid,
+    },
+
+    /// Unable to lookup system user data (by name or from process EUID).
+    #[error(
+        "Unable to lookup data for system user {}:\n{source}",
+        match user {
+            NameOrUid::Name(name) => format!("user {name}"),
+            NameOrUid::Uid(uid) => format!("uid {uid}"),
+        }
+    )]
+    SystemUserLookup {
+        /// The user identifier for which data could not be looked up.
+        user: NameOrUid,
+        /// The source error.
+        source: nix::errno::Errno,
+    },
+
+    /// The calling user does not match the targeted system user.
+    #[error(
+        "The targeted system user {target_user} is not the currently calling system user {current_user}."
+    )]
+    SystemUserMismatch {
+        /// The system user that is the target of the operation.
+        target_user: String,
+        /// The currently calling system user.
+        current_user: String,
+    },
+
+    /// The current user is an unprivileged user, but should be root.
+    #[error("The command requires running as root, but running as \"{user}\"")]
+    SystemUserNotRoot {
+        /// The system user that is used instead of `root`.
+        user: String,
+    },
+
+    /// The current user is root, but should be an unprivileged user.
+    #[error("The command must not be run as root, but running as \"root\"")]
+    SystemUserRoot,
+}
+
+/// A name or uid of a system user on a host
+#[derive(Debug, strum::Display)]
+pub enum NameOrUid {
+    /// The name of the system user.
+    Name(SystemUserId),
+    /// The ID of the system user.
+    Uid(nix::unistd::Uid),
+}
+
+/// Returns the path to a `command`.
+///
+/// Searches for an executable in `$PATH` of the current environment and returns the first one
+/// found.
+///
+/// # Errors
+///
+/// Returns an error if no executable matches the provided `command`.
+pub(crate) fn get_command(command: &str) -> Result<PathBuf, Error> {
+    which(command).map_err(|source| Error::ExecutableNotFound {
+        command: command.to_string(),
+        source,
+    })
+}
+
+/// Fails if not running as root.
+///
+/// Evaluates the effective user ID.
+///
+/// # Errors
+///
+/// Returns an error if the effective user ID is not that of root.
+pub(crate) fn fail_if_not_root(user: &User) -> Result<(), Error> {
+    if !user.uid.is_root() {
+        return Err(Error::SystemUserNotRoot {
+            user: user.name.clone(),
+        });
+    }
+    Ok(())
+}
+
+/// Fails if running as root.
+///
+/// Evaluates the effective user ID.
+///
+/// # Errors
+///
+/// Returns an error if the effective user ID is that of root.
+pub(crate) fn fail_if_root(user: &User) -> Result<(), Error> {
+    if user.uid.is_root() {
+        return Err(Error::SystemUserRoot);
+    }
+    Ok(())
+}
+
+/// Returns the [`User`] associated with the current process.
+///
+/// Retrieves user data of the system based on the effective user ID of the current process.
+///
+/// # Errors
+///
+/// Returns an error if
+/// - no user data can be derived from the current process
+/// - no user data can be found on the system, associated with the ID of the user of the current
+///   process.
+pub(crate) fn get_current_system_user() -> Result<User, Error> {
+    let euid = geteuid();
+    let Some(user) = User::from_uid(euid).map_err(|source| Error::SystemUserLookup {
+        user: NameOrUid::Uid(euid),
+        source,
+    })?
+    else {
+        return Err(Error::SystemUserData {
+            user: NameOrUid::Uid(euid),
+        });
+    };
+    Ok(user)
+}
+
+/// Checks whether the current system user is the targeted user.
+///
+/// Compares two [`User`] instances and fails if they are not the same.
+///
+/// # Errors
+///
+/// Returns an error if the current system user is not the targeted user.
+pub(crate) fn match_current_system_user(
+    current_user: &User,
+    target_user: &User,
+) -> Result<(), Error> {
+    if current_user != target_user {
+        return Err(Error::SystemUserMismatch {
+            target_user: target_user.name.clone(),
+            current_user: current_user.name.clone(),
+        });
+    }
+    Ok(())
+}
+
+/// Returns a [`SystemUserId`] and matching Unix system [`User`] associated with it.
+///
+/// # Errors
+///
+/// Returns an error if
+/// - there is no [`SystemUserId`] in the mapping,
+/// - or no [`User`] data can be retrieved from a found [`SystemUserId`].
+pub(crate) fn get_system_user_pair(
+    mapping: &ExtendedUserMapping,
+) -> Result<(SystemUserId, User), Error> {
+    // retrieve the targeted system user from the mapping
+    let Some(system_user) = mapping.get_user_mapping().get_system_user() else {
+        return Err(Error::MappingSystemUserGet(format!(
+            "{:?}",
+            mapping.get_user_mapping()
+        )));
+    };
+
+    // retrieve the actual user data on the system
+    let Some(user) =
+        User::from_name(system_user.as_ref()).map_err(|source| Error::SystemUserLookup {
+            user: NameOrUid::Name(system_user.clone()),
+            source,
+        })?
+    else {
+        return Err(Error::SystemUserData {
+            user: NameOrUid::Name(system_user.clone()),
+        });
+    };
+
+    Ok((system_user.clone(), user))
+}
diff --git a/signstar-config/tests/admin_credentials/mod.rs b/signstar-config/tests/admin_credentials/mod.rs
new file mode 100644
index 00000000..a02f6726
--- /dev/null
+++ b/signstar-config/tests/admin_credentials/mod.rs
@@ -0,0 +1,182 @@
+//! Integration tests for [`signstar_config::admin_credentials`].
+use std::fs::{copy, create_dir_all, read_to_string};
+
+use nethsm::UserId;
+use nethsm_config::AdministrativeSecretHandling;
+use rstest::rstest;
+use signstar_common::admin_credentials::{
+    create_credentials_dir,
+    get_credentials_dir,
+    get_plaintext_credentials_file,
+    get_systemd_creds_credentials_file,
+};
+use signstar_config::admin_credentials::{AdminCredentials, User};
+use testresult::TestResult;
+
+use super::utils::write_machine_id;
+use crate::utils::{SIGNSTAR_ADMIN_CREDS_SIMPLE, get_tmp_config};
+
+#[rstest]
+fn fail_to_load_on_path_not_a_file() -> TestResult {
+    let credentials_file = get_plaintext_credentials_file();
+    create_dir_all(&credentials_file)?;
+    let error = AdminCredentials::load(AdministrativeSecretHandling::Plaintext)
+        .expect_err("Did not return an error!");
+    if let signstar_config::Error::AdminSecretHandling(
+        signstar_config::admin_credentials::Error::CredsFileNotAFile { path },
+    ) = error
+    {
+        assert_eq!(credentials_file, path)
+    } else {
+        panic!("Did not return an the correct error variant!")
+    }
+
+    Ok(())
+}
+
+/// Copies a plaintext TOML containing admin credentials to the default location and loads it.
+#[rstest]
+fn load_plaintext_toml() -> TestResult {
+    create_credentials_dir()?;
+    let config_file = get_tmp_config(SIGNSTAR_ADMIN_CREDS_SIMPLE)?;
+
+    copy(config_file, get_plaintext_credentials_file())?;
+
+    let creds = AdminCredentials::load(AdministrativeSecretHandling::Plaintext)?;
+    println!("{creds:?}");
+    Ok(())
+}
+
+/// Stores as plaintext TOML in the default location and compares to a fixture.
+#[rstest]
+fn store_plaintext_toml() -> TestResult {
+    let config_file = get_tmp_config(SIGNSTAR_ADMIN_CREDS_SIMPLE)?;
+    let creds = AdminCredentials::new(
+        1,
+        "backup-passphrase".to_string(),
+        "unlock-passphrase".to_string(),
+        vec![User::new(
+            UserId::new("admin".to_string())?,
+            "admin-passphrase".to_string(),
+        )],
+        vec![User::new(
+            UserId::new("ns1~admin".to_string())?,
+            "ns1-admin-passphrase".to_string(),
+        )],
+    );
+    creds.store(AdministrativeSecretHandling::Plaintext)?;
+
+    let creds_string = read_to_string(get_plaintext_credentials_file())?;
+    let fixture_string = read_to_string(config_file)?;
+    assert_eq!(creds_string, fixture_string);
+    Ok(())
+}
+
+/// Stores as systemd-creds encrypted TOML in the default location and compares to a fixture.
+#[rstest]
+fn store_and_load_systemd_creds() -> TestResult {
+    write_machine_id()?;
+
+    let config_file = get_tmp_config(SIGNSTAR_ADMIN_CREDS_SIMPLE)?;
+
+    // load AdminCredentials from plaintext fixture
+    let creds =
+        AdminCredentials::load_from_file(&config_file, AdministrativeSecretHandling::Plaintext)?;
+
+    println!("Store systemd-creds encrypted");
+    creds.store(AdministrativeSecretHandling::SystemdCreds)?;
+
+    println!("Load systemd-creds encrypted");
+    let read_creds = AdminCredentials::load(AdministrativeSecretHandling::SystemdCreds)?;
+
+    println!("Write systemd-creds encrypted to plaintext");
+    read_creds.store(AdministrativeSecretHandling::Plaintext)?;
+
+    println!("Compare plaintext of roundtripped and fixture");
+    let creds_string = read_to_string(get_plaintext_credentials_file())?;
+    let fixture_string = read_to_string(&config_file)?;
+    assert_eq!(creds_string, fixture_string);
+    Ok(())
+}
+
+#[rstest]
+#[case(AdministrativeSecretHandling::Plaintext)]
+#[case(AdministrativeSecretHandling::SystemdCreds)]
+fn load_admin_creds_from_default_location(
+    #[case] handling: AdministrativeSecretHandling,
+) -> TestResult {
+    write_machine_id()?;
+    create_credentials_dir()?;
+    let config_file = get_tmp_config(SIGNSTAR_ADMIN_CREDS_SIMPLE)?;
+
+    match handling {
+        AdministrativeSecretHandling::Plaintext => {
+            // make sure a dummy plaintext file exists
+            copy(config_file, get_plaintext_credentials_file())?;
+        }
+        AdministrativeSecretHandling::SystemdCreds => {
+            // load AdminCredentials from plaintext fixture and store it systemd-creds encrypted
+            let creds = AdminCredentials::load_from_file(
+                &config_file,
+                AdministrativeSecretHandling::Plaintext,
+            )?;
+            creds.store(AdministrativeSecretHandling::SystemdCreds)?;
+        }
+        AdministrativeSecretHandling::ShamirsSecretSharing => {
+            unimplemented!("Shamir's Secret Sharing is not yet implemented!");
+        }
+    }
+    AdminCredentials::load(handling)?;
+
+    Ok(())
+}
+
+#[rstest]
+#[case(AdministrativeSecretHandling::Plaintext)]
+#[case(AdministrativeSecretHandling::SystemdCreds)]
+fn store_admin_creds_to_default_location(
+    #[case] handling: AdministrativeSecretHandling,
+) -> TestResult {
+    write_machine_id()?;
+
+    let config_file = get_tmp_config(SIGNSTAR_ADMIN_CREDS_SIMPLE)?;
+
+    // load credentials from plaintext fixture
+    let admin_creds =
+        AdminCredentials::load_from_file(&config_file, AdministrativeSecretHandling::Plaintext)?;
+
+    // force remove any files that may be present in the persistent location
+    let _remove = std::fs::remove_dir_all(get_credentials_dir());
+    admin_creds.store(handling)?;
+
+    match handling {
+        AdministrativeSecretHandling::Plaintext => {
+            assert!(get_plaintext_credentials_file().exists());
+        }
+        AdministrativeSecretHandling::SystemdCreds => {
+            assert!(get_systemd_creds_credentials_file().exists());
+        }
+        AdministrativeSecretHandling::ShamirsSecretSharing => {
+            unimplemented!("Shamir's Secret Sharing is not yet implemented!");
+        }
+    }
+
+    Ok(())
+}
+
+#[rstest]
+fn fail_to_load_on_missing_file() -> TestResult {
+    let credentials_file = get_plaintext_credentials_file();
+    let error = AdminCredentials::load(AdministrativeSecretHandling::Plaintext)
+        .expect_err("Did not return an error!");
+    if let signstar_config::Error::AdminSecretHandling(
+        signstar_config::admin_credentials::Error::CredsFileMissing { path },
+    ) = error
+    {
+        assert_eq!(credentials_file, path)
+    } else {
+        panic!("Did not return an the correct error variant!")
+    }
+
+    Ok(())
+}
diff --git a/signstar-config/tests/config/mod.rs b/signstar-config/tests/config/mod.rs
new file mode 100644
index 00000000..1ece26d1
--- /dev/null
+++ b/signstar-config/tests/config/mod.rs
@@ -0,0 +1,64 @@
+//! Integration tests for [`signstar_config::config`].
+use std::{fs::copy, path::PathBuf};
+
+use rstest::rstest;
+use signstar_common::config::{
+    create_default_config_dir,
+    create_etc_override_config_dir,
+    create_run_override_config_dir,
+    create_usr_local_override_config_dir,
+    get_default_config_dir_path,
+    get_default_config_file_path,
+    get_etc_override_config_file_path,
+    get_etc_override_dir_path,
+    get_run_override_config_file_path,
+    get_run_override_dir_path,
+    get_usr_local_override_config_file_path,
+    get_usr_local_override_dir_path,
+};
+use signstar_config::config::load_config;
+use testresult::TestResult;
+
+use crate::utils::{SIGNSTAR_CONFIG_FULL, get_tmp_config};
+
+#[rstest]
+#[case(get_default_config_dir_path())]
+#[case(get_etc_override_dir_path())]
+#[case(get_run_override_dir_path())]
+#[case(get_usr_local_override_dir_path())]
+fn load_config_from_default_location(#[case] config_dir: PathBuf) -> TestResult {
+    println!("Config dir to test: {config_dir:?}");
+    let config_file_fixture = get_tmp_config(SIGNSTAR_CONFIG_FULL)?;
+    // force remove any files that may be present in any of the configuration dirs
+    for dir in [
+        get_default_config_dir_path(),
+        get_etc_override_dir_path(),
+        get_run_override_dir_path(),
+        get_usr_local_override_dir_path(),
+    ] {
+        let _remove = std::fs::remove_dir_all(dir);
+    }
+
+    let config_file_path = if config_dir == get_usr_local_override_dir_path() {
+        create_usr_local_override_config_dir()?;
+        get_usr_local_override_config_file_path()
+    } else if config_dir == get_default_config_dir_path() {
+        create_default_config_dir()?;
+        get_default_config_file_path()
+    } else if config_dir == get_etc_override_dir_path() {
+        create_etc_override_config_dir()?;
+        get_etc_override_config_file_path()
+    } else if config_dir == get_run_override_dir_path() {
+        create_run_override_config_dir()?;
+        get_run_override_config_file_path()
+    } else {
+        unimplemented!("No test case for config dir: {config_dir:?}")
+    };
+
+    println!("Copying {config_file_fixture:?} to {config_file_path:?}");
+    copy(config_file_fixture, &config_file_path)?;
+
+    load_config()?;
+
+    Ok(())
+}
diff --git a/signstar-config/tests/fixtures/admin-creds-simple.toml b/signstar-config/tests/fixtures/admin-creds-simple.toml
new file mode 100644
index 00000000..ec5796e0
--- /dev/null
+++ b/signstar-config/tests/fixtures/admin-creds-simple.toml
@@ -0,0 +1,11 @@
+iteration = 1
+backup_passphrase = "backup-passphrase"
+unlock_passphrase = "unlock-passphrase"
+
+[[administrators]]
+name = "admin"
+passphrase = "admin-passphrase"
+
+[[namespace_administrators]]
+name = "ns1~admin"
+passphrase = "ns1-admin-passphrase"
diff --git a/signstar-config/tests/fixtures/signstar-config-full.toml b/signstar-config/tests/fixtures/signstar-config-full.toml
new file mode 100644
index 00000000..152ad3ef
--- /dev/null
+++ b/signstar-config/tests/fixtures/signstar-config-full.toml
@@ -0,0 +1,129 @@
+iteration = 1
+admin_secret_handling = "shamirs-secret-sharing"
+non_admin_secret_handling = "systemd-creds"
+
+[[connections]]
+url = "https://localhost:8443/api/v1/"
+tls_security = "Unsafe"
+
+[[users]]
+nethsm_only_admin = "admin"
+
+[[users]]
+
+[users.system_nethsm_backup]
+nethsm_user = "backup1"
+ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host"
+system_user = "ssh-backup1"
+
+[[users]]
+
+[users.system_nethsm_metrics]
+ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host"
+system_user = "ssh-metrics1"
+
+[users.system_nethsm_metrics.nethsm_users]
+metrics_user = "metrics1"
+operator_users = ["operator1metrics1", "ns1~operator1metrics1"]
+
+[[users]]
+
+[users.system_nethsm_operator_signing]
+nethsm_user = "operator1"
+ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host"
+system_user = "ssh-operator1"
+tag = "tag1"
+
+[users.system_nethsm_operator_signing.nethsm_key_setup]
+key_id = "key1"
+key_type = "Curve25519"
+key_mechanisms = ["EdDsaSignature"]
+signature_type = "EdDsa"
+
+[users.system_nethsm_operator_signing.nethsm_key_setup.key_context.openpgp]
+user_ids = ["Foobar McFooface <foobar@mcfooface.org>"]
+version = "4"
+
+[[users]]
+
+[users.system_nethsm_operator_signing]
+nethsm_user = "operator2"
+ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host"
+system_user = "ssh-operator2"
+tag = "tag2"
+
+[users.system_nethsm_operator_signing.nethsm_key_setup]
+key_id = "key2"
+key_type = "Curve25519"
+key_mechanisms = ["EdDsaSignature"]
+signature_type = "EdDsa"
+
+[users.system_nethsm_operator_signing.nethsm_key_setup.key_context.openpgp]
+user_ids = ["Foobar McFooface <foobar@mcfooface.org>"]
+version = "4"
+
+[[users]]
+nethsm_only_admin = "ns1~admin"
+
+[[users]]
+
+[users.system_nethsm_operator_signing]
+nethsm_user = "ns1~operator1"
+ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host"
+system_user = "ns1-ssh-operator1"
+tag = "tag1"
+
+[users.system_nethsm_operator_signing.nethsm_key_setup]
+key_id = "key1"
+key_type = "Curve25519"
+key_mechanisms = ["EdDsaSignature"]
+signature_type = "EdDsa"
+
+[users.system_nethsm_operator_signing.nethsm_key_setup.key_context.openpgp]
+user_ids = ["Foobar McFooface <foobar@mcfooface.org>"]
+version = "4"
+
+[[users]]
+
+[users.system_nethsm_operator_signing]
+nethsm_user = "ns1~operator2"
+ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINrIYA+bfMBThUP5lKbMFEHiytmcCPhpkGrB/85n0mAN user@host"
+system_user = "ns1-ssh-operator2"
+tag = "tag2"
+
+[users.system_nethsm_operator_signing.nethsm_key_setup]
+key_id = "key2"
+key_type = "Curve25519"
+key_mechanisms = ["EdDsaSignature"]
+signature_type = "EdDsa"
+
+[users.system_nethsm_operator_signing.nethsm_key_setup.key_context.openpgp]
+user_ids = ["Foobar McFooface <foobar@mcfooface.org>"]
+version = "4"
+
+[[users]]
+
+[users.hermetic_system_nethsm_metrics]
+system_user = "local-metrics1"
+
+[users.hermetic_system_nethsm_metrics.nethsm_users]
+metrics_user = "metrics2"
+operator_users = ["operator2metrics1"]
+
+[[users]]
+
+[users.system_only_share_download]
+ssh_authorized_keys = ["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host"]
+system_user = "ssh-share-down"
+
+[[users]]
+
+[users.system_only_share_upload]
+ssh_authorized_keys = ["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host"]
+system_user = "ssh-share-up"
+
+[[users]]
+
+[users.system_only_wireguard_download]
+ssh_authorized_keys = ["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host"]
+system_user = "ssh-wireguard-down"
diff --git a/signstar-config/tests/fixtures/signstar-config-plaintext.toml b/signstar-config/tests/fixtures/signstar-config-plaintext.toml
new file mode 100644
index 00000000..205e4f5f
--- /dev/null
+++ b/signstar-config/tests/fixtures/signstar-config-plaintext.toml
@@ -0,0 +1,129 @@
+iteration = 1
+admin_secret_handling = "shamirs-secret-sharing"
+non_admin_secret_handling = "plaintext"
+
+[[connections]]
+url = "https://localhost:8443/api/v1/"
+tls_security = "Unsafe"
+
+[[users]]
+nethsm_only_admin = "admin"
+
+[[users]]
+
+[users.system_nethsm_backup]
+nethsm_user = "backup1"
+ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host"
+system_user = "ssh-backup1"
+
+[[users]]
+
+[users.system_nethsm_metrics]
+ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPDgwGfIRBAsOUuDEZw/uJQZSwOYr4sg2DAZpcc7MfOj user@host"
+system_user = "ssh-metrics1"
+
+[users.system_nethsm_metrics.nethsm_users]
+metrics_user = "metrics1"
+operator_users = ["operator1metrics1", "ns1~operator1metrics1"]
+
+[[users]]
+
+[users.system_nethsm_operator_signing]
+nethsm_user = "operator1"
+ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAN54Gd1jMz+yNDjBRwX1SnOtWuUsVF64RJIeYJ8DI7b user@host"
+system_user = "ssh-operator1"
+tag = "tag1"
+
+[users.system_nethsm_operator_signing.nethsm_key_setup]
+key_id = "key1"
+key_type = "Curve25519"
+key_mechanisms = ["EdDsaSignature"]
+signature_type = "EdDsa"
+
+[users.system_nethsm_operator_signing.nethsm_key_setup.key_context.openpgp]
+user_ids = ["Foobar McFooface <foobar@mcfooface.org>"]
+version = "4"
+
+[[users]]
+
+[users.system_nethsm_operator_signing]
+nethsm_user = "operator2"
+ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh9BTe81DC6A0YZALsq9dWcyl6xjjqlxWPwlExTFgBt user@host"
+system_user = "ssh-operator2"
+tag = "tag2"
+
+[users.system_nethsm_operator_signing.nethsm_key_setup]
+key_id = "key2"
+key_type = "Curve25519"
+key_mechanisms = ["EdDsaSignature"]
+signature_type = "EdDsa"
+
+[users.system_nethsm_operator_signing.nethsm_key_setup.key_context.openpgp]
+user_ids = ["Foobar McFooface <foobar@mcfooface.org>"]
+version = "4"
+
+[[users]]
+nethsm_only_admin = "ns1~admin"
+
+[[users]]
+
+[users.system_nethsm_operator_signing]
+nethsm_user = "ns1~operator1"
+ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILWqWyMCk5BdSl1c3KYoLEokKr7qNVPbI1IbBhgEBQj5 user@host"
+system_user = "ns1-ssh-operator1"
+tag = "tag1"
+
+[users.system_nethsm_operator_signing.nethsm_key_setup]
+key_id = "key1"
+key_type = "Curve25519"
+key_mechanisms = ["EdDsaSignature"]
+signature_type = "EdDsa"
+
+[users.system_nethsm_operator_signing.nethsm_key_setup.key_context.openpgp]
+user_ids = ["Foobar McFooface <foobar@mcfooface.org>"]
+version = "4"
+
+[[users]]
+
+[users.system_nethsm_operator_signing]
+nethsm_user = "ns1~operator2"
+ssh_authorized_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINrIYA+bfMBThUP5lKbMFEHiytmcCPhpkGrB/85n0mAN user@host"
+system_user = "ns1-ssh-operator2"
+tag = "tag2"
+
+[users.system_nethsm_operator_signing.nethsm_key_setup]
+key_id = "key2"
+key_type = "Curve25519"
+key_mechanisms = ["EdDsaSignature"]
+signature_type = "EdDsa"
+
+[users.system_nethsm_operator_signing.nethsm_key_setup.key_context.openpgp]
+user_ids = ["Foobar McFooface <foobar@mcfooface.org>"]
+version = "4"
+
+[[users]]
+
+[users.hermetic_system_nethsm_metrics]
+system_user = "local-metrics1"
+
+[users.hermetic_system_nethsm_metrics.nethsm_users]
+metrics_user = "metrics2"
+operator_users = ["operator2metrics1"]
+
+[[users]]
+
+[users.system_only_share_download]
+ssh_authorized_keys = ["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host"]
+system_user = "ssh-share-down"
+
+[[users]]
+
+[users.system_only_share_upload]
+ssh_authorized_keys = ["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOh96uFTnvX6P1ebbLxXFvy6sK7qFqlMHDOuJ0TmuXQQ user@host"]
+system_user = "ssh-share-up"
+
+[[users]]
+
+[users.system_only_wireguard_download]
+ssh_authorized_keys = ["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClIXZdx0aDOPcIQA+6Qx68cwSUgGTL3TWzDSX3qUEOQ user@host"]
+system_user = "ssh-wireguard-down"
diff --git a/signstar-config/tests/integration.rs b/signstar-config/tests/integration.rs
new file mode 100644
index 00000000..88fec84b
--- /dev/null
+++ b/signstar-config/tests/integration.rs
@@ -0,0 +1,12 @@
+//! Integration tests for signstar-config modules.
+#[cfg(feature = "_containerized-integration-test")]
+pub mod admin_credentials;
+
+#[cfg(feature = "_containerized-integration-test")]
+pub mod config;
+
+#[cfg(feature = "_containerized-integration-test")]
+pub mod non_admin_credentials;
+
+#[cfg(feature = "_containerized-integration-test")]
+mod utils;
diff --git a/signstar-config/tests/non_admin_credentials/mod.rs b/signstar-config/tests/non_admin_credentials/mod.rs
new file mode 100644
index 00000000..439e5eee
--- /dev/null
+++ b/signstar-config/tests/non_admin_credentials/mod.rs
@@ -0,0 +1,383 @@
+//! Integration tests for [`signstar_config::non_admin_credentials`].
+use std::{
+    fs::{File, Permissions, remove_file, set_permissions},
+    io::Write,
+    os::unix::fs::{PermissionsExt, chown},
+};
+
+use rstest::rstest;
+use signstar_common::{
+    common::{SECRET_FILE_MODE, get_data_home},
+    config::get_default_config_file_path,
+    system_user::{get_systemd_creds_secret_file, get_user_secrets_dir},
+};
+use signstar_config::{error::ErrorExitCode, non_admin_credentials::SecretsWriter};
+use testresult::TestResult;
+
+use crate::utils::{
+    SIGNSTAR_CONFIG_FULL,
+    SIGNSTAR_CONFIG_PLAINTEXT,
+    create_users,
+    list_files_in_dir,
+    prepare_system_with_config,
+    run_command_as_user,
+};
+
+const GET_CREDENTIALS_PAYLOAD: &str = "get-nethsm-credentials";
+
+/// Loading credentials for unprivileged system users succeeds.
+///
+/// Tests integration with `systemd-creds` encrypted secrets and plaintext secrets.
+#[rstest]
+#[case(SIGNSTAR_CONFIG_FULL)]
+#[case(SIGNSTAR_CONFIG_PLAINTEXT)]
+fn load_credentials_for_user(#[case] config_data: &[u8]) -> TestResult {
+    let (creds_mapping, _credentials_socket) = prepare_system_with_config(config_data)?;
+    // Get all system users
+    let system_users = creds_mapping
+        .iter()
+        .filter_map(|mapping| {
+            mapping
+                .get_user_mapping()
+                .get_system_user()
+                .map(|user| user.to_string())
+        })
+        .collect::<Vec<String>>();
+    // Create all system users and their homes
+    create_users(system_users.as_slice())?;
+    // Create secrets for each system user and their backend users
+    for mapping in &creds_mapping {
+        mapping.create_secrets_dir()?;
+        mapping.create_non_administrative_secrets()?;
+    }
+    // List all files and directories in the data home.
+    list_files_in_dir(get_data_home())?;
+
+    // Retrieve backend credentials for each system user
+    for mapping in &creds_mapping {
+        if let Some(system_user_id) = mapping.get_user_mapping().get_system_user() {
+            let (output, command_string) =
+                run_command_as_user(GET_CREDENTIALS_PAYLOAD, &[], system_user_id.as_ref())?;
+
+            if !output.status.success() {
+                return Err(signstar_config::Error::CommandNonZero {
+                    command: command_string,
+                    exit_status: output.status,
+                    stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
+                }
+                .into());
+            }
+        }
+    }
+
+    Ok(())
+}
+
+/// Loading credentials for unprivileged system users fails on missing Signstar configuration.
+#[rstest]
+fn load_credentials_for_user_fails_on_missing_signstar_config() -> TestResult {
+    let (creds_mapping, _credentials_socket) = prepare_system_with_config(SIGNSTAR_CONFIG_FULL)?;
+    // Get all system users
+    let system_users = creds_mapping
+        .iter()
+        .filter_map(|mapping| {
+            mapping
+                .get_user_mapping()
+                .get_system_user()
+                .map(|user| user.to_string())
+        })
+        .collect::<Vec<String>>();
+    // Create all system users and their homes
+    create_users(system_users.as_slice())?;
+    // Create secrets for each system user and their backend users
+    for mapping in &creds_mapping {
+        mapping.create_secrets_dir()?;
+        mapping.create_non_administrative_secrets()?;
+    }
+    // List all files and directories in the data home.
+    list_files_in_dir(get_data_home())?;
+
+    // Remove signstar config from default location
+    remove_file(get_default_config_file_path())?;
+
+    // Retrieve backend credentials for each system user
+    for mapping in &creds_mapping {
+        if let Some(system_user_id) = mapping.get_user_mapping().get_system_user() {
+            let (output, command_string) =
+                run_command_as_user(GET_CREDENTIALS_PAYLOAD, &[], system_user_id.as_ref())?;
+            if !output.status.success() {
+                let Some(exit_code) = output.status.code() else {
+                    panic!("There should be an exit code for {command_string}!")
+                };
+                assert_eq!(
+                    exit_code,
+                    std::convert::Into::<i32>::into(ErrorExitCode::ConfigConfigMissing)
+                );
+            } else {
+                panic!("The command {command_string} should have failed!")
+            }
+        }
+    }
+
+    Ok(())
+}
+
+/// Loading credentials for unprivileged system users fails on /run/systemd/io.systemd.Credentials
+/// socket not being available.
+#[rstest]
+fn load_credentials_for_user_fails_on_credentials_socket() -> TestResult {
+    let (creds_mapping, mut credentials_socket) = prepare_system_with_config(SIGNSTAR_CONFIG_FULL)?;
+    // Get all system users
+    let system_users = creds_mapping
+        .iter()
+        .filter_map(|mapping| {
+            mapping
+                .get_user_mapping()
+                .get_system_user()
+                .map(|user| user.to_string())
+        })
+        .collect::<Vec<String>>();
+    // Create all system users and their homes
+    create_users(system_users.as_slice())?;
+    // Create secrets for each system user and their backend users
+    for mapping in &creds_mapping {
+        mapping.create_secrets_dir()?;
+        mapping.create_non_administrative_secrets()?;
+    }
+    // List all files and directories in the data home.
+    list_files_in_dir(get_data_home())?;
+
+    // Kill socket /run/systemd/io.systemd.Credentials and `systemd-creds` process to not leak the
+    // subprocess
+    credentials_socket.kill()?;
+
+    // Retrieve backend credentials for each system user
+    for mapping in &creds_mapping {
+        if let Some(system_user_id) = mapping.get_user_mapping().get_system_user() {
+            let (output, command_string) =
+                run_command_as_user(GET_CREDENTIALS_PAYLOAD, &[], system_user_id.as_ref())?;
+
+            if !output.status.success() {
+                let Some(exit_code) = output.status.code() else {
+                    panic!("There should be an exit code for {command_string}!")
+                };
+                assert_eq!(
+                    exit_code,
+                    std::convert::Into::<i32>::into(
+                        ErrorExitCode::NonAdminCredentialsCredentialsLoading
+                    )
+                );
+            } else {
+                panic!("The command {command_string} should have failed!")
+            }
+        }
+    }
+
+    Ok(())
+}
+
+/// Loading credentials for unprivileged system users fails on missing secrets dir.
+#[rstest]
+fn load_credentials_for_user_fails_on_missing_secrets_dir() -> TestResult {
+    let (creds_mapping, _credentials_socket) = prepare_system_with_config(SIGNSTAR_CONFIG_FULL)?;
+    // Get all system users
+    let system_users = creds_mapping
+        .iter()
+        .filter_map(|mapping| {
+            mapping
+                .get_user_mapping()
+                .get_system_user()
+                .map(|user| user.to_string())
+        })
+        .collect::<Vec<String>>();
+    // Create all system users and their homes
+    create_users(system_users.as_slice())?;
+    // List all files and directories in the data home.
+    list_files_in_dir(get_data_home())?;
+
+    // Retrieve backend credentials for each system user
+    for mapping in &creds_mapping {
+        if let Some(system_user_id) = mapping.get_user_mapping().get_system_user() {
+            let (output, command_string) =
+                run_command_as_user(GET_CREDENTIALS_PAYLOAD, &[], system_user_id.as_ref())?;
+
+            if !output.status.success() {
+                let Some(exit_code) = output.status.code() else {
+                    panic!("There should be an exit code for {command_string}!")
+                };
+                assert_eq!(
+                    exit_code,
+                    std::convert::Into::<i32>::into(
+                        ErrorExitCode::NonAdminCredentialsCredentialsLoading
+                    )
+                );
+            } else {
+                panic!("The command {command_string} should have failed!")
+            }
+        }
+    }
+
+    Ok(())
+}
+
+/// Loading credentials for unprivileged system users fails on missing secrets file.
+#[rstest]
+fn load_credentials_for_user_fails_on_missing_secrets_file() -> TestResult {
+    let (creds_mapping, _credentials_socket) = prepare_system_with_config(SIGNSTAR_CONFIG_FULL)?;
+    // Get all system users
+    let system_users = creds_mapping
+        .iter()
+        .filter_map(|mapping| {
+            mapping
+                .get_user_mapping()
+                .get_system_user()
+                .map(|user| user.to_string())
+        })
+        .collect::<Vec<String>>();
+    // Create all system users and their homes
+    create_users(system_users.as_slice())?;
+    // Only create secret dir for each system user.
+    for mapping in &creds_mapping {
+        mapping.create_secrets_dir()?;
+    }
+    // List all files and directories in the data home.
+    list_files_in_dir(get_data_home())?;
+
+    // Retrieve backend credentials for each system user
+    for mapping in &creds_mapping {
+        if let Some(system_user_id) = mapping.get_user_mapping().get_system_user() {
+            let (output, command_string) =
+                run_command_as_user(GET_CREDENTIALS_PAYLOAD, &[], system_user_id.as_ref())?;
+
+            if !output.status.success() {
+                let Some(exit_code) = output.status.code() else {
+                    panic!("There should be an exit code for {command_string}!")
+                };
+                assert_eq!(
+                    exit_code,
+                    std::convert::Into::<i32>::into(
+                        ErrorExitCode::NonAdminCredentialsCredentialsLoading
+                    )
+                );
+            } else {
+                panic!("The command {command_string} should have failed!")
+            }
+        }
+    }
+
+    Ok(())
+}
+
+/// Loading credentials for unprivileged system users fails on inaccessible secrets file.
+#[rstest]
+fn load_credentials_for_user_fails_on_inaccessible_secrets_file() -> TestResult {
+    let (creds_mapping, _credentials_socket) = prepare_system_with_config(SIGNSTAR_CONFIG_FULL)?;
+    // Get all system users
+    let system_users = creds_mapping
+        .iter()
+        .filter_map(|mapping| {
+            mapping
+                .get_user_mapping()
+                .get_system_user()
+                .map(|user| user.to_string())
+        })
+        .collect::<Vec<String>>();
+    // Create all system users and their homes
+    create_users(system_users.as_slice())?;
+    // Create secrets file for each system user (and then render it inaccessible).
+    for mapping in &creds_mapping {
+        mapping.create_secrets_dir()?;
+        mapping.create_non_administrative_secrets()?;
+        if let Some(user) = mapping.get_user_mapping().get_system_user() {
+            let secrets_dir = get_user_secrets_dir(user.as_ref());
+            chown(secrets_dir.as_path(), Some(0), Some(0))?;
+            set_permissions(
+                secrets_dir.as_path(),
+                Permissions::from_mode(SECRET_FILE_MODE),
+            )?;
+        }
+    }
+    // List all files and directories in the data home.
+    list_files_in_dir(get_data_home())?;
+
+    // Retrieve backend credentials for each system user
+    for mapping in &creds_mapping {
+        if let Some(system_user_id) = mapping.get_user_mapping().get_system_user() {
+            let (output, command_string) =
+                run_command_as_user(GET_CREDENTIALS_PAYLOAD, &[], system_user_id.as_ref())?;
+
+            if !output.status.success() {
+                let Some(exit_code) = output.status.code() else {
+                    panic!("There should be an exit code for {command_string}!")
+                };
+                assert_eq!(
+                    exit_code,
+                    std::convert::Into::<i32>::into(
+                        ErrorExitCode::NonAdminCredentialsCredentialsLoading
+                    )
+                );
+            } else {
+                panic!("The command {command_string} should have failed!")
+            }
+        }
+    }
+
+    Ok(())
+}
+
+/// Loading credentials for unprivileged system users fails on garbage "encrypted" secrets file.
+#[rstest]
+fn load_credentials_for_user_fails_on_garbage_secrets_file() -> TestResult {
+    let (creds_mapping, _credentials_socket) = prepare_system_with_config(SIGNSTAR_CONFIG_FULL)?;
+    // Get all system users
+    let system_users = creds_mapping
+        .iter()
+        .filter_map(|mapping| {
+            mapping
+                .get_user_mapping()
+                .get_system_user()
+                .map(|user| user.to_string())
+        })
+        .collect::<Vec<String>>();
+    // Create all system users and their homes
+    create_users(system_users.as_slice())?;
+    // Create secrets file for each system user (and then write garbage to them).
+    for mapping in &creds_mapping {
+        mapping.create_secrets_dir()?;
+        mapping.create_non_administrative_secrets()?;
+        if let Some(user) = mapping.get_user_mapping().get_system_user() {
+            for backend_user in mapping.get_user_mapping().get_nethsm_users() {
+                let secrets_file =
+                    get_systemd_creds_secret_file(user.as_ref(), &backend_user.to_string());
+                let mut output = File::create(secrets_file.as_path())?;
+                write!(output, "GARBAGE")?;
+            }
+        }
+    }
+    // List all files and directories in the data home.
+    list_files_in_dir(get_data_home())?;
+
+    // Retrieve backend credentials for each system user
+    for mapping in &creds_mapping {
+        if let Some(system_user_id) = mapping.get_user_mapping().get_system_user() {
+            let (output, command_string) =
+                run_command_as_user(GET_CREDENTIALS_PAYLOAD, &[], system_user_id.as_ref())?;
+
+            if !output.status.success() {
+                let Some(exit_code) = output.status.code() else {
+                    panic!("There should be an exit code for {command_string}!")
+                };
+                assert_eq!(
+                    exit_code,
+                    std::convert::Into::<i32>::into(
+                        ErrorExitCode::NonAdminCredentialsCredentialsLoading
+                    )
+                );
+            } else {
+                panic!("The command {command_string} should have failed!")
+            }
+        }
+    }
+
+    Ok(())
+}
diff --git a/signstar-config/tests/utils/mod.rs b/signstar-config/tests/utils/mod.rs
new file mode 100644
index 00000000..b8bf4cc2
--- /dev/null
+++ b/signstar-config/tests/utils/mod.rs
@@ -0,0 +1,424 @@
+//! Utilities used for test setups.
+use std::{
+    fs::{Permissions, create_dir_all, read_dir, read_to_string, set_permissions, write},
+    os::{linux::fs::MetadataExt, unix::fs::PermissionsExt},
+    path::{Path, PathBuf},
+    process::{Child, Command, Output},
+    thread,
+    time,
+};
+
+use nethsm_config::{
+    ConfigInteractivity,
+    ConfigSettings,
+    ExtendedUserMapping,
+    HermeticParallelConfig,
+};
+use signstar_common::{config::get_default_config_file_path, system_user::get_home_base_dir_path};
+use tempfile::NamedTempFile;
+use testresult::TestResult;
+use which::which;
+
+pub const SIGNSTAR_CONFIG_FULL: &[u8] = include_bytes!("../fixtures/signstar-config-full.toml");
+pub const SIGNSTAR_CONFIG_PLAINTEXT: &[u8] =
+    include_bytes!("../fixtures/signstar-config-plaintext.toml");
+pub const SIGNSTAR_ADMIN_CREDS_SIMPLE: &[u8] =
+    include_bytes!("../fixtures/admin-creds-simple.toml");
+
+/// An error that may occur when using test utils.
+#[derive(Debug, thiserror::Error)]
+pub enum Error {
+    /// Applying permissions to a file failed.
+    #[error("Unable to apply permissions to {path}:\n{source}")]
+    ApplyPermissions {
+        path: PathBuf,
+        source: std::io::Error,
+    },
+
+    /// A directory can not be created.
+    #[error("Unable to create directory {dir}:\n{source}")]
+    CreateDirectory {
+        dir: PathBuf,
+        source: std::io::Error,
+    },
+
+    /// The socket for io.systemd.Credentials could not be started.
+    #[error("Unable to start socket for io.systemd.Credentials:\n{0}")]
+    CredentialsSocket(#[source] std::io::Error),
+
+    /// An I/O error.
+    #[error("I/O error while {context}:\n{source}")]
+    Io {
+        context: &'static str,
+        source: std::io::Error,
+    },
+
+    /// An I/O error with a specific path.
+    #[error("I/O error at {path} while {context}:\n{source}")]
+    IoPath {
+        path: PathBuf,
+        context: &'static str,
+        source: std::io::Error,
+    },
+
+    /// A signstar-config error.
+    #[error("Signstar-config error:\n{0}")]
+    SignstarConfig(#[from] signstar_config::Error),
+
+    /// A timeout has been reached.
+    #[error("Timeout of {timeout}ms reached while {context}")]
+    Timeout { timeout: u64, context: String },
+
+    /// A temporary file cannot be created.
+    #[error("A temporary file for {purpose} cannot be created:\n{source}")]
+    Tmpfile {
+        purpose: &'static str,
+        source: std::io::Error,
+    },
+}
+
+/// Recursively lists files, their permissions and ownership.
+pub fn list_files_in_dir(path: impl AsRef<Path>) -> Result<(), Error> {
+    let path = path.as_ref();
+    let entries = read_dir(path).map_err(|source| Error::IoPath {
+        path: path.to_path_buf(),
+        context: "reading its children",
+        source,
+    })?;
+
+    for entry in entries {
+        let entry = entry.map_err(|source| Error::IoPath {
+            path: path.to_path_buf(),
+            context: "getting an entry below it",
+            source,
+        })?;
+        let meta = entry.metadata().map_err(|source| Error::IoPath {
+            path: path.to_path_buf(),
+            context: "getting metadata",
+            source,
+        })?;
+
+        println!(
+            "{} {}/{} {entry:?}",
+            meta.permissions().mode(),
+            meta.st_uid(),
+            meta.st_gid()
+        );
+
+        if meta.is_dir() {
+            list_files_in_dir(entry.path())?;
+        }
+    }
+
+    Ok(())
+}
+
+/// Returns a configuration file with `data` as contents in a temporary location.
+pub fn get_tmp_config(data: &[u8]) -> Result<NamedTempFile, Error> {
+    let tmp_config = NamedTempFile::new().map_err(|source| Error::Tmpfile {
+        purpose: "full signstar configuration",
+        source,
+    })?;
+    write(&tmp_config, data).map_err(|source| Error::Io {
+        context: "writing full signstar configuration to temporary file",
+        source,
+    })?;
+    Ok(tmp_config)
+}
+
+/// Writes a dummy /etc/machine-id, which is required for systemd-creds.
+pub fn write_machine_id() -> Result<(), Error> {
+    println!("Write dummy /etc/machine-id, required for systemd-creds");
+    let machine_id = PathBuf::from("/etc/machine-id");
+    std::fs::write(&machine_id, "d3b07384d113edec49eaa6238ad5ff00").map_err(|source| {
+        Error::IoPath {
+            path: machine_id.to_path_buf(),
+            context: "writing machine-id",
+            source,
+        }
+    })?;
+
+    let metadata = machine_id.metadata().map_err(|source| Error::IoPath {
+        path: machine_id,
+        context: "getting metadata of file",
+        source,
+    })?;
+    println!(
+        "/etc/machine-id\nmode: {}\nuid: {}\ngid: {}",
+        metadata.permissions().mode(),
+        metadata.st_uid(),
+        metadata.st_gid()
+    );
+    Ok(())
+}
+
+/// A background process.
+///
+/// Tracks a [`Child`] which represents a process that runs in the background.
+/// The background process is automatically killed upon dropping the [`BackgroundProcess`].
+pub struct BackgroundProcess {
+    child: Child,
+    command: String,
+}
+
+impl BackgroundProcess {
+    /// Kills the tracked background process.
+    ///
+    /// # Errors
+    ///
+    /// Returns an error if the process could not be killed.
+    pub fn kill(&mut self) -> Result<(), Error> {
+        self.child.kill().map_err(|source| Error::Io {
+            context: "killing process",
+            source,
+        })
+    }
+}
+
+impl Drop for BackgroundProcess {
+    /// Kills the tracked background process when destructing the [`BackgroundProcess`].
+    fn drop(&mut self) {
+        if let Err(error) = self.child.kill() {
+            eprintln!(
+                "Unable to kill background process of command {}:\n{error}",
+                self.command
+            )
+        }
+    }
+}
+
+/// Starts a socket for `io.systemd.Credentials` using `systemd-socket-activate`.
+///
+/// Sets the file mode of the socket to `666` so that all users on the system have access.
+///
+/// # Errors
+///
+/// Returns an error if
+///
+/// - `systemd-socket-activate` is unable to start the required socket,
+/// - one or more files in `/run/systemd` can not be listed,
+/// - applying of permissions on `/run/systemd/io.systemd.Credentials` fails,
+/// - or the socket has not been made available within 10000ms.
+pub fn start_credentials_socket() -> Result<BackgroundProcess, Error> {
+    let systemd_run_path = PathBuf::from("/run/systemd");
+    let socket_path = PathBuf::from("/run/systemd/io.systemd.Credentials");
+    create_dir_all(&systemd_run_path).map_err(|source| Error::CreateDirectory {
+        dir: systemd_run_path,
+        source,
+    })?;
+
+    // Run systemd-socket-activate to provide /run/systemd/io.systemd.Credentials
+    let command = "systemd-socket-activate";
+    let systemd_socket_activate = which(command).map_err(|source| {
+        Error::SignstarConfig(
+            signstar_config::utils::Error::ExecutableNotFound {
+                command: command.to_string(),
+                source,
+            }
+            .into(),
+        )
+    })?;
+    let mut command = Command::new(systemd_socket_activate);
+    let command = command.args([
+        "--listen",
+        "/run/systemd/io.systemd.Credentials",
+        "--accept",
+        "--fdname=varlink",
+        "systemd-creds",
+    ]);
+    let child = command.spawn().map_err(Error::CredentialsSocket)?;
+
+    // Set the socket to be writable by all, once it's available.
+    let timeout = 10000;
+    let step = 100;
+    let mut elapsed = 0;
+    let mut permissions_set = false;
+    while elapsed < timeout {
+        if socket_path.exists() {
+            println!("Found {socket_path:?}");
+            set_permissions(socket_path.as_path(), Permissions::from_mode(0o666)).map_err(
+                |source| Error::ApplyPermissions {
+                    path: socket_path.to_path_buf(),
+                    source,
+                },
+            )?;
+            permissions_set = true;
+            break;
+        } else {
+            thread::sleep(time::Duration::from_millis(step));
+            elapsed += step;
+        }
+    }
+    if !permissions_set {
+        return Err(Error::Timeout {
+            timeout,
+            context: format!("waiting for {socket_path:?}"),
+        });
+    }
+
+    Ok(BackgroundProcess {
+        child,
+        command: format!("{command:?}"),
+    })
+}
+
+/// Runs a `command` as a specific `user` and returns its [`Output`] and the command's [`String`]
+/// representation.
+///
+/// Uses `su` to run the the command as a specific user.
+pub fn run_command_as_user(
+    user_command: &str,
+    user_command_args: &[&str],
+    user: &str,
+) -> Result<(Output, String), Error> {
+    /// Returns the path to a `command`.
+    ///
+    /// # Errors
+    ///
+    /// Returns an error if `command` can not be found in PATH.
+    fn get_command(command: &str) -> Result<PathBuf, Error> {
+        which(command).map_err(|source| {
+            Error::SignstarConfig(signstar_config::Error::Utils(
+                signstar_config::utils::Error::ExecutableNotFound {
+                    command: command.to_string(),
+                    source,
+                },
+            ))
+        })
+    }
+
+    let priv_command = get_command("runuser")?;
+    eprintln!("Checking availability of command {user_command}");
+    get_command(user_command)?;
+
+    // Run command as user
+    let mut command = Command::new(priv_command);
+    let command = command
+        .arg(format!(
+            "--command='{user_command}{}'",
+            if !user_command_args.is_empty() {
+                format!(" {}", user_command_args.join(" "))
+            } else {
+                "".to_string()
+            }
+        ))
+        .arg("--group")
+        .arg(user)
+        .arg("--login")
+        .arg(user);
+
+    let command_string = format!("{command:?}");
+    eprintln!("Running command {command_string}");
+    let command_output = command.output().map_err(|source| {
+        Error::SignstarConfig(signstar_config::Error::CommandExec {
+            command: command_string.clone(),
+            source,
+        })
+    })?;
+    eprintln!(
+        "stdout:\n{}",
+        String::from_utf8_lossy(&command_output.stdout).into_owned()
+    );
+    eprintln!(
+        "stderr:\n{}",
+        String::from_utf8_lossy(&command_output.stderr).into_owned()
+    );
+
+    Ok((command_output, command_string))
+}
+
+/// Creates a set of users.
+pub fn create_users(users: &[String]) -> TestResult {
+    println!("Creating users: {:?}", users);
+    for user in users {
+        println!("Creating user: {}", user);
+
+        // create the user and its home
+        let mut command = Command::new("useradd");
+        let command = command
+            .arg("--base-dir")
+            .arg(get_home_base_dir_path())
+            .arg("--create-home")
+            .arg("--user-group")
+            .arg("--shell")
+            .arg("/usr/bin/bash")
+            .arg(user);
+
+        let command_output = command.output()?;
+        if !command_output.status.success() {
+            return Err(signstar_config::Error::CommandNonZero {
+                command: format!("{command:?}"),
+                exit_status: command_output.status,
+                stderr: String::from_utf8_lossy(&command_output.stderr).into_owned(),
+            }
+            .into());
+        }
+
+        // unlock the user
+        let mut command = Command::new("usermod");
+        command.arg("--unlock");
+        command.arg(user);
+        let command_output = command.output()?;
+        if !command_output.status.success() {
+            return Err(signstar_config::Error::CommandNonZero {
+                command: format!("{command:?}"),
+                exit_status: command_output.status,
+                stderr: String::from_utf8_lossy(&command_output.stderr).into_owned(),
+            }
+            .into());
+        }
+    }
+
+    println!("/etc/passwd:\n{}", read_to_string("/etc/passwd")?);
+
+    Ok(())
+}
+
+/// Prepares a system for use with Signstar.
+///
+/// Prepares the following:
+///
+/// - Creates `/etc/machine-id`, which is needed for `systemd-creds` to function.
+/// - Reads Signstar configuration from data and writes to default config location.
+/// - Creates `/run/systemd/io.systemd.Credentials` by running `systemd-socket-activate` in the
+///   background
+///
+/// Returns the list of [`ExtendedUserMapping`]s derived from the Signstar configuration and the
+/// [`Child`] of the `systemd-socket-activate` process that created
+/// `/run/systemd/io.systemd.Credentials`.
+pub fn prepare_system_with_config(
+    config_data: &[u8],
+) -> Result<(Vec<ExtendedUserMapping>, BackgroundProcess), Error> {
+    write_machine_id()?;
+
+    // Read Signstar config from `config_data`
+    let config = HermeticParallelConfig::new_from_file(
+        ConfigSettings::new(
+            "my_app".to_string(),
+            ConfigInteractivity::NonInteractive,
+            None,
+        ),
+        Some(get_tmp_config(config_data)?.path()),
+    )
+    .map_err(|source| {
+        Error::SignstarConfig(signstar_config::Error::Config(
+            signstar_config::config::Error::NetHsmConfig(source),
+        ))
+    })?;
+
+    // Store Signstar config in default location
+    config
+        .store(Some(&get_default_config_file_path()))
+        .map_err(|source| {
+            Error::SignstarConfig(signstar_config::Error::Config(
+                signstar_config::config::Error::NetHsmConfig(source),
+            ))
+        })?;
+
+    // Get extended user mappings for all users.
+    let creds_mapping: Vec<ExtendedUserMapping> = config.into();
+
+    // Return extended user mappings contained in Signstar config and the background process
+    // providing /run/systemd/io.systemd.Credentials
+    Ok((creds_mapping, start_credentials_socket()?))
+}
-- 
GitLab


From c33fe4c99e0d8d602d05257346b844a3b67e100d Mon Sep 17 00:00:00 2001
From: David Runge <dvzrv@archlinux.org>
Date: Mon, 17 Mar 2025 15:39:18 +0100
Subject: [PATCH 6/6] ci(GitLab): Add integration test target for containerized
 tests

Signed-off-by: David Runge <dvzrv@archlinux.org>
---
 .gitlab-ci.yml | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index b29e828f..feedd393 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -104,6 +104,17 @@ integration-test:
   tags:
     - vm
 
+containerized-integration-test:
+  extends: .default
+  script:
+    - useradd -m testuser
+    - loginctl enable-linger testuser
+    - chown -R testuser:testuser .
+    - WORKDIR="$(pwd)" runuser -w WORKDIR -l testuser -c 'env && cd "$WORKDIR" && just install-rust-dev-tools && just build-container-integration-test-image && just containerized-integration-tests'
+  stage: test
+  tags:
+    - vm
+
 test-readmes:
   extends: .default
   script:
-- 
GitLab