diff --git a/Cargo.lock b/Cargo.lock
index be2ca2cd12d..3b8b2d79396 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1110,8 +1110,18 @@ version = "0.13.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c"
 dependencies = [
- "darling_core",
- "darling_macro",
+ "darling_core 0.13.4",
+ "darling_macro 0.13.4",
+]
+
+[[package]]
+name = "darling"
+version = "0.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0558d22a7b463ed0241e993f76f09f30b126687447751a8638587b864e4b3944"
+dependencies = [
+ "darling_core 0.20.1",
+ "darling_macro 0.20.1",
 ]
 
 [[package]]
@@ -1128,17 +1138,42 @@ dependencies = [
  "syn 1.0.109",
 ]
 
+[[package]]
+name = "darling_core"
+version = "0.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab8bfa2e259f8ee1ce5e97824a3c55ec4404a0d772ca7fa96bf19f0752a046eb"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2 1.0.56",
+ "quote 1.0.26",
+ "strsim",
+ "syn 2.0.15",
+]
+
 [[package]]
 name = "darling_macro"
 version = "0.13.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835"
 dependencies = [
- "darling_core",
+ "darling_core 0.13.4",
  "quote 1.0.26",
  "syn 1.0.109",
 ]
 
+[[package]]
+name = "darling_macro"
+version = "0.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29a358ff9f12ec09c3e61fef9b5a9902623a695a46a917b07f269bff1445611a"
+dependencies = [
+ "darling_core 0.20.1",
+ "quote 1.0.26",
+ "syn 2.0.15",
+]
+
 [[package]]
 name = "data-encoding"
 version = "2.3.3"
@@ -1215,9 +1250,9 @@ checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
 
 [[package]]
 name = "doku"
-version = "0.20.0"
+version = "0.21.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "966b1227ac4d9d77f4d7e9507dd01c56ceaec6e35888661b54319123da47b159"
+checksum = "b738a9a7942e6996e6df51b8a58db17105fe5a448112add828fc9ff93fcd6c8e"
 dependencies = [
  "doku-derive",
  "serde",
@@ -1226,11 +1261,11 @@ dependencies = [
 
 [[package]]
 name = "doku-derive"
-version = "0.20.0"
+version = "0.21.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "252ec56116f931b050b5d80512c2c76f4807a297dd95a93f37593dd7650868a5"
+checksum = "4dd9ccf9dd3a20ad0a08c3a099693490eb243be3287330d77bd3c9c06225babb"
 dependencies = [
- "darling",
+ "darling 0.13.4",
  "proc-macro2 1.0.56",
  "quote 1.0.26",
  "syn 1.0.109",
@@ -2460,6 +2495,15 @@ version = "3.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f"
 
+[[package]]
+name = "pad"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2ad9b889f1b12e0b9ee24db044b5129150d5eada288edc800f789928dc8c0e3"
+dependencies = [
+ "unicode-width",
+]
+
 [[package]]
 name = "parking_lot"
 version = "0.12.1"
@@ -3604,13 +3648,16 @@ dependencies = [
  "anyhow",
  "assert_cmd",
  "assert_matches",
+ "atty",
  "base64 0.13.1",
  "camino",
  "certificate",
  "clap 3.2.25",
+ "doku",
  "hyper",
  "mockito",
  "mqtt_tests",
+ "pad",
  "pem",
  "predicates 2.1.5",
  "reqwest",
@@ -3629,6 +3676,7 @@ dependencies = [
  "tracing",
  "url",
  "which",
+ "yansi",
 ]
 
 [[package]]
@@ -3788,9 +3836,11 @@ dependencies = [
  "doku",
  "figment",
  "mqtt_channel",
+ "once_cell",
  "serde",
  "serde_ignored",
  "strum_macros",
+ "tedge_config_macros",
  "tedge_test_utils",
  "tedge_utils",
  "tempfile",
@@ -3802,6 +3852,41 @@ dependencies = [
  "url",
 ]
 
+[[package]]
+name = "tedge_config_macros"
+version = "0.1.0"
+dependencies = [
+ "camino",
+ "doku",
+ "once_cell",
+ "serde",
+ "tedge_config_macros-macro",
+ "thiserror",
+ "tracing",
+ "url",
+]
+
+[[package]]
+name = "tedge_config_macros-impl"
+version = "0.1.0"
+dependencies = [
+ "darling 0.20.1",
+ "heck",
+ "proc-macro2 1.0.56",
+ "quote 1.0.26",
+ "syn 2.0.15",
+]
+
+[[package]]
+name = "tedge_config_macros-macro"
+version = "0.1.0"
+dependencies = [
+ "proc-macro2 1.0.56",
+ "quote 1.0.26",
+ "syn 1.0.109",
+ "tedge_config_macros-impl",
+]
+
 [[package]]
 name = "tedge_downloader_ext"
 version = "0.10.0"
diff --git a/ci/configure_bridge.sh b/ci/configure_bridge.sh
index 628110f3295..9fd06f178f4 100755
--- a/ci/configure_bridge.sh
+++ b/ci/configure_bridge.sh
@@ -31,11 +31,11 @@ sudo tedge cert show
 
 sudo tedge config set c8y.url "$URL"
 
-sudo tedge config set c8y.root.cert.path /etc/ssl/certs
+sudo tedge config set c8y.root_cert_path /etc/ssl/certs
 
 sudo tedge config set az.url "$IOTHUBNAME.azure-devices.net"
 
-sudo tedge config set az.root.cert.path /etc/ssl/certs/Baltimore_CyberTrust_Root.pem
+sudo tedge config set az.root_cert_path /etc/ssl/certs/Baltimore_CyberTrust_Root.pem
 
 sudo tedge config list
 
diff --git a/crates/common/tedge_config/Cargo.toml b/crates/common/tedge_config/Cargo.toml
index 05cd0948210..6074621ac57 100644
--- a/crates/common/tedge_config/Cargo.toml
+++ b/crates/common/tedge_config/Cargo.toml
@@ -11,12 +11,14 @@ repository = { workspace = true }
 [dependencies]
 camino = { version = "1.1.4", features = ["serde", "serde1"] }
 certificate = { path = "../certificate" }
-doku = "0.20.0"
+doku = "0.21"
 figment = { version = "0.10", features = ["env", "toml"] }
 mqtt_channel = { path = "../mqtt_channel" }
+once_cell = "1.17"
 serde = { version = "1.0", features = ["derive"] }
 serde_ignored = "0.1"
 strum_macros = { version = "0.24" }
+tedge_config_macros = { path = "../tedge_config_macros" }
 tedge_utils = { path = "../tedge_utils" }
 thiserror = "1.0"
 toml = "0.7"
diff --git a/crates/common/tedge_config/src/lib.rs b/crates/common/tedge_config/src/lib.rs
index fac817e454f..63c6c152871 100644
--- a/crates/common/tedge_config/src/lib.rs
+++ b/crates/common/tedge_config/src/lib.rs
@@ -5,10 +5,10 @@ pub mod tedge_config_cli;
 pub use self::tedge_config_cli::config_setting::*;
 pub use self::tedge_config_cli::error::*;
 pub use self::tedge_config_cli::models::*;
+pub use self::tedge_config_cli::new;
 pub use self::tedge_config_cli::settings::*;
 pub use self::tedge_config_cli::tedge_config::*;
 pub use self::tedge_config_cli::tedge_config_defaults::*;
-use self::tedge_config_cli::tedge_config_dto::*;
 pub use self::tedge_config_cli::tedge_config_location::*;
 pub use self::tedge_config_cli::tedge_config_repository::*;
 pub use camino::Utf8Path as Path;
diff --git a/crates/common/tedge_config/src/tedge_config_cli/config_setting.rs b/crates/common/tedge_config/src/tedge_config_cli/config_setting.rs
index a62357e8676..d8e493930a4 100644
--- a/crates/common/tedge_config/src/tedge_config_cli/config_setting.rs
+++ b/crates/common/tedge_config/src/tedge_config_cli/config_setting.rs
@@ -75,4 +75,7 @@ pub enum ConfigSettingError {
 
     #[error("An error occurred: {msg}")]
     Other { msg: &'static str },
+
+    #[error(transparent)]
+    Write(#[from] crate::new::WriteError),
 }
diff --git a/crates/common/tedge_config/src/tedge_config_cli/figment.rs b/crates/common/tedge_config/src/tedge_config_cli/figment.rs
index 7b90bc222b4..3ef6d256930 100644
--- a/crates/common/tedge_config/src/tedge_config_cli/figment.rs
+++ b/crates/common/tedge_config/src/tedge_config_cli/figment.rs
@@ -1,12 +1,16 @@
 use std::borrow::Cow;
+use std::collections::HashSet;
 use std::fmt::Display;
 use std::path::Path;
 use std::path::PathBuf;
+use std::sync::Mutex;
 
 use figment::providers::Format;
 use figment::providers::Toml;
+use figment::value::Uncased;
 use figment::Figment;
 use figment::Metadata;
+use once_cell::sync::Lazy;
 use serde::de::DeserializeOwned;
 
 use crate::TEdgeConfigError;
@@ -28,10 +32,22 @@ impl ConfigSources for FileOnly {
     const INCLUDE_ENVIRONMENT: bool = false;
 }
 
+#[derive(Default, Debug, PartialEq, Eq)]
+#[must_use]
+pub struct UnusedValueWarnings(Vec<String>);
+
+impl UnusedValueWarnings {
+    pub fn emit(self) {
+        for warning in self.0 {
+            tracing::warn!("{warning}");
+        }
+    }
+}
+
 /// Extract the configuration data from the provided TOML path and `TEDGE_` prefixed environment variables
 pub fn extract_data<T: DeserializeOwned, Sources: ConfigSources>(
     path: impl AsRef<Path>,
-) -> Result<T, TEdgeConfigError> {
+) -> Result<(T, UnusedValueWarnings), TEdgeConfigError> {
     let env = TEdgeEnv::default();
     let figment = Figment::new().merge(Toml::file(path));
 
@@ -43,14 +59,18 @@ pub fn extract_data<T: DeserializeOwned, Sources: ConfigSources>(
 
     let data = extract_exact(&figment, &env);
 
-    for warning in unused_value_warnings::<T>(&figment, &env)
+    let warnings = unused_value_warnings::<T>(&figment, &env)
         .ok()
-        .unwrap_or_default()
-    {
-        tracing::warn!("{warning}");
+        .map(UnusedValueWarnings)
+        .unwrap_or_default();
+
+    match data {
+        Ok(data) => Ok((data, warnings)),
+        Err(e) => {
+            warnings.emit();
+            Err(e)
+        }
     }
-
-    data
 }
 
 fn unused_value_warnings<T: DeserializeOwned>(
@@ -172,9 +192,27 @@ impl TEdgeEnv {
     }
 
     fn provider(&self) -> figment::providers::Env {
-        let pattern = self.separator;
-        figment::providers::Env::prefixed(self.prefix)
-            .map(move |name| name.as_str().replacen(pattern, ".", 1).into())
+        static WARNINGS: Lazy<Mutex<HashSet<String>>> = Lazy::new(<_>::default);
+        figment::providers::Env::prefixed(self.prefix).map(move |name| {
+            let lowercase_name = name.as_str().to_ascii_lowercase();
+            Uncased::new(
+                tracing::subscriber::with_default(
+                    tracing::subscriber::NoSubscriber::default(),
+                    || lowercase_name.parse::<crate::new::WritableKey>(),
+                )
+                .map(|key| key.as_str().to_owned())
+                .map_err(|err| {
+                    let is_read_only_key = matches!(err, crate::new::ParseKeyError::ReadOnly(_));
+                    if is_read_only_key && !WARNINGS.lock().unwrap().insert(lowercase_name.clone()) {
+                            tracing::error!(
+                                "Failed to configure tedge with environment variable `TEDGE_{name}`: {}",
+                                err.to_string().replace('\n', " ")
+                            )
+         }
+                })
+                .unwrap_or(lowercase_name),
+            )
+        })
     }
 }
 
@@ -212,6 +250,7 @@ mod tests {
             assert_eq!(
                 extract_data::<Config, FileAndEnvironment>(&PathBuf::from("tedge.toml"))
                     .unwrap()
+                    .0
                     .c8y
                     .url,
                 "override.c8y.io"
@@ -298,7 +337,7 @@ mod tests {
             jail.set_env(variable_name, "environment");
 
             let data = extract_data::<Config, FileOnly>("tedge.toml").unwrap();
-            assert_eq!(data.value, "config");
+            assert_eq!(data.0.value, "config");
             Ok(())
         })
     }
diff --git a/crates/common/tedge_config/src/tedge_config_cli/mod.rs b/crates/common/tedge_config/src/tedge_config_cli/mod.rs
index 4cfd7de9af5..4e9ad211a1b 100644
--- a/crates/common/tedge_config/src/tedge_config_cli/mod.rs
+++ b/crates/common/tedge_config/src/tedge_config_cli/mod.rs
@@ -3,9 +3,9 @@ pub mod error;
 pub mod settings;
 pub mod tedge_config;
 pub mod tedge_config_defaults;
-pub mod tedge_config_dto;
 pub mod tedge_config_location;
 pub mod tedge_config_repository;
 
 mod figment;
 pub mod models;
+pub mod new;
diff --git a/crates/common/tedge_config/src/tedge_config_cli/models/connect_url.rs b/crates/common/tedge_config/src/tedge_config_cli/models/connect_url.rs
index 8b1f80313b9..aa50ecd4208 100644
--- a/crates/common/tedge_config/src/tedge_config_cli/models/connect_url.rs
+++ b/crates/common/tedge_config/src/tedge_config_cli/models/connect_url.rs
@@ -1,4 +1,6 @@
 use std::convert::TryFrom;
+use std::fmt;
+use std::str::FromStr;
 use url::Host;
 
 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Eq, PartialEq)]
@@ -29,6 +31,26 @@ impl TryFrom<String> for ConnectUrl {
     }
 }
 
+impl FromStr for ConnectUrl {
+    type Err = InvalidConnectUrl;
+
+    fn from_str(input: &str) -> Result<Self, Self::Err> {
+        ConnectUrl::try_from(input.to_string())
+    }
+}
+
+impl fmt::Display for ConnectUrl {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        self.input.fmt(f)
+    }
+}
+
+impl doku::Document for ConnectUrl {
+    fn ty() -> doku::Type {
+        String::ty()
+    }
+}
+
 impl TryFrom<&str> for ConnectUrl {
     type Error = InvalidConnectUrl;
 
diff --git a/crates/common/tedge_config/src/tedge_config_cli/models/host_port.rs b/crates/common/tedge_config/src/tedge_config_cli/models/host_port.rs
index d35ba7b0587..57e98d05c13 100644
--- a/crates/common/tedge_config/src/tedge_config_cli/models/host_port.rs
+++ b/crates/common/tedge_config/src/tedge_config_cli/models/host_port.rs
@@ -2,7 +2,9 @@ use crate::ConnectUrl;
 use crate::Port;
 use serde::Deserialize;
 use serde::Serialize;
+use std::fmt;
 use std::num::ParseIntError;
+use std::str::FromStr;
 use url::Host;
 
 /// A combination of a host and a port number.
@@ -72,21 +74,41 @@ impl<const P: u16> From<HostPort<P>> for String {
     }
 }
 
+impl<const P: u16> fmt::Display for HostPort<P> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        self.input.fmt(f)
+    }
+}
+
+impl<const P: u16> FromStr for HostPort<P> {
+    type Err = ParseHostPortError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        Self::try_from(s.to_owned())
+    }
+}
+
+impl<const P: u16> doku::Document for HostPort<P> {
+    fn ty() -> doku::Type {
+        String::ty()
+    }
+}
+
 impl<const P: u16> TryFrom<String> for HostPort<P> {
     type Error = ParseHostPortError;
 
-    fn try_from(s: String) -> Result<Self, Self::Error> {
-        let (hostname, port) = if let Some((hostname, port)) = s.split_once(':') {
+    fn try_from(input: String) -> Result<Self, Self::Error> {
+        let (hostname, port) = if let Some((hostname, port)) = input.split_once(':') {
             let port = Port(port.parse()?);
             let hostname: Host<String> = Host::parse(hostname)?;
             (hostname, port)
         } else {
-            let hostname: Host<String> = Host::parse(&s)?;
+            let hostname: Host<String> = Host::parse(&input)?;
             (hostname, Port(P))
         };
 
         Ok(HostPort {
-            input: s,
+            input,
             hostname,
             port,
         })
diff --git a/crates/common/tedge_config/src/tedge_config_cli/models/seconds.rs b/crates/common/tedge_config/src/tedge_config_cli/models/seconds.rs
index 354aa519512..ee27412341b 100644
--- a/crates/common/tedge_config/src/tedge_config_cli/models/seconds.rs
+++ b/crates/common/tedge_config/src/tedge_config_cli/models/seconds.rs
@@ -1,8 +1,14 @@
 use std::convert::TryFrom;
 use std::convert::TryInto;
+use std::fmt;
+use std::str::FromStr;
+use std::time::Duration;
 
-#[derive(Debug, Copy, Clone, Eq, PartialEq)]
-pub struct Seconds(pub u64);
+#[derive(
+    Copy, Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq, doku::Document,
+)]
+#[serde(transparent)]
+pub struct Seconds(pub(crate) u64);
 
 #[derive(thiserror::Error, Debug)]
 #[error("Invalid seconds number: '{input}'.")]
@@ -30,12 +36,38 @@ impl TryInto<String> for Seconds {
     }
 }
 
+impl FromStr for Seconds {
+    type Err = <u64 as FromStr>::Err;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        u64::from_str(s).map(Self)
+    }
+}
+
+impl fmt::Display for Seconds {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        self.0.fmt(f)
+    }
+}
+
+impl Seconds {
+    pub fn duration(self) -> Duration {
+        Duration::from_secs(self.0)
+    }
+}
+
 impl From<Seconds> for u64 {
     fn from(val: Seconds) -> Self {
         val.0
     }
 }
 
+impl From<u64> for Seconds {
+    fn from(value: u64) -> Self {
+        Self(value)
+    }
+}
+
 #[cfg(test)]
 use assert_matches::*;
 #[test]
diff --git a/crates/common/tedge_config/src/tedge_config_cli/models/templates_set.rs b/crates/common/tedge_config/src/tedge_config_cli/models/templates_set.rs
index 92b69e589e1..2862de50007 100644
--- a/crates/common/tedge_config/src/tedge_config_cli/models/templates_set.rs
+++ b/crates/common/tedge_config/src/tedge_config_cli/models/templates_set.rs
@@ -1,4 +1,6 @@
+use std::convert::Infallible;
 use std::convert::TryInto;
+use std::str::FromStr;
 
 /// Represents a set of smartrest templates.
 ///
@@ -7,6 +9,12 @@ use std::convert::TryInto;
 #[serde(from = "FromTomlOrCli")]
 pub struct TemplatesSet(pub Vec<String>);
 
+impl doku::Document for TemplatesSet {
+    fn ty() -> doku::Type {
+        Vec::<String>::ty()
+    }
+}
+
 #[derive(serde::Deserialize)]
 #[serde(from = "String")]
 struct CommaDelimited(Vec<String>);
@@ -39,12 +47,8 @@ impl From<FromTomlOrCli> for TemplatesSet {
     }
 }
 
-#[derive(thiserror::Error, Debug)]
-#[error("TemplateSet to String conversion failed: {0:?}")]
-pub struct TemplatesSetToStringConversionFailure(String);
-
 impl TryFrom<Vec<String>> for TemplatesSet {
-    type Error = TemplatesSetToStringConversionFailure;
+    type Error = Infallible;
 
     fn try_from(value: Vec<String>) -> Result<Self, Self::Error> {
         Ok(TemplatesSet(value))
@@ -52,7 +56,7 @@ impl TryFrom<Vec<String>> for TemplatesSet {
 }
 
 impl TryFrom<Vec<&str>> for TemplatesSet {
-    type Error = TemplatesSetToStringConversionFailure;
+    type Error = Infallible;
 
     fn try_from(value: Vec<&str>) -> Result<Self, Self::Error> {
         Ok(TemplatesSet(
@@ -62,9 +66,9 @@ impl TryFrom<Vec<&str>> for TemplatesSet {
 }
 
 impl TryInto<Vec<String>> for TemplatesSet {
-    type Error = TemplatesSetToStringConversionFailure;
+    type Error = Infallible;
 
-    fn try_into(self) -> Result<Vec<String>, TemplatesSetToStringConversionFailure> {
+    fn try_into(self) -> Result<Vec<String>, Self::Error> {
         Ok(self.0)
     }
 }
@@ -77,11 +81,24 @@ impl From<TemplatesSet> for String {
 
 impl From<String> for TemplatesSet {
     fn from(val: String) -> Self {
+        Self::from(val.as_str())
+    }
+}
+
+impl<'a> From<&'a str> for TemplatesSet {
+    fn from(val: &'a str) -> Self {
         let strings = val.split(',').map(|ss| ss.into()).collect();
         TemplatesSet(strings)
     }
 }
 
+impl FromStr for TemplatesSet {
+    type Err = Infallible;
+    fn from_str(value: &str) -> Result<Self, Self::Err> {
+        Ok(Self::from(value))
+    }
+}
+
 impl std::fmt::Display for TemplatesSet {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         write!(f, "{:?}", self.0)
diff --git a/crates/common/tedge_config/src/tedge_config_cli/new.rs b/crates/common/tedge_config/src/tedge_config_cli/new.rs
new file mode 100644
index 00000000000..2494618484f
--- /dev/null
+++ b/crates/common/tedge_config/src/tedge_config_cli/new.rs
@@ -0,0 +1,687 @@
+use crate::ConnectUrl;
+use crate::HostPort;
+use crate::Seconds;
+use crate::TEdgeConfigLocation;
+use crate::TemplatesSet;
+use crate::HTTPS_PORT;
+use crate::MQTT_TLS_PORT;
+use camino::Utf8PathBuf;
+use certificate::CertificateError;
+use certificate::PemCertificate;
+use doku::Document;
+use once_cell::sync::Lazy;
+use std::borrow::Cow;
+use std::net::IpAddr;
+use std::net::Ipv4Addr;
+use std::num::NonZeroU16;
+use std::path::PathBuf;
+use tedge_config_macros::define_tedge_config;
+use tedge_config_macros::struct_field_aliases;
+use tedge_config_macros::struct_field_paths;
+pub use tedge_config_macros::ConfigNotSet;
+use tedge_config_macros::OptionalConfig;
+use toml::Table;
+
+const DEFAULT_ROOT_CERT_PATH: &str = "/etc/ssl/certs";
+
+pub trait OptionalConfigError<T> {
+    fn or_err(&self) -> Result<&T, ReadError>;
+}
+
+impl<T> OptionalConfigError<T> for OptionalConfig<T> {
+    fn or_err(&self) -> Result<&T, ReadError> {
+        self.or_config_not_set().map_err(ReadError::from)
+    }
+}
+
+pub struct TEdgeConfig(TEdgeConfigReader);
+
+impl std::ops::Deref for TEdgeConfig {
+    type Target = TEdgeConfigReader;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl TEdgeConfig {
+    pub fn from_dto(dto: &TEdgeConfigDto, location: &TEdgeConfigLocation) -> Self {
+        Self(TEdgeConfigReader::from_dto(dto, location))
+    }
+}
+
+#[derive(serde::Deserialize, serde::Serialize, Clone, Copy, PartialEq, Eq, Debug)]
+#[serde(into = "&'static str", try_from = "String")]
+/// A version of tedge.toml, used to manage migrations (see [Self::migrations])
+pub enum TEdgeTomlVersion {
+    One,
+    Two,
+}
+
+impl Default for TEdgeTomlVersion {
+    fn default() -> Self {
+        Self::One
+    }
+}
+
+impl TryFrom<String> for TEdgeTomlVersion {
+    type Error = String;
+
+    fn try_from(value: String) -> Result<Self, Self::Error> {
+        match value.as_str() {
+            "1" => Ok(Self::One),
+            "2" => Ok(Self::Two),
+            _ => todo!(),
+        }
+    }
+}
+
+impl From<TEdgeTomlVersion> for &'static str {
+    fn from(value: TEdgeTomlVersion) -> Self {
+        match value {
+            TEdgeTomlVersion::One => "1",
+            TEdgeTomlVersion::Two => "2",
+        }
+    }
+}
+
+impl From<TEdgeTomlVersion> for toml::Value {
+    fn from(value: TEdgeTomlVersion) -> Self {
+        let str: &str = value.into();
+        toml::Value::String(str.to_owned())
+    }
+}
+
+pub enum TomlMigrationStep {
+    UpdateFieldValue {
+        key: &'static str,
+        value: toml::Value,
+    },
+
+    MoveKey {
+        original: &'static str,
+        target: &'static str,
+    },
+
+    RemoveTableIfEmpty {
+        key: &'static str,
+    },
+}
+
+impl TomlMigrationStep {
+    pub fn apply_to(self, mut toml: toml::Value) -> toml::Value {
+        match self {
+            TomlMigrationStep::MoveKey { original, target } => {
+                let mut doc = &mut toml;
+                let (tables, field) = original.rsplit_once('.').unwrap();
+                for key in tables.split('.') {
+                    if doc.as_table().map(|table| table.contains_key(key)) == Some(true) {
+                        doc = &mut doc[key];
+                    } else {
+                        return toml;
+                    }
+                }
+                let value = doc.as_table_mut().unwrap().remove(field);
+
+                if let Some(value) = value {
+                    let mut doc = &mut toml;
+                    let (tables, field) = target.rsplit_once('.').unwrap();
+                    for key in tables.split('.') {
+                        let table = doc.as_table_mut().unwrap();
+                        if !table.contains_key(key) {
+                            table.insert(key.to_owned(), toml::Value::Table(Table::new()));
+                        }
+                        doc = &mut doc[key];
+                    }
+                    let table = doc.as_table_mut().unwrap();
+                    // TODO if this returns Some, something is going wrong? Maybe this could be an error, or maybe it doesn't matter
+                    table.insert(field.to_owned(), value);
+                }
+            }
+            TomlMigrationStep::UpdateFieldValue { key, value } => {
+                let mut doc = &mut toml;
+                let (tables, field) = key.rsplit_once('.').unwrap();
+                for key in tables.split('.') {
+                    let table = doc.as_table_mut().unwrap();
+                    if !table.contains_key(key) {
+                        table.insert(key.to_owned(), toml::Value::Table(Table::new()));
+                    }
+                    doc = &mut doc[key];
+                }
+                let table = doc.as_table_mut().unwrap();
+                // TODO if this returns Some, something is going wrong? Maybe this could be an error, or maybe it doesn't matter
+                table.insert(field.to_owned(), value);
+            }
+            TomlMigrationStep::RemoveTableIfEmpty { key } => {
+                let mut doc = &mut toml;
+                let (parents, target) = key.rsplit_once('.').unwrap();
+                for key in parents.split('.') {
+                    let table = doc.as_table_mut().unwrap();
+                    if !table.contains_key(key) {
+                        table.insert(key.to_owned(), toml::Value::Table(Table::new()));
+                    }
+                    doc = &mut doc[key];
+                }
+                let table = doc.as_table_mut().unwrap();
+                if let Some(table) = table.get(target) {
+                    let table = table.as_table().unwrap();
+                    // TODO make sure this is covered in toml migration test
+                    if !table.is_empty() {
+                        return toml;
+                    }
+                }
+                table.remove(target);
+            }
+        }
+
+        toml
+    }
+}
+
+/// The keys that can be read from the configuration
+pub static READABLE_KEYS: Lazy<Vec<(Cow<'static, str>, doku::Type)>> = Lazy::new(|| {
+    let ty = TEdgeConfigReader::ty();
+    let doku::TypeKind::Struct { fields, transparent: false } = ty.kind else { panic!("Expected struct but got {:?}", ty.kind) };
+    let doku::Fields::Named { fields } = fields else { panic!("Expected named fields but got {:?}", fields)};
+    struct_field_paths(None, &fields)
+});
+
+impl TEdgeTomlVersion {
+    fn next(self) -> Self {
+        match self {
+            Self::One => Self::Two,
+            Self::Two => Self::Two,
+        }
+    }
+
+    /// The migrations to upgrade `tedge.toml` from its current version to the
+    /// next version.
+    ///
+    /// If this returns `None`, the version of `tedge.toml` is the latest
+    /// version, and no migrations need to be applied.
+    pub fn migrations(self) -> Option<Vec<TomlMigrationStep>> {
+        use WritableKey::*;
+        let mv = |original, target: WritableKey| TomlMigrationStep::MoveKey {
+            original,
+            target: target.as_str(),
+        };
+        let update_version_field = || TomlMigrationStep::UpdateFieldValue {
+            key: "config.version",
+            value: self.next().into(),
+        };
+        let rm = |key| TomlMigrationStep::RemoveTableIfEmpty { key };
+
+        match self {
+            Self::One => Some(vec![
+                mv("mqtt.port", MqttBindPort),
+                mv("mqtt.bind_address", MqttBindAddress),
+                mv("mqtt.client_host", MqttClientHost),
+                mv("mqtt.client_port", MqttClientPort),
+                mv("mqtt.client_ca_file", MqttClientAuthCaFile),
+                mv("mqtt.client_ca_path", MqttClientAuthCaDir),
+                mv("mqtt.client_auth.cert_file", MqttClientAuthCertFile),
+                mv("mqtt.client_auth.key_file", MqttClientAuthKeyFile),
+                rm("mqtt.client_auth"),
+                mv("mqtt.external_port", MqttExternalBindPort),
+                mv("mqtt.external_bind_address", MqttExternalBindAddress),
+                mv("mqtt.external_bind_interface", MqttExternalBindInterface),
+                mv("mqtt.external_capath", MqttExternalCaPath),
+                mv("mqtt.external_certfile", MqttExternalCertFile),
+                mv("mqtt.external_keyfile", MqttExternalKeyFile),
+                mv("az.mapper_timestamp", AzMapperTimestamp),
+                mv("aws.mapper_timestamp", AwsMapperTimestamp),
+                mv("http.port", HttpBindPort),
+                mv("http.bind_address", HttpBindAddress),
+                mv("software.default_plugin_type", SoftwarePluginDefault),
+                mv("run.lock_files", RunLockFiles),
+                mv("firmware.child_update_timeout", FirmwareChildUpdateTimeout),
+                mv("c8y.smartrest_templates", C8ySmartrestTemplates),
+                update_version_field(),
+            ]),
+            Self::Two => None,
+        }
+    }
+}
+
+define_tedge_config! {
+    #[tedge_config(reader(skip))]
+    config: {
+        #[tedge_config(default(variable = "TEdgeTomlVersion::One"))]
+        version: TEdgeTomlVersion,
+    },
+
+    device: {
+        /// Identifier of the device within the fleet. It must be globally
+        /// unique and is derived from the device certificate.
+        #[tedge_config(readonly(
+            write_error = "\
+                The device id is read from the device certificate and cannot be set directly.\n\
+                To set 'device.id' to some <id>, you can use `tedge cert create --device-id <id>`.",
+            function = "device_id",
+        ))]
+        #[tedge_config(example = "Raspberrypi-4d18303a-6d3a-11eb-b1a6-175f6bb72665")]
+        #[tedge_config(note = "This setting is derived from the device certificate and is therefore read only.")]
+        #[doku(as = "String")]
+        id: Result<String, ReadError>,
+
+        /// Path where the device's private key is stored
+        #[tedge_config(example = "/etc/tedge/device-certs/tedge-private-key.pem", default(function = "default_device_key"))]
+        #[doku(as = "PathBuf")]
+        key_path: Utf8PathBuf,
+
+        /// Path where the device's certificate is stored
+        #[tedge_config(example = "/etc/tedge/device-certs/tedge-certificate.pem", default(function = "default_device_cert"))]
+        #[doku(as = "PathBuf")]
+        cert_path: Utf8PathBuf,
+
+        /// The default device type
+        #[tedge_config(example = "thin-edge.io", default(value = "thin-edge.io"))]
+        #[tedge_config(rename = "type")]
+        ty: String,
+    },
+
+    c8y: {
+        /// Endpoint URL of Cumulocity tenant
+        #[tedge_config(example = "your-tenant.cumulocity.com")]
+        #[tedge_config(reader(private))]
+        url: ConnectUrl,
+
+        /// The path where Cumulocity root certificate(s) are stared
+        #[tedge_config(note = "The value can be a directory path as well as the path of the direct certificate file.")]
+        #[tedge_config(example = "/etc/tedge/az-trusted-root-certificates.pem", default(variable = "DEFAULT_ROOT_CERT_PATH"))]
+        #[doku(as = "PathBuf")]
+        root_cert_path: Utf8PathBuf,
+
+        smartrest: {
+            /// Set of SmartREST template IDs the device should subscribe to
+            #[tedge_config(example = "templateId1,templateId2", default(function = "TemplatesSet::default"))]
+            templates: TemplatesSet,
+        },
+
+
+        /// HTTP Endpoint for the Cumulocity tenant, with optional port.
+        #[tedge_config(example = "http.your-tenant.cumulocity.com:1234")]
+        #[tedge_config(default(from_optional_key = "c8y.url"))]
+        http: HostPort<HTTPS_PORT>,
+
+        /// MQTT Endpoint for the Cumulocity tenant, with optional port.
+        #[tedge_config(example = "mqtt.your-tenant.cumulocity.com:1234")]
+        #[tedge_config(default(from_optional_key = "c8y.url"))]
+        mqtt: HostPort<MQTT_TLS_PORT>,
+
+    },
+
+    #[tedge_config(deprecated_name = "azure")] // for 0.1.0 compatibility
+    az: {
+        /// Endpoint URL of Azure IoT tenant
+        #[tedge_config(example = "myazure.azure-devices.net")]
+        url: ConnectUrl,
+
+        /// The path where Azure IoT root certificate(s) are stared
+        #[tedge_config(note = "The value can be a directory path as well as the path of the direct certificate file.")]
+        #[tedge_config(example = "/etc/tedge/az-trusted-root-certificates.pem", default(variable = "DEFAULT_ROOT_CERT_PATH"))]
+        #[doku(as = "PathBuf")]
+        root_cert_path: Utf8PathBuf,
+
+        mapper: {
+            /// Whether the Azure IoT mapper should add a timestamp or not
+            #[tedge_config(example = "true")]
+            #[tedge_config(default(value = true))]
+            timestamp: bool,
+        }
+    },
+
+    aws: {
+        /// Endpoint URL of AWS IoT tenant
+        #[tedge_config(example = "your-endpoint.amazonaws.com")]
+        url: ConnectUrl,
+
+        /// The path where AWS IoT root certificate(s) are stared
+        #[tedge_config(note = "The value can be a directory path as well as the path of the direct certificate file.")]
+        #[tedge_config(example = "/etc/tedge/aws-trusted-root-certificates.pem", default(variable = "DEFAULT_ROOT_CERT_PATH"))]
+        #[doku(as = "PathBuf")]
+        root_cert_path: Utf8PathBuf,
+
+        mapper: {
+            /// Whether the AWS IoT mapper should add a timestamp or not
+            #[tedge_config(example = "true")]
+            #[tedge_config(default(value = true))]
+            timestamp: bool,
+        }
+    },
+
+    mqtt: {
+        bind: {
+            /// The address mosquitto binds to for internal use
+            #[tedge_config(example = "127.0.0.1", default(variable = "Ipv4Addr::LOCALHOST"))]
+            address: IpAddr,
+
+            /// The port mosquitto binds to for internal use
+            #[tedge_config(example = "1883", default(function = "default_mqtt_port"), deprecated_key = "mqtt.port")]
+            #[doku(as = "u16")]
+            // This was originally u16, but I can't think of any way in which
+            // tedge could actually connect to mosquitto if it bound to a random
+            // free port, so I don't think 0 is *really* valid here
+            port: NonZeroU16,
+        },
+
+        client: {
+            /// The host that the thin-edge MQTT client should connect to
+            #[tedge_config(example = "localhost", default(value = "localhost"))]
+            host: String,
+
+            /// The port that the thin-edge MQTT client should connect to
+            #[tedge_config(default(from_key = "mqtt.bind.port"))]
+            #[doku(as = "u16")]
+            port: NonZeroU16,
+
+            #[tedge_config(reader(private))]
+            auth: {
+                /// Path to the CA certificate used by MQTT clients to use when authenticating the MQTT broker
+                #[tedge_config(example = "/etc/mosquitto/ca_certificates/ca.crt")]
+                #[doku(as = "PathBuf")]
+                #[tedge_config(deprecated_name = "cafile")]
+                ca_file: Utf8PathBuf,
+
+                /// Path to the directory containing the CA certificates used by MQTT
+                /// clients when authenticating the MQTT broker
+                #[tedge_config(example = "/etc/mosquitto/ca_certificates")]
+                #[doku(as = "PathBuf")]
+                #[tedge_config(deprecated_name = "cadir")]
+                ca_dir: Utf8PathBuf,
+
+                /// Path to the client certficate
+                #[doku(as = "PathBuf")]
+                #[tedge_config(example = "/etc/mosquitto/auth_certificates/cert.pem")]
+                #[tedge_config(deprecated_name = "certfile")]
+                cert_file: Utf8PathBuf,
+
+                /// Path to the client private key
+                #[doku(as = "PathBuf")]
+                #[tedge_config(example = "/etc/mosquitto/auth_certificates/key.pem")]
+                #[tedge_config(deprecated_name = "keyfile")]
+                key_file: Utf8PathBuf,
+            }
+        },
+
+        external: {
+            bind: {
+                /// The port mosquitto binds to for external use
+                #[tedge_config(example = "8883", deprecated_key = "mqtt.external.port")]
+                port: u16,
+
+                /// The address mosquitto binds to for external use
+                #[tedge_config(example = "0.0.0.0")]
+                address: IpAddr,
+
+                /// Name of the network interface which mosquitto limits incoming connections on
+                #[tedge_config(example = "wlan0")]
+                interface: String,
+            },
+
+            /// Path to a file containing the PEM encoded CA certificates that are
+            /// trusted when checking incoming client certificates
+            #[tedge_config(example = "/etc/ssl/certs")]
+            #[doku(as = "PathBuf")]
+            #[tedge_config(deprecated_key = "mqtt.external.capath")]
+            ca_path: Utf8PathBuf,
+
+            /// Path to the certificate file which is used by the external MQTT listener
+            #[tedge_config(note = "This setting shall be used together with `mqtt.external.key_file` for external connections.")]
+            #[tedge_config(example = "/etc/tedge/device-certs/tedge-certificate.pem")]
+            #[doku(as = "PathBuf")]
+            #[tedge_config(deprecated_key = "mqtt.external.certfile")]
+            cert_file: Utf8PathBuf,
+
+            /// Path to the key file which is used by the external MQTT listener
+            #[tedge_config(note = "This setting shall be used together with `mqtt.external.cert_file` for external connections.")]
+            #[tedge_config(example = "/etc/tedge/device-certs/tedge-private-key.pem")]
+            #[doku(as = "PathBuf")]
+            #[tedge_config(deprecated_key = "mqtt.external.keyfile")]
+            key_file: Utf8PathBuf,
+        }
+    },
+
+    http: {
+        bind: {
+            /// Http server port used by the File Transfer Service
+            #[tedge_config(example = "8000", deprecated_key = "http.port")]
+            port: u16,
+
+            /// Http server address used by the File Transfer Service
+            #[tedge_config(default(function = "default_http_address"), deprecated_key = "http.address")]
+            #[tedge_config(example = "127.0.0.1", example = "192.168.1.2")]
+            address: IpAddr,
+        },
+    },
+
+    software: {
+        plugin: {
+            /// The default software plugin to be used for software management on the device
+            #[tedge_config(example = "apt")]
+            default: String,
+        }
+    },
+
+    run: {
+        /// The directory used to store runtime information, such as file locks
+        #[doku(as = "PathBuf")]
+        #[tedge_config(example = "/run", default(value = "/run"))]
+        path: Utf8PathBuf,
+
+        /// Whether to create a lock file or not
+        #[tedge_config(example = "true", default(value = true))]
+        lock_files: bool,
+    },
+
+    logs: {
+        /// The directory used to store logs
+        #[tedge_config(example = "/var/log", default(value = "/var/log"))]
+        #[doku(as = "PathBuf")]
+        path: Utf8PathBuf,
+    },
+
+    tmp: {
+        /// The temporary directory used to download files to the device
+        #[tedge_config(example = "/tmp", default(value = "/tmp"))]
+        #[doku(as = "PathBuf")]
+        path: Utf8PathBuf,
+    },
+
+    data: {
+        /// The directory used to store data like cached files, runtime metadata, etc.
+        #[tedge_config(example = "/var/tedge", default(value = "/var/tedge"))]
+        #[doku(as = "PathBuf")]
+        path: Utf8PathBuf,
+    },
+
+    firmware: {
+        child: {
+            update: {
+                /// The timeout limit in seconds for firmware update operations on child devices
+                #[tedge_config(example = "3600", default(value = 3600_u64))]
+                timeout: Seconds,
+            }
+        }
+    },
+
+    service: {
+        /// The thin-edge.io service's service type
+        #[tedge_config(rename = "type", example = "systemd", default(value = "service"))]
+        ty: String,
+    },
+}
+
+fn default_http_address(dto: &TEdgeConfigDto) -> IpAddr {
+    let external_address = dto.mqtt.external.bind.address;
+    external_address
+        .or(dto.mqtt.bind.address)
+        .unwrap_or(Ipv4Addr::LOCALHOST.into())
+}
+
+fn device_id(reader: &TEdgeConfigReader) -> Result<String, ReadError> {
+    let pem = PemCertificate::from_pem_file(&reader.device.cert_path)
+        .map_err(|err| cert_error_into_config_error(ReadOnlyKey::DeviceId.as_str(), err))?;
+    let device_id = pem
+        .subject_common_name()
+        .map_err(|err| cert_error_into_config_error(ReadOnlyKey::DeviceId.as_str(), err))?;
+    Ok(device_id)
+}
+
+fn cert_error_into_config_error(key: &'static str, err: CertificateError) -> ReadError {
+    match &err {
+        CertificateError::IoError(io_err) => match io_err.kind() {
+            std::io::ErrorKind::NotFound => ReadError::ReadOnlyNotFound { key,
+                message: concat!(
+                    "The device id is read from the device certificate.\n",
+                    "To set 'device.id' to some <id>, you can use `tedge cert create --device-id <id>`.",
+                ),
+            },
+            _ => ReadError::DerivationFailed {
+                key,
+                cause: format!("{}", err),
+            },
+        },
+        _ => ReadError::DerivationFailed {
+            key,
+            cause: format!("{}", err),
+        },
+    }
+}
+
+fn default_device_key(location: &TEdgeConfigLocation) -> Utf8PathBuf {
+    location
+        .tedge_config_root_path()
+        .join("device-certs")
+        .join("tedge-private-key.pem")
+}
+
+fn default_device_cert(location: &TEdgeConfigLocation) -> Utf8PathBuf {
+    location
+        .tedge_config_root_path()
+        .join("device-certs")
+        .join("tedge-certificate.pem")
+}
+
+fn default_mqtt_port() -> NonZeroU16 {
+    NonZeroU16::try_from(1883).unwrap()
+}
+
+#[derive(thiserror::Error, Debug)]
+pub enum ReadError {
+    #[error(transparent)]
+    ConfigNotSet(#[from] ConfigNotSet),
+
+    #[error("Config value {key}, cannot be read: {message} ")]
+    ReadOnlyNotFound {
+        key: &'static str,
+        message: &'static str,
+    },
+
+    #[error("Derivation for `{key}` failed: {cause}")]
+    DerivationFailed { key: &'static str, cause: String },
+}
+
+/// An abstraction over the possible default functions for tedge config values
+///
+/// Some configuration defaults are relative to the config location, and
+/// this trait allows us to pass that in, or the DTO, both, or neither!
+pub trait TEdgeConfigDefault<T, Args> {
+    type Output;
+    fn call(self, data: &T, location: &TEdgeConfigLocation) -> Self::Output;
+}
+
+impl<F, Out, T> TEdgeConfigDefault<T, ()> for F
+where
+    F: FnOnce() -> Out + Clone,
+{
+    type Output = Out;
+    fn call(self, _: &T, _: &TEdgeConfigLocation) -> Self::Output {
+        (self)()
+    }
+}
+
+impl<F, Out, T> TEdgeConfigDefault<T, &T> for F
+where
+    F: FnOnce(&T) -> Out + Clone,
+{
+    type Output = Out;
+    fn call(self, data: &T, _location: &TEdgeConfigLocation) -> Self::Output {
+        (self)(data)
+    }
+}
+
+impl<F, Out, T> TEdgeConfigDefault<T, (&TEdgeConfigLocation,)> for F
+where
+    F: FnOnce(&TEdgeConfigLocation) -> Out + Clone,
+{
+    type Output = Out;
+    fn call(self, _data: &T, location: &TEdgeConfigLocation) -> Self::Output {
+        (self)(location)
+    }
+}
+
+impl<F, Out, T> TEdgeConfigDefault<T, (&T, &TEdgeConfigLocation)> for F
+where
+    F: FnOnce(&T, &TEdgeConfigLocation) -> Out + Clone,
+{
+    type Output = Out;
+    fn call(self, data: &T, location: &TEdgeConfigLocation) -> Self::Output {
+        (self)(data, location)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test_case::test_case("device.id")]
+    #[test_case::test_case("device.type")]
+    #[test_case::test_case("device.key.path")]
+    #[test_case::test_case("device.cert.path")]
+    #[test_case::test_case("c8y.url")]
+    #[test_case::test_case("c8y.root.cert.path")]
+    #[test_case::test_case("c8y.smartrest.templates")]
+    #[test_case::test_case("az.url")]
+    #[test_case::test_case("az.root.cert.path")]
+    #[test_case::test_case("aws.url")]
+    #[test_case::test_case("aws.root.cert.path")]
+    #[test_case::test_case("aws.mapper.timestamp")]
+    #[test_case::test_case("az.mapper.timestamp")]
+    #[test_case::test_case("mqtt.bind_address")]
+    #[test_case::test_case("http.address")]
+    #[test_case::test_case("mqtt.client.host")]
+    #[test_case::test_case("mqtt.client.port")]
+    #[test_case::test_case("mqtt.client.auth.cafile")]
+    #[test_case::test_case("mqtt.client.auth.cadir")]
+    #[test_case::test_case("mqtt.client.auth.certfile")]
+    #[test_case::test_case("mqtt.client.auth.keyfile")]
+    #[test_case::test_case("mqtt.port")]
+    #[test_case::test_case("http.port")]
+    #[test_case::test_case("mqtt.external.port")]
+    #[test_case::test_case("mqtt.external.bind_address")]
+    #[test_case::test_case("mqtt.external.bind_interface")]
+    #[test_case::test_case("mqtt.external.capath")]
+    #[test_case::test_case("mqtt.external.certfile")]
+    #[test_case::test_case("mqtt.external.keyfile")]
+    #[test_case::test_case("software.plugin.default")]
+    #[test_case::test_case("tmp.path")]
+    #[test_case::test_case("logs.path")]
+    #[test_case::test_case("run.path")]
+    #[test_case::test_case("data.path")]
+    #[test_case::test_case("firmware.child.update.timeout")]
+    #[test_case::test_case("service.type")]
+    #[test_case::test_case("run.lock_files")]
+    fn all_0_10_keys_can_be_deserialised(key: &str) {
+        key.parse::<ReadableKey>().unwrap();
+    }
+
+    #[test]
+    fn missing_c8y_http_directs_user_towards_setting_c8y_url() {
+        let dto = TEdgeConfigDto::default();
+
+        let reader = TEdgeConfigReader::from_dto(&dto, &TEdgeConfigLocation::default());
+
+        assert_eq!(reader.c8y.http.key(), "c8y.url");
+    }
+}
diff --git a/crates/common/tedge_config/src/tedge_config_cli/settings.rs b/crates/common/tedge_config/src/tedge_config_cli/settings.rs
index 18f0f315eb9..a5ed5764031 100644
--- a/crates/common/tedge_config/src/tedge_config_cli/settings.rs
+++ b/crates/common/tedge_config/src/tedge_config_cli/settings.rs
@@ -43,7 +43,7 @@ impl ConfigSetting for DeviceTypeSetting {
 pub struct DeviceKeyPathSetting;
 
 impl ConfigSetting for DeviceKeyPathSetting {
-    const KEY: &'static str = "device.key.path";
+    const KEY: &'static str = "device.key_path";
 
     const DESCRIPTION: &'static str =
         "Path to the private key file. Example: /home/user/.tedge/tedge-private-key.pem";
@@ -60,7 +60,7 @@ impl ConfigSetting for DeviceKeyPathSetting {
 pub struct DeviceCertPathSetting;
 
 impl ConfigSetting for DeviceCertPathSetting {
-    const KEY: &'static str = "device.cert.path";
+    const KEY: &'static str = "device.cert_path";
 
     const DESCRIPTION: &'static str =
         "Path to the certificate file. Example: /home/user/.tedge/tedge-certificate.crt";
@@ -123,7 +123,7 @@ impl ConfigSetting for C8yMqttSetting {
 pub struct C8yRootCertPathSetting;
 
 impl ConfigSetting for C8yRootCertPathSetting {
-    const KEY: &'static str = "c8y.root.cert.path";
+    const KEY: &'static str = "c8y.root_cert_path";
 
     const DESCRIPTION: &'static str = concat!(
         "Path where Cumulocity root certificate(s) are located. ",
@@ -180,7 +180,7 @@ impl ConfigSetting for AzureUrlSetting {
 pub struct AzureRootCertPathSetting;
 
 impl ConfigSetting for AzureRootCertPathSetting {
-    const KEY: &'static str = "az.root.cert.path";
+    const KEY: &'static str = "az.root_cert_path";
 
     const DESCRIPTION: &'static str = concat!(
         "Path where Azure IoT root certificate(s) are located. ",
@@ -253,7 +253,7 @@ impl ConfigSetting for AwsUrlSetting {
 pub struct AwsRootCertPathSetting;
 
 impl ConfigSetting for AwsRootCertPathSetting {
-    const KEY: &'static str = "aws.root.cert.path";
+    const KEY: &'static str = "aws.root_cert_path";
 
     const DESCRIPTION: &'static str = concat!(
         "Path where AWS IoT root certificate(s) are located. ",
@@ -295,7 +295,7 @@ impl ConfigSetting for MqttClientPortSetting {
 pub struct MqttClientCafileSetting;
 
 impl ConfigSetting for MqttClientCafileSetting {
-    const KEY: &'static str = "mqtt.client.auth.cafile";
+    const KEY: &'static str = "mqtt.client.auth.ca_file";
 
     const DESCRIPTION: &'static str = concat!(
         "Path to the CA certificate used by MQTT clients to use when ",
@@ -309,7 +309,7 @@ impl ConfigSetting for MqttClientCafileSetting {
 pub struct MqttClientCapathSetting;
 
 impl ConfigSetting for MqttClientCapathSetting {
-    const KEY: &'static str = "mqtt.client.auth.cadir";
+    const KEY: &'static str = "mqtt.client.auth.ca_dir";
 
     const DESCRIPTION: &'static str = concat!(
         "Path to the directory containing CA certificate used by MQTT clients ",
@@ -324,7 +324,7 @@ impl ConfigSetting for MqttClientCapathSetting {
 pub struct MqttClientAuthCertSetting;
 
 impl ConfigSetting for MqttClientAuthCertSetting {
-    const KEY: &'static str = "mqtt.client.auth.certfile";
+    const KEY: &'static str = "mqtt.client.auth.cert_file";
 
     const DESCRIPTION: &'static str =
         "Path to the client certificate used for client authentication";
@@ -336,7 +336,7 @@ impl ConfigSetting for MqttClientAuthCertSetting {
 pub struct MqttClientAuthKeySetting;
 
 impl ConfigSetting for MqttClientAuthKeySetting {
-    const KEY: &'static str = "mqtt.client.auth.keyfile";
+    const KEY: &'static str = "mqtt.client.auth.key_file";
 
     const DESCRIPTION: &'static str =
         "Path to the client private key used for client authentication";
@@ -348,7 +348,7 @@ impl ConfigSetting for MqttClientAuthKeySetting {
 pub struct MqttPortSetting;
 
 impl ConfigSetting for MqttPortSetting {
-    const KEY: &'static str = "mqtt.port";
+    const KEY: &'static str = "mqtt.bind.port";
 
     const DESCRIPTION: &'static str = concat!(
         "Mqtt broker port, which is used by the local mqtt clients to publish or subscribe. ",
@@ -389,7 +389,7 @@ impl ConfigSetting for HttpBindAddressSetting {
 pub struct MqttBindAddressSetting;
 
 impl ConfigSetting for MqttBindAddressSetting {
-    const KEY: &'static str = "mqtt.bind_address";
+    const KEY: &'static str = "mqtt.bind.address";
 
     const DESCRIPTION: &'static str = concat!(
         "Mqtt bind address, which is used by the mqtt clients to publish or subscribe. ",
@@ -403,7 +403,7 @@ impl ConfigSetting for MqttBindAddressSetting {
 pub struct MqttExternalPortSetting;
 
 impl ConfigSetting for MqttExternalPortSetting {
-    const KEY: &'static str = "mqtt.external.port";
+    const KEY: &'static str = "mqtt.external.bind.port";
 
     const DESCRIPTION: &'static str = concat!(
         "Mqtt broker port, which is used by the external mqtt clients to publish or subscribe. ",
@@ -417,7 +417,7 @@ impl ConfigSetting for MqttExternalPortSetting {
 pub struct MqttExternalBindAddressSetting;
 
 impl ConfigSetting for MqttExternalBindAddressSetting {
-    const KEY: &'static str = "mqtt.external.bind_address";
+    const KEY: &'static str = "mqtt.external.bind.address";
 
     const DESCRIPTION: &'static str = concat!(
         "IP address / hostname, which the mqtt broker limits incoming connections on. ",
@@ -431,7 +431,7 @@ impl ConfigSetting for MqttExternalBindAddressSetting {
 pub struct MqttExternalBindInterfaceSetting;
 
 impl ConfigSetting for MqttExternalBindInterfaceSetting {
-    const KEY: &'static str = "mqtt.external.bind_interface";
+    const KEY: &'static str = "mqtt.external.bind.interface";
 
     const DESCRIPTION: &'static str = concat!(
         "Name of network interface, which the mqtt broker limits incoming connections on. ",
@@ -445,7 +445,7 @@ impl ConfigSetting for MqttExternalBindInterfaceSetting {
 pub struct MqttExternalCAPathSetting;
 
 impl ConfigSetting for MqttExternalCAPathSetting {
-    const KEY: &'static str = "mqtt.external.capath";
+    const KEY: &'static str = "mqtt.external.ca_path";
 
     const DESCRIPTION: &'static str = concat!(
         "Path to a file containing the PEM encoded CA certificates ",
@@ -461,12 +461,12 @@ impl ConfigSetting for MqttExternalCAPathSetting {
 pub struct MqttExternalCertfileSetting;
 
 impl ConfigSetting for MqttExternalCertfileSetting {
-    const KEY: &'static str = "mqtt.external.certfile";
+    const KEY: &'static str = "mqtt.external.cert_file";
 
     const DESCRIPTION: &'static str = concat!(
         "Path to the certificate file, which is used by external MQTT listener",
         "Example: /etc/tedge/device-certs/tedge-certificate.pem",
-        "Note: This setting shall be used together with `mqtt.external.keyfile` for external connections."
+        "Note: This setting shall be used together with `mqtt.external.key_file` for external connections."
     );
 
     type Value = Utf8PathBuf;
@@ -476,12 +476,12 @@ impl ConfigSetting for MqttExternalCertfileSetting {
 pub struct MqttExternalKeyfileSetting;
 
 impl ConfigSetting for MqttExternalKeyfileSetting {
-    const KEY: &'static str = "mqtt.external.keyfile";
+    const KEY: &'static str = "mqtt.external.key_file";
 
     const DESCRIPTION: &'static str = concat!(
         "Path to the private key file, which is used by external MQTT listener",
         "Example: /etc/tedge/device-certs/tedge-private-key.pem",
-        "Note: This setting shall be used together with `mqtt.external.certfile` for external connections."
+        "Note: This setting shall be used together with `mqtt.external.cert_file` for external connections."
     );
 
     type Value = Utf8PathBuf;
diff --git a/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs b/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs
index 15faf929009..67298e42f5a 100644
--- a/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs
+++ b/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs
@@ -1,4 +1,3 @@
-use crate::seconds::Seconds;
 use crate::*;
 use camino::Utf8PathBuf;
 use certificate::CertificateError;
@@ -19,7 +18,7 @@ pub fn get_tedge_config() -> Result<TEdgeConfig, TEdgeConfigError> {
 ///
 #[derive(Debug)]
 pub struct TEdgeConfig {
-    pub(crate) data: TEdgeConfigDto,
+    pub(crate) data: new::TEdgeConfigDto,
     pub(crate) config_defaults: TEdgeConfigDefaults,
 }
 
@@ -48,7 +47,7 @@ impl ConfigSettingAccessor<DeviceTypeSetting> for TEdgeConfig {
         let device_type = self
             .data
             .device
-            .device_type
+            .ty
             .clone()
             .unwrap_or_else(|| self.config_defaults.default_device_type.clone());
         Ok(device_type)
@@ -59,12 +58,12 @@ impl ConfigSettingAccessor<DeviceTypeSetting> for TEdgeConfig {
         _setting: DeviceTypeSetting,
         value: <DeviceTypeSetting as ConfigSetting>::Value,
     ) -> ConfigSettingResult<()> {
-        self.data.device.device_type = Some(value);
+        self.data.device.ty = Some(value);
         Ok(())
     }
 
     fn unset(&mut self, _setting: DeviceTypeSetting) -> ConfigSettingResult<()> {
-        self.data.device.device_type = None;
+        self.data.device.ty = None;
         Ok(())
     }
 }
@@ -82,7 +81,7 @@ fn http_bind_address_read_only_error() -> ConfigSettingError {
     ConfigSettingError::ReadonlySetting {
         message: concat!(
             "The http address cannot be set directly. It is read from the mqtt bind address.\n",
-            "To set 'http.bind_address' to some <address>, you can `tedge config set mqtt.bind_address <address>`.",
+            "To set 'http.bind_address' to some <address>, you can `tedge config set mqtt.bind.address <address>`.",
         ),
     }
 }
@@ -238,7 +237,8 @@ impl ConfigSettingAccessor<C8ySmartRestTemplates> for TEdgeConfig {
         Ok(self
             .data
             .c8y
-            .smartrest_templates
+            .smartrest
+            .templates
             .clone()
             .unwrap_or_else(|| self.config_defaults.default_c8y_smartrest_templates.clone()))
     }
@@ -248,12 +248,12 @@ impl ConfigSettingAccessor<C8ySmartRestTemplates> for TEdgeConfig {
         _setting: C8ySmartRestTemplates,
         value: TemplatesSet,
     ) -> ConfigSettingResult<()> {
-        self.data.c8y.smartrest_templates = Some(value);
+        self.data.c8y.smartrest.templates = Some(value);
         Ok(())
     }
 
     fn unset(&mut self, _setting: C8ySmartRestTemplates) -> ConfigSettingResult<()> {
-        self.data.c8y.smartrest_templates = None;
+        self.data.c8y.smartrest.templates = None;
         Ok(())
     }
 }
@@ -363,18 +363,19 @@ impl ConfigSettingAccessor<AzureMapperTimestamp> for TEdgeConfig {
         Ok(self
             .data
             .az
-            .mapper_timestamp
+            .mapper
+            .timestamp
             .map(Flag)
             .unwrap_or_else(|| self.config_defaults.default_mapper_timestamp.clone()))
     }
 
     fn update(&mut self, _setting: AzureMapperTimestamp, value: Flag) -> ConfigSettingResult<()> {
-        self.data.az.mapper_timestamp = Some(value.into());
+        self.data.az.mapper.timestamp = Some(value.into());
         Ok(())
     }
 
     fn unset(&mut self, _setting: AzureMapperTimestamp) -> ConfigSettingResult<()> {
-        self.data.az.mapper_timestamp = None;
+        self.data.az.mapper.timestamp = None;
         Ok(())
     }
 }
@@ -384,18 +385,19 @@ impl ConfigSettingAccessor<AwsMapperTimestamp> for TEdgeConfig {
         Ok(self
             .data
             .aws
-            .mapper_timestamp
+            .mapper
+            .timestamp
             .map(Flag)
             .unwrap_or_else(|| self.config_defaults.default_mapper_timestamp.clone()))
     }
 
     fn update(&mut self, _setting: AwsMapperTimestamp, value: Flag) -> ConfigSettingResult<()> {
-        self.data.aws.mapper_timestamp = Some(value.into());
+        self.data.aws.mapper.timestamp = Some(value.into());
         Ok(())
     }
 
     fn unset(&mut self, _setting: AwsMapperTimestamp) -> ConfigSettingResult<()> {
-        self.data.aws.mapper_timestamp = None;
+        self.data.aws.mapper.timestamp = None;
         Ok(())
     }
 }
@@ -430,7 +432,8 @@ impl ConfigSettingAccessor<MqttClientHostSetting> for TEdgeConfig {
         Ok(self
             .data
             .mqtt
-            .client_host
+            .client
+            .host
             .clone()
             .unwrap_or(self.config_defaults.default_mqtt_client_host.clone()))
     }
@@ -440,12 +443,12 @@ impl ConfigSettingAccessor<MqttClientHostSetting> for TEdgeConfig {
         _setting: MqttClientHostSetting,
         value: String,
     ) -> ConfigSettingResult<()> {
-        self.data.mqtt.client_host = Some(value);
+        self.data.mqtt.client.host = Some(value);
         Ok(())
     }
 
     fn unset(&mut self, _setting: MqttClientHostSetting) -> ConfigSettingResult<()> {
-        self.data.mqtt.client_host = None;
+        self.data.mqtt.client.host = None;
         Ok(())
     }
 }
@@ -455,7 +458,8 @@ impl ConfigSettingAccessor<MqttClientPortSetting> for TEdgeConfig {
         Ok(self
             .data
             .mqtt
-            .client_port
+            .client
+            .port
             .map(|p| Port(p.into()))
             .unwrap_or_else(|| self.config_defaults.default_mqtt_port))
     }
@@ -465,12 +469,12 @@ impl ConfigSettingAccessor<MqttClientPortSetting> for TEdgeConfig {
         let port: NonZeroU16 = port.try_into().map_err(|_| ConfigSettingError::Other {
             msg: "Can't use 0 for a client port",
         })?;
-        self.data.mqtt.client_port = Some(port);
+        self.data.mqtt.client.port = Some(port);
         Ok(())
     }
 
     fn unset(&mut self, _setting: MqttClientPortSetting) -> ConfigSettingResult<()> {
-        self.data.mqtt.client_port = None;
+        self.data.mqtt.client.port = None;
         Ok(())
     }
 }
@@ -479,10 +483,12 @@ impl ConfigSettingAccessor<MqttClientCafileSetting> for TEdgeConfig {
     fn query(&self, _setting: MqttClientCafileSetting) -> ConfigSettingResult<Utf8PathBuf> {
         self.data
             .mqtt
-            .client_ca_file
+            .client
+            .auth
+            .ca_file
             .clone()
             .ok_or(ConfigSettingError::ConfigNotSet {
-                key: "mqtt.client.auth.cafile",
+                key: "mqtt.client.auth.ca_file",
             })
     }
 
@@ -491,12 +497,12 @@ impl ConfigSettingAccessor<MqttClientCafileSetting> for TEdgeConfig {
         _setting: MqttClientCafileSetting,
         ca_file: Utf8PathBuf,
     ) -> ConfigSettingResult<()> {
-        self.data.mqtt.client_ca_file = Some(ca_file);
+        self.data.mqtt.client.auth.ca_file = Some(ca_file);
         Ok(())
     }
 
     fn unset(&mut self, _setting: MqttClientCafileSetting) -> ConfigSettingResult<()> {
-        self.data.mqtt.client_ca_file = None;
+        self.data.mqtt.client.auth.ca_file = None;
         Ok(())
     }
 }
@@ -505,10 +511,12 @@ impl ConfigSettingAccessor<MqttClientCapathSetting> for TEdgeConfig {
     fn query(&self, _setting: MqttClientCapathSetting) -> ConfigSettingResult<Utf8PathBuf> {
         self.data
             .mqtt
-            .client_ca_path
+            .client
+            .auth
+            .ca_dir
             .clone()
             .ok_or(ConfigSettingError::ConfigNotSet {
-                key: "mqtt.client.auth.cadir",
+                key: "mqtt.client.auth.ca_dir",
             })
     }
 
@@ -517,12 +525,12 @@ impl ConfigSettingAccessor<MqttClientCapathSetting> for TEdgeConfig {
         _setting: MqttClientCapathSetting,
         cafile: Utf8PathBuf,
     ) -> ConfigSettingResult<()> {
-        self.data.mqtt.client_ca_path = Some(cafile);
+        self.data.mqtt.client.auth.ca_dir = Some(cafile);
         Ok(())
     }
 
     fn unset(&mut self, _setting: MqttClientCapathSetting) -> ConfigSettingResult<()> {
-        self.data.mqtt.client_ca_path = None;
+        self.data.mqtt.client.auth.ca_dir = None;
         Ok(())
     }
 }
@@ -531,17 +539,12 @@ impl ConfigSettingAccessor<MqttClientAuthCertSetting> for TEdgeConfig {
     fn query(&self, _setting: MqttClientAuthCertSetting) -> ConfigSettingResult<Utf8PathBuf> {
         self.data
             .mqtt
-            .client_auth
-            .as_ref()
-            .ok_or(ConfigSettingError::ConfigNotSet {
-                key: "mqtt.client.auth.certfile",
-            })?
+            .client
+            .auth
             .cert_file
             .clone()
-            // TODO (Marcel): remove unnecessary Options once tedge_config is
-            // refactored
             .ok_or(ConfigSettingError::ConfigNotSet {
-                key: "mqtt.client.auth.certfile",
+                key: "mqtt.client.auth.cert_file",
             })
     }
 
@@ -550,24 +553,14 @@ impl ConfigSettingAccessor<MqttClientAuthCertSetting> for TEdgeConfig {
         _setting: MqttClientAuthCertSetting,
         cafile: Utf8PathBuf,
     ) -> ConfigSettingResult<()> {
-        let client_auth_config = self
-            .data
-            .mqtt
-            .client_auth
-            .get_or_insert(MqttClientAuthConfig {
-                cert_file: None,
-                key_file: None,
-            });
-
-        client_auth_config.cert_file = Some(cafile);
+        self.data.mqtt.client.auth.cert_file = Some(cafile);
 
         Ok(())
     }
 
     fn unset(&mut self, _setting: MqttClientAuthCertSetting) -> ConfigSettingResult<()> {
-        if let Some(client_auth) = self.data.mqtt.client_auth.as_mut() {
-            client_auth.cert_file = None;
-        }
+        self.data.mqtt.client.auth.cert_file = None;
+
         Ok(())
     }
 }
@@ -576,43 +569,28 @@ impl ConfigSettingAccessor<MqttClientAuthKeySetting> for TEdgeConfig {
     fn query(&self, _setting: MqttClientAuthKeySetting) -> ConfigSettingResult<Utf8PathBuf> {
         self.data
             .mqtt
-            .client_auth
-            .as_ref()
-            .ok_or(ConfigSettingError::ConfigNotSet {
-                key: "mqtt.client.auth.keyfile",
-            })?
+            .client
+            .auth
             .key_file
             .clone()
-            // TODO (Marcel): remove unnecessary Options once tedge_config is
-            // refactored
             .ok_or(ConfigSettingError::ConfigNotSet {
-                key: "mqtt.client.auth.keyfile",
+                key: "mqtt.client.auth.key_file",
             })
     }
 
     fn update(
         &mut self,
         _setting: MqttClientAuthKeySetting,
-        cafile: Utf8PathBuf,
+        key_file: Utf8PathBuf,
     ) -> ConfigSettingResult<()> {
-        let client_auth_config = self
-            .data
-            .mqtt
-            .client_auth
-            .get_or_insert(MqttClientAuthConfig {
-                cert_file: None,
-                key_file: None,
-            });
-
-        client_auth_config.key_file = Some(cafile);
+        self.data.mqtt.client.auth.key_file = Some(key_file);
 
         Ok(())
     }
 
     fn unset(&mut self, _setting: MqttClientAuthKeySetting) -> ConfigSettingResult<()> {
-        if let Some(client_auth) = self.data.mqtt.client_auth.as_mut() {
-            client_auth.key_file = None;
-        }
+        self.data.mqtt.client.auth.key_file = None;
+
         Ok(())
     }
 }
@@ -622,18 +600,23 @@ impl ConfigSettingAccessor<MqttPortSetting> for TEdgeConfig {
         Ok(self
             .data
             .mqtt
+            .bind
             .port
+            .map(u16::from)
             .map(Port)
             .unwrap_or_else(|| self.config_defaults.default_mqtt_port))
     }
 
     fn update(&mut self, _setting: MqttPortSetting, value: Port) -> ConfigSettingResult<()> {
-        self.data.mqtt.port = Some(value.into());
+        self.data.mqtt.bind.port =
+            Some(NonZeroU16::new(value.0).ok_or(ConfigSettingError::Other {
+                msg: "mqtt.bind.port cannot be set to 0",
+            })?);
         Ok(())
     }
 
     fn unset(&mut self, _setting: MqttPortSetting) -> ConfigSettingResult<()> {
-        self.data.mqtt.port = None;
+        self.data.mqtt.bind.port = None;
         Ok(())
     }
 }
@@ -643,18 +626,19 @@ impl ConfigSettingAccessor<HttpPortSetting> for TEdgeConfig {
         Ok(self
             .data
             .http
+            .bind
             .port
             .map(Port)
             .unwrap_or_else(|| self.config_defaults.default_http_port))
     }
 
     fn update(&mut self, _setting: HttpPortSetting, value: Port) -> ConfigSettingResult<()> {
-        self.data.http.port = Some(value.into());
+        self.data.http.bind.port = Some(value.into());
         Ok(())
     }
 
     fn unset(&mut self, _setting: HttpPortSetting) -> ConfigSettingResult<()> {
-        self.data.http.port = None;
+        self.data.http.bind.port = None;
         Ok(())
     }
 }
@@ -690,8 +674,9 @@ impl ConfigSettingAccessor<MqttBindAddressSetting> for TEdgeConfig {
         Ok(self
             .data
             .mqtt
-            .bind_address
-            .clone()
+            .bind
+            .address
+            .map(IpAddress)
             .unwrap_or_else(|| self.config_defaults.default_mqtt_bind_address.clone()))
     }
 
@@ -700,12 +685,12 @@ impl ConfigSettingAccessor<MqttBindAddressSetting> for TEdgeConfig {
         _setting: MqttBindAddressSetting,
         value: IpAddress,
     ) -> ConfigSettingResult<()> {
-        self.data.mqtt.bind_address = Some(value);
+        self.data.mqtt.bind.address = Some(value.0);
         Ok(())
     }
 
     fn unset(&mut self, _setting: MqttBindAddressSetting) -> ConfigSettingResult<()> {
-        self.data.mqtt.bind_address = None;
+        self.data.mqtt.bind.address = None;
         Ok(())
     }
 }
@@ -714,7 +699,9 @@ impl ConfigSettingAccessor<MqttExternalPortSetting> for TEdgeConfig {
     fn query(&self, _setting: MqttExternalPortSetting) -> ConfigSettingResult<Port> {
         self.data
             .mqtt
-            .external_port
+            .external
+            .bind
+            .port
             .map(Port)
             .ok_or(ConfigSettingError::ConfigNotSet {
                 key: MqttExternalPortSetting::KEY,
@@ -726,25 +713,23 @@ impl ConfigSettingAccessor<MqttExternalPortSetting> for TEdgeConfig {
         _setting: MqttExternalPortSetting,
         value: Port,
     ) -> ConfigSettingResult<()> {
-        self.data.mqtt.external_port = Some(value.into());
+        self.data.mqtt.external.bind.port = Some(value.into());
         Ok(())
     }
 
     fn unset(&mut self, _setting: MqttExternalPortSetting) -> ConfigSettingResult<()> {
-        self.data.mqtt.external_port = None;
+        self.data.mqtt.external.bind.port = None;
         Ok(())
     }
 }
 
 impl ConfigSettingAccessor<MqttExternalBindAddressSetting> for TEdgeConfig {
     fn query(&self, _setting: MqttExternalBindAddressSetting) -> ConfigSettingResult<IpAddress> {
-        self.data
-            .mqtt
-            .external_bind_address
-            .clone()
-            .ok_or(ConfigSettingError::ConfigNotSet {
+        self.data.mqtt.external.bind.address.map(IpAddress).ok_or(
+            ConfigSettingError::ConfigNotSet {
                 key: MqttExternalBindAddressSetting::KEY,
-            })
+            },
+        )
     }
 
     fn update(
@@ -752,12 +737,12 @@ impl ConfigSettingAccessor<MqttExternalBindAddressSetting> for TEdgeConfig {
         _setting: MqttExternalBindAddressSetting,
         value: IpAddress,
     ) -> ConfigSettingResult<()> {
-        self.data.mqtt.external_bind_address = Some(value);
+        self.data.mqtt.external.bind.address = Some(value.0);
         Ok(())
     }
 
     fn unset(&mut self, _setting: MqttExternalBindAddressSetting) -> ConfigSettingResult<()> {
-        self.data.mqtt.external_bind_address = None;
+        self.data.mqtt.external.bind.address = None;
         Ok(())
     }
 }
@@ -766,7 +751,9 @@ impl ConfigSettingAccessor<MqttExternalBindInterfaceSetting> for TEdgeConfig {
     fn query(&self, _setting: MqttExternalBindInterfaceSetting) -> ConfigSettingResult<String> {
         self.data
             .mqtt
-            .external_bind_interface
+            .external
+            .bind
+            .interface
             .clone()
             .ok_or(ConfigSettingError::ConfigNotSet {
                 key: MqttExternalBindInterfaceSetting::KEY,
@@ -778,12 +765,12 @@ impl ConfigSettingAccessor<MqttExternalBindInterfaceSetting> for TEdgeConfig {
         _setting: MqttExternalBindInterfaceSetting,
         value: String,
     ) -> ConfigSettingResult<()> {
-        self.data.mqtt.external_bind_interface = Some(value);
+        self.data.mqtt.external.bind.interface = Some(value);
         Ok(())
     }
 
     fn unset(&mut self, _setting: MqttExternalBindInterfaceSetting) -> ConfigSettingResult<()> {
-        self.data.mqtt.external_bind_interface = None;
+        self.data.mqtt.external.bind.interface = None;
         Ok(())
     }
 }
@@ -792,7 +779,8 @@ impl ConfigSettingAccessor<MqttExternalCAPathSetting> for TEdgeConfig {
     fn query(&self, _setting: MqttExternalCAPathSetting) -> ConfigSettingResult<Utf8PathBuf> {
         self.data
             .mqtt
-            .external_capath
+            .external
+            .ca_path
             .clone()
             .ok_or(ConfigSettingError::ConfigNotSet {
                 key: MqttExternalCAPathSetting::KEY,
@@ -804,12 +792,12 @@ impl ConfigSettingAccessor<MqttExternalCAPathSetting> for TEdgeConfig {
         _setting: MqttExternalCAPathSetting,
         value: Utf8PathBuf,
     ) -> ConfigSettingResult<()> {
-        self.data.mqtt.external_capath = Some(value);
+        self.data.mqtt.external.ca_path = Some(value);
         Ok(())
     }
 
     fn unset(&mut self, _setting: MqttExternalCAPathSetting) -> ConfigSettingResult<()> {
-        self.data.mqtt.external_capath = None;
+        self.data.mqtt.external.ca_path = None;
         Ok(())
     }
 }
@@ -818,7 +806,8 @@ impl ConfigSettingAccessor<MqttExternalCertfileSetting> for TEdgeConfig {
     fn query(&self, _setting: MqttExternalCertfileSetting) -> ConfigSettingResult<Utf8PathBuf> {
         self.data
             .mqtt
-            .external_certfile
+            .external
+            .cert_file
             .clone()
             .ok_or(ConfigSettingError::ConfigNotSet {
                 key: MqttExternalCertfileSetting::KEY,
@@ -830,12 +819,12 @@ impl ConfigSettingAccessor<MqttExternalCertfileSetting> for TEdgeConfig {
         _setting: MqttExternalCertfileSetting,
         value: Utf8PathBuf,
     ) -> ConfigSettingResult<()> {
-        self.data.mqtt.external_certfile = Some(value);
+        self.data.mqtt.external.cert_file = Some(value);
         Ok(())
     }
 
     fn unset(&mut self, _setting: MqttExternalCertfileSetting) -> ConfigSettingResult<()> {
-        self.data.mqtt.external_certfile = None;
+        self.data.mqtt.external.cert_file = None;
         Ok(())
     }
 }
@@ -844,7 +833,8 @@ impl ConfigSettingAccessor<MqttExternalKeyfileSetting> for TEdgeConfig {
     fn query(&self, _setting: MqttExternalKeyfileSetting) -> ConfigSettingResult<Utf8PathBuf> {
         self.data
             .mqtt
-            .external_keyfile
+            .external
+            .key_file
             .clone()
             .ok_or(ConfigSettingError::ConfigNotSet {
                 key: MqttExternalKeyfileSetting::KEY,
@@ -856,12 +846,12 @@ impl ConfigSettingAccessor<MqttExternalKeyfileSetting> for TEdgeConfig {
         _setting: MqttExternalKeyfileSetting,
         value: Utf8PathBuf,
     ) -> ConfigSettingResult<()> {
-        self.data.mqtt.external_keyfile = Some(value);
+        self.data.mqtt.external.key_file = Some(value);
         Ok(())
     }
 
     fn unset(&mut self, _setting: MqttExternalKeyfileSetting) -> ConfigSettingResult<()> {
-        self.data.mqtt.external_keyfile = None;
+        self.data.mqtt.external.key_file = None;
         Ok(())
     }
 }
@@ -870,7 +860,8 @@ impl ConfigSettingAccessor<SoftwarePluginDefaultSetting> for TEdgeConfig {
     fn query(&self, _setting: SoftwarePluginDefaultSetting) -> ConfigSettingResult<String> {
         self.data
             .software
-            .default_plugin_type
+            .plugin
+            .default
             .clone()
             .ok_or(ConfigSettingError::ConfigNotSet {
                 key: SoftwarePluginDefaultSetting::KEY,
@@ -882,12 +873,12 @@ impl ConfigSettingAccessor<SoftwarePluginDefaultSetting> for TEdgeConfig {
         _setting: SoftwarePluginDefaultSetting,
         value: String,
     ) -> ConfigSettingResult<()> {
-        self.data.software.default_plugin_type = Some(value);
+        self.data.software.plugin.default = Some(value);
         Ok(())
     }
 
     fn unset(&mut self, _setting: SoftwarePluginDefaultSetting) -> ConfigSettingResult<()> {
-        self.data.software.default_plugin_type = None;
+        self.data.software.plugin.default = None;
         Ok(())
     }
 }
@@ -919,18 +910,18 @@ impl ConfigSettingAccessor<TmpPathSetting> for TEdgeConfig {
         Ok(self
             .data
             .tmp
-            .dir_path
+            .path
             .clone()
             .unwrap_or_else(|| self.config_defaults.default_tmp_path.clone()))
     }
 
     fn update(&mut self, _setting: TmpPathSetting, value: Utf8PathBuf) -> ConfigSettingResult<()> {
-        self.data.tmp.dir_path = Some(value);
+        self.data.tmp.path = Some(value);
         Ok(())
     }
 
     fn unset(&mut self, _setting: TmpPathSetting) -> ConfigSettingResult<()> {
-        self.data.tmp.dir_path = None;
+        self.data.tmp.path = None;
         Ok(())
     }
 }
@@ -940,18 +931,18 @@ impl ConfigSettingAccessor<LogPathSetting> for TEdgeConfig {
         Ok(self
             .data
             .logs
-            .dir_path
+            .path
             .clone()
             .unwrap_or_else(|| self.config_defaults.default_logs_path.clone()))
     }
 
     fn update(&mut self, _setting: LogPathSetting, value: Utf8PathBuf) -> ConfigSettingResult<()> {
-        self.data.logs.dir_path = Some(value);
+        self.data.logs.path = Some(value);
         Ok(())
     }
 
     fn unset(&mut self, _setting: LogPathSetting) -> ConfigSettingResult<()> {
-        self.data.logs.dir_path = None;
+        self.data.logs.path = None;
         Ok(())
     }
 }
@@ -961,18 +952,18 @@ impl ConfigSettingAccessor<RunPathSetting> for TEdgeConfig {
         Ok(self
             .data
             .run
-            .dir_path
+            .path
             .clone()
             .unwrap_or_else(|| self.config_defaults.default_run_path.clone()))
     }
 
     fn update(&mut self, _setting: RunPathSetting, value: Utf8PathBuf) -> ConfigSettingResult<()> {
-        self.data.run.dir_path = Some(value);
+        self.data.run.path = Some(value);
         Ok(())
     }
 
     fn unset(&mut self, _setting: RunPathSetting) -> ConfigSettingResult<()> {
-        self.data.run.dir_path = None;
+        self.data.run.path = None;
         Ok(())
     }
 }
@@ -982,18 +973,18 @@ impl ConfigSettingAccessor<DataPathSetting> for TEdgeConfig {
         Ok(self
             .data
             .data
-            .dir_path
+            .path
             .clone()
             .unwrap_or_else(|| self.config_defaults.default_data_path.clone()))
     }
 
     fn update(&mut self, _setting: DataPathSetting, value: Utf8PathBuf) -> ConfigSettingResult<()> {
-        self.data.data.dir_path = Some(value);
+        self.data.data.path = Some(value);
         Ok(())
     }
 
     fn unset(&mut self, _setting: DataPathSetting) -> ConfigSettingResult<()> {
-        self.data.data.dir_path = None;
+        self.data.data.path = None;
         Ok(())
     }
 }
@@ -1024,8 +1015,10 @@ impl ConfigSettingAccessor<FirmwareChildUpdateTimeoutSetting> for TEdgeConfig {
         Ok(self
             .data
             .firmware
-            .child_update_timeout
-            .map(Seconds)
+            .child
+            .update
+            .timeout
+            .map(|s| Seconds(s.duration().as_secs()))
             .unwrap_or(self.config_defaults.default_firmware_child_update_timeout))
     }
 
@@ -1034,16 +1027,16 @@ impl ConfigSettingAccessor<FirmwareChildUpdateTimeoutSetting> for TEdgeConfig {
         _setting: FirmwareChildUpdateTimeoutSetting,
         value: Seconds,
     ) -> ConfigSettingResult<()> {
-        self.data.firmware.child_update_timeout = Some(value.into());
+        self.data.firmware.child.update.timeout = Some(Seconds(value.into()));
         Ok(())
     }
 
     fn unset(&mut self, _setting: FirmwareChildUpdateTimeoutSetting) -> ConfigSettingResult<()> {
-        self.data.firmware.child_update_timeout = Some(
+        self.data.firmware.child.update.timeout = Some(Seconds(
             self.config_defaults
                 .default_firmware_child_update_timeout
                 .into(),
-        );
+        ));
         Ok(())
     }
 }
@@ -1053,18 +1046,18 @@ impl ConfigSettingAccessor<ServiceTypeSetting> for TEdgeConfig {
         Ok(self
             .data
             .service
-            .service_type
+            .ty
             .clone()
             .unwrap_or_else(|| self.config_defaults.default_service_type.clone()))
     }
 
     fn update(&mut self, _setting: ServiceTypeSetting, value: String) -> ConfigSettingResult<()> {
-        self.data.service.service_type = Some(value);
+        self.data.service.ty = Some(value);
         Ok(())
     }
 
     fn unset(&mut self, _setting: ServiceTypeSetting) -> ConfigSettingResult<()> {
-        self.data.service.service_type = Some(self.config_defaults.default_service_type.clone());
+        self.data.service.ty = Some(self.config_defaults.default_service_type.clone());
         Ok(())
     }
 }
diff --git a/crates/common/tedge_config/src/tedge_config_cli/tedge_config_dto.rs b/crates/common/tedge_config/src/tedge_config_cli/tedge_config_dto.rs
deleted file mode 100644
index 5bcc83cb716..00000000000
--- a/crates/common/tedge_config/src/tedge_config_cli/tedge_config_dto.rs
+++ /dev/null
@@ -1,337 +0,0 @@
-//! Crate-private plain-old data-type used for serialization.
-
-use std::num::NonZeroU16;
-use std::path::PathBuf;
-
-use crate::*;
-use camino::Utf8PathBuf;
-use doku::Document;
-use serde::Deserialize;
-use serde::Serialize;
-
-#[derive(Debug, Default, Deserialize, Serialize, Document)]
-pub struct TEdgeConfigDto {
-    /// Captures the device specific configurations
-    #[serde(default)]
-    pub(crate) device: DeviceConfigDto,
-
-    /// Captures the configurations required to connect to Cumulocity
-    #[serde(default)]
-    pub(crate) c8y: CumulocityConfigDto,
-
-    #[serde(default, alias = "azure")] // for version 0.1.0 compatibility
-    pub(crate) az: AzureConfigDto,
-
-    #[serde(default)]
-    pub(crate) aws: AwsConfigDto,
-
-    #[serde(default)]
-    pub(crate) mqtt: MqttConfigDto,
-
-    #[serde(default)]
-    pub(crate) http: HttpConfigDto,
-
-    #[serde(default)]
-    pub(crate) software: SoftwareConfigDto,
-
-    #[serde(default)]
-    pub(crate) tmp: PathConfigDto,
-
-    #[serde(default)]
-    pub(crate) logs: PathConfigDto,
-
-    #[serde(default)]
-    pub(crate) run: PathConfigDto,
-
-    #[serde(default)]
-    pub(crate) data: PathConfigDto,
-
-    #[serde(default)]
-    pub(crate) firmware: FirmwareConfigDto,
-
-    #[serde(default)]
-    pub(crate) service: ServiceTypeConfigDto,
-}
-
-/// Represents the device specific configurations defined in the [device]
-/// section of the thin edge configuration TOML file
-#[derive(Debug, Default, Deserialize, Serialize, Document)]
-pub struct DeviceConfigDto {
-    /// Path where the device's private key is stored
-    #[doku(example = "/etc/tedge/device-certs/tedge-private-key.pem")]
-    #[doku(as = "PathBuf")]
-    pub(crate) key_path: Option<Utf8PathBuf>,
-
-    /// Path where the device's certificate is stored
-    #[doku(example = "/etc/tedge/device-certs/tedge-certificate.pem")]
-    #[doku(as = "PathBuf")]
-    pub(crate) cert_path: Option<Utf8PathBuf>,
-
-    /// The default device type
-    #[serde(rename = "type")]
-    #[doku(example = "thin-edge.io")]
-    pub(crate) device_type: Option<String>,
-}
-
-/// Represents the Cumulocity specific configurations defined in the
-/// [c8y] section of the thin edge configuration TOML file
-#[derive(Debug, Default, Deserialize, Serialize, Document)]
-pub(crate) struct CumulocityConfigDto {
-    /// Endpoint URL of the Cumulocity tenant
-    #[doku(example = "your-tenant.cumulocity.com", as = "String")]
-    pub(crate) url: Option<ConnectUrl>,
-
-    /// HTTP Endpoint for the Cumulocity tenant, with optional port.
-    #[doku(example = "http.your-tenant.cumulocity.com:1234", as = "String")]
-    pub(crate) http: Option<HostPort<HTTPS_PORT>>,
-
-    /// MQTT Endpoint for the Cumulocity tenant, with optional port.
-    #[doku(example = "mqtt.your-tenant.cumulocity.com:1234", as = "String")]
-    pub(crate) mqtt: Option<HostPort<MQTT_TLS_PORT>>,
-
-    /// The path where Cumulocity root certificate(s) are stored. The value can
-    /// be a directory path as well as the path of the direct certificate file.
-    #[doku(example = "/etc/tedge/c8y-trusted-root-certificates.pem")]
-    #[doku(as = "PathBuf")]
-    pub(crate) root_cert_path: Option<Utf8PathBuf>,
-
-    /// Set of c8y template IDs used for subscriptions
-    #[doku(literal_example = "templateId1,templateId2", as = "String")]
-    pub(crate) smartrest_templates: Option<TemplatesSet>,
-}
-
-#[derive(Debug, Default, Deserialize, Serialize, Document)]
-pub(crate) struct AzureConfigDto {
-    /// Endpoint URL of Azure IoT tenant
-    #[doku(example = "myazure.azure-devices.net", as = "String")]
-    pub(crate) url: Option<ConnectUrl>,
-
-    /// The path where Azure root certificate(s) are stored
-    #[doku(example = "/etc/tedge/az-trusted-root-certificates.pem")]
-    #[doku(as = "PathBuf")]
-    pub(crate) root_cert_path: Option<Utf8PathBuf>,
-
-    /// Whether Azure mapper should add timestamp or not
-    #[doku(example = "true")]
-    pub(crate) mapper_timestamp: Option<bool>,
-}
-
-#[derive(Debug, Default, Deserialize, Serialize, Document)]
-pub(crate) struct AwsConfigDto {
-    /// Endpoint URL of AWS instance
-    #[doku(example = "your-endpoint.amazonaws.com", as = "String")]
-    pub(crate) url: Option<ConnectUrl>,
-
-    /// The path where AWS root certificate(s) are stored
-    #[doku(example = "/etc/tedge/aws-trusted-root-certificates.pem")]
-    #[doku(as = "PathBuf")]
-    pub(crate) root_cert_path: Option<Utf8PathBuf>,
-
-    /// Whether Azure mapper should add timestamp or not
-    #[doku(example = "true")]
-    pub(crate) mapper_timestamp: Option<bool>,
-}
-
-#[derive(Debug, Default, Deserialize, Serialize, Document)]
-pub(crate) struct MqttConfigDto {
-    /// The address mosquitto binds to for internal use
-    pub(crate) bind_address: Option<IpAddress>,
-    // TODO example
-    /// The port mosquitto binds to for internal use
-    pub(crate) port: Option<u16>,
-
-    pub(crate) client_host: Option<String>,
-
-    /// Mqtt broker port, which is used by the mqtt clients to publish or
-    /// subscribe
-    #[doku(example = "1883", as = "u16")]
-    // When connecting to a host, port 0 is invalid. When binding, however, port
-    // 0 is accepted and understood by the system to dynamically assign any free
-    // port to the process. The process then needs to take notice of what port
-    // it received, which I'm not sure if we're doing.
-    //
-    // If we don't want to allow binding to port 0, then we can also use
-    // `NonZeroU16` there as well, which because it can never be 0, can make the
-    // `Option` completely free, because Option can use 0x0000 value for the
-    // `None` variant.
-    pub(crate) client_port: Option<NonZeroU16>,
-
-    /// Path to the trusted CA certificate file used by MQTT clients when
-    /// authenticating the MQTT broker.
-    #[doku(example = "/etc/mosquitto/ca_certificates/ca.crt", as = "PathBuf")]
-    pub(crate) client_ca_file: Option<Utf8PathBuf>,
-
-    /// Path to the directory containing trusted CA certificates used by MQTT
-    /// clients when authenticating the MQTT broker.
-    #[doku(example = "/etc/mosquitto/ca_certificates", as = "PathBuf")]
-    pub(crate) client_ca_path: Option<Utf8PathBuf>,
-
-    /// MQTT client authentication configuration, containing a path to a client
-    /// certificate and a private key.
-    pub(crate) client_auth: Option<MqttClientAuthConfig>,
-
-    /// The port mosquitto binds to for external use
-    #[doku(example = "1883")]
-    pub(crate) external_port: Option<u16>,
-
-    /// The address mosquitto binds to for external use
-    #[doku(example = "0.0.0.0")]
-    pub(crate) external_bind_address: Option<IpAddress>,
-
-    /// The interface mosquitto listens on for external use
-    #[doku(example = "wlan0")]
-    pub(crate) external_bind_interface: Option<String>,
-
-    // All the paths relating to mosquitto are strings as they need to be safe
-    // to write to a configuration file (i.e. probably valid utf-8 at the least)
-    /// Path to a file containing the PEM encoded CA certificates that are
-    /// trusted when checking incoming client certificates
-    #[doku(example = "/etc/ssl/certs", as = "PathBuf")]
-    pub(crate) external_capath: Option<Utf8PathBuf>,
-
-    /// Path to the certificate file which is used by the external MQTT listener
-    #[doku(
-        example = "/etc/tedge/device-certs/tedge-certificate.pem",
-        as = "PathBuf"
-    )]
-    pub(crate) external_certfile: Option<Utf8PathBuf>,
-
-    /// Path to the key file which is used by the external MQTT listener
-    #[doku(example = "/etc/tedge/device-certs/tedge-private-key.pem")]
-    #[doku(as = "PathBuf")]
-    pub(crate) external_keyfile: Option<Utf8PathBuf>,
-}
-
-#[derive(Debug, Default, Deserialize, Serialize, Document)]
-pub(crate) struct HttpConfigDto {
-    /// HTTP server port used by the File Transfer Service
-    #[doku(example = "8000")]
-    #[serde(alias = "bind_port")]
-    pub(crate) port: Option<u16>,
-
-    /// HTTP bind address used by the File Transfer service
-    #[doku(example = "127.0.0.1")]
-    #[doku(example = "192.168.1.2")]
-    pub(crate) bind_address: Option<IpAddress>,
-}
-
-#[derive(Debug, Default, Deserialize, Serialize, Document)]
-pub struct SoftwareConfigDto {
-    pub(crate) default_plugin_type: Option<String>,
-}
-
-#[derive(Debug, Default, Deserialize, Serialize, Document)]
-pub struct PathConfigDto {
-    #[serde(rename = "path")]
-    #[doku(as = "PathBuf")]
-    pub(crate) dir_path: Option<Utf8PathBuf>,
-
-    /// Whether create lock file or not
-    pub(crate) lock_files: Option<bool>,
-}
-
-#[derive(Debug, Default, Deserialize, Serialize, Document)]
-pub struct FirmwareConfigDto {
-    pub(crate) child_update_timeout: Option<u64>,
-}
-
-#[derive(Debug, Default, Deserialize, Serialize, Document)]
-pub struct ServiceTypeConfigDto {
-    #[serde(rename = "type")]
-    pub(crate) service_type: Option<String>,
-}
-
-/// Contains MQTT client authentication configuration.
-///
-// Despite both cert_file and key_file being required for client authentication,
-// fields in this struct are optional because `tedge config set` needs to
-// successfully parse the configuration, update it in memory, and then save
-// deserialized object. If the upcoming configuration refactor discussed in [1]
-// ends up supporting partial updates to such objects, then these fields could
-// be made non-optional.
-//
-// [1]: https://github.com/thin-edge/thin-edge.io/issues/1812
-#[derive(Debug, Default, Deserialize, Serialize, Document)]
-pub(crate) struct MqttClientAuthConfig {
-    /// Path to the client certificate
-    #[doku(example = "/path/to/client.crt", as = "PathBuf")]
-    pub cert_file: Option<Utf8PathBuf>,
-
-    /// Path to the client private key
-    #[doku(example = "/path/to/client.key", as = "PathBuf")]
-    pub key_file: Option<Utf8PathBuf>,
-}
-
-#[cfg(test)]
-mod tests {
-    use core::panic;
-    use std::borrow::Cow;
-
-    use figment::providers::Format;
-    use serde::de::DeserializeOwned;
-
-    use super::*;
-
-    #[test]
-    fn example_values_can_be_deserialised() {
-        let ty = TEdgeConfigDto::ty();
-        let doku::TypeKind::Struct { fields, transparent: false } = ty.kind else { panic!("Expected struct but got {:?}", ty.kind) };
-        let doku::Fields::Named { fields } = fields else { panic!("Expected named fields but got {:?}", fields)};
-        for (key, ty) in struct_field_paths(None, &fields) {
-            verify_examples_for::<TEdgeConfigDto>(&key, ty)
-        }
-    }
-
-    fn verify_examples_for<Dto>(key: &str, ty: doku::Type)
-    where
-        Dto: Default + Serialize + DeserializeOwned,
-    {
-        for example in ty.example.iter().flat_map(|e| e.iter()) {
-            println!("Testing {key}={example}");
-            figment::Jail::expect_with(|jail| {
-                jail.set_env(key, example);
-                let figment = figment::Figment::new()
-                    .merge(figment::providers::Toml::string(
-                        &toml::to_string(&Dto::default()).unwrap(),
-                    ))
-                    .merge(figment::providers::Env::raw().split("."));
-
-                figment.extract::<Dto>().unwrap_or_else(|_| {
-                    panic!("\n\nFailed to deserialize example data: {key}={example}\n\n")
-                });
-
-                Ok(())
-            });
-        }
-    }
-
-    fn key_name(prefix: Option<&str>, name: &'static str) -> Cow<'static, str> {
-        match prefix {
-            Some(prefix) => Cow::Owned(format!("{prefix}.{name}")),
-            None => Cow::Borrowed(name),
-        }
-    }
-
-    fn struct_field_paths(
-        prefix: Option<&str>,
-        fields: &[(&'static str, doku::Field)],
-    ) -> Vec<(Cow<'static, str>, doku::Type)> {
-        fields
-            .iter()
-            .flat_map(|(name, field)| match named_fields(&field.ty.kind) {
-                Some(fields) => struct_field_paths(Some(&key_name(prefix, name)), fields),
-                None => vec![(key_name(prefix, name), field.ty.clone())],
-            })
-            .collect()
-    }
-
-    fn named_fields(kind: &doku::TypeKind) -> Option<&[(&'static str, doku::Field)]> {
-        match kind {
-            doku::TypeKind::Struct {
-                fields: doku::Fields::Named { fields },
-                transparent: false,
-            } => Some(fields),
-            _ => None,
-        }
-    }
-}
diff --git a/crates/common/tedge_config/src/tedge_config_cli/tedge_config_repository.rs b/crates/common/tedge_config/src/tedge_config_cli/tedge_config_repository.rs
index aa75221a4a5..0690a232b4c 100644
--- a/crates/common/tedge_config/src/tedge_config_cli/tedge_config_repository.rs
+++ b/crates/common/tedge_config/src/tedge_config_cli/tedge_config_repository.rs
@@ -1,11 +1,14 @@
 use crate::*;
+use camino::Utf8Path;
+use serde::Serialize;
 use std::fs;
-use std::path::PathBuf;
 use tedge_utils::fs::atomically_write_file_sync;
 
 use super::figment::ConfigSources;
 use super::figment::FileAndEnvironment;
 use super::figment::FileOnly;
+use super::figment::UnusedValueWarnings;
+use super::new;
 
 /// TEdgeConfigRepository is responsible for loading and storing TEdgeConfig entities.
 #[derive(Debug, Clone)]
@@ -27,9 +30,8 @@ impl ConfigRepository<TEdgeConfig> for TEdgeConfigRepository {
     type Error = TEdgeConfigError;
 
     fn load(&self) -> Result<TEdgeConfig, TEdgeConfigError> {
-        let config = self.read_file_or_default::<FileAndEnvironment>(
-            self.config_location.tedge_config_file_path().into(),
-        )?;
+        let config =
+            self.make_tedge_config(self.load_dto::<FileAndEnvironment>(self.toml_path())?)?;
         Ok(config)
     }
 
@@ -37,16 +39,28 @@ impl ConfigRepository<TEdgeConfig> for TEdgeConfigRepository {
         &self,
         update: &impl Fn(&mut TEdgeConfig) -> ConfigSettingResult<()>,
     ) -> Result<(), Self::Error> {
-        let mut config = self.read_file_or_default::<FileOnly>(
-            self.config_location.tedge_config_file_path().into(),
-        )?;
+        let mut config = self.read_file_or_default::<FileOnly>(self.toml_path())?;
         update(&mut config)?;
 
-        self.store(&config)
+        self.store(&config.data)
     }
 }
 
 impl TEdgeConfigRepository {
+    pub fn update_toml_new(
+        &self,
+        update: &impl Fn(&mut new::TEdgeConfigDto) -> ConfigSettingResult<()>,
+    ) -> Result<(), TEdgeConfigError> {
+        let mut config = self.load_dto::<FileOnly>(self.toml_path())?;
+        update(&mut config)?;
+
+        self.store(&config)
+    }
+
+    fn toml_path(&self) -> &Utf8Path {
+        self.config_location.tedge_config_file_path()
+    }
+
     pub fn new(config_location: TEdgeConfigLocation) -> Self {
         let config_defaults = TEdgeConfigDefaults::from(&config_location);
         Self::new_with_defaults(config_location, config_defaults)
@@ -62,20 +76,67 @@ impl TEdgeConfigRepository {
         }
     }
 
+    pub fn load_new(&self) -> Result<new::TEdgeConfig, TEdgeConfigError> {
+        let dto = self.load_dto::<FileAndEnvironment>(self.toml_path())?;
+        Ok(new::TEdgeConfig::from_dto(&dto, &self.config_location))
+    }
+
+    fn load_dto<Sources: ConfigSources>(
+        &self,
+        path: &Utf8Path,
+    ) -> Result<new::TEdgeConfigDto, TEdgeConfigError> {
+        let (dto, warnings) = self.load_dto_with_warnings::<Sources>(path)?;
+
+        warnings.emit();
+
+        Ok(dto)
+    }
+
+    fn load_dto_with_warnings<Sources: ConfigSources>(
+        &self,
+        path: &Utf8Path,
+    ) -> Result<(new::TEdgeConfigDto, UnusedValueWarnings), TEdgeConfigError> {
+        let (mut dto, mut warnings): (new::TEdgeConfigDto, _) =
+            super::figment::extract_data::<_, Sources>(path)?;
+
+        if let Some(migrations) = dto.config.version.unwrap_or_default().migrations() {
+            'migrate_toml: {
+                let Ok(config) = std::fs::read_to_string(self.toml_path()) else { break 'migrate_toml };
+
+                tracing::info!("Migrating tedge.toml configuration to version 2");
+
+                let toml = toml::de::from_str(&config)?;
+                let migrated_toml = migrations
+                    .into_iter()
+                    .fold(toml, |toml, migration| migration.apply_to(toml));
+
+                self.store(&migrated_toml)?;
+
+                // Reload DTO to get the settings in the right place
+                (dto, warnings) = super::figment::extract_data::<_, Sources>(self.toml_path())?;
+            }
+        }
+
+        Ok((dto, warnings))
+    }
+
     pub fn get_config_location(&self) -> &TEdgeConfigLocation {
         &self.config_location
     }
 
     fn read_file_or_default<Sources: ConfigSources>(
         &self,
-        path: PathBuf,
+        path: &Utf8Path,
     ) -> Result<TEdgeConfig, TEdgeConfigError> {
-        let data: TEdgeConfigDto = super::figment::extract_data::<_, Sources>(path)?;
+        let dto = self.load_dto::<Sources>(path)?;
 
-        self.make_tedge_config(data)
+        self.make_tedge_config(dto)
     }
 
-    fn make_tedge_config(&self, data: TEdgeConfigDto) -> Result<TEdgeConfig, TEdgeConfigError> {
+    fn make_tedge_config(
+        &self,
+        data: new::TEdgeConfigDto,
+    ) -> Result<TEdgeConfig, TEdgeConfigError> {
         Ok(TEdgeConfig {
             data,
             config_defaults: self.config_defaults.clone(),
@@ -83,18 +144,119 @@ impl TEdgeConfigRepository {
     }
 
     // TODO: Explicitly set the file permissions in this function and file ownership!
-    fn store(&self, config: &TEdgeConfig) -> Result<(), TEdgeConfigError> {
-        let toml = toml::to_string_pretty(&config.data)?;
+    fn store<S: Serialize>(&self, config: &S) -> Result<(), TEdgeConfigError> {
+        let toml = toml::to_string_pretty(&config)?;
 
         // Create `$HOME/.tedge` or `/etc/tedge` directory in case it does not exist yet
         if !self.config_location.tedge_config_root_path.exists() {
             fs::create_dir(self.config_location.tedge_config_root_path())?;
         }
 
-        atomically_write_file_sync(
-            self.config_location.tedge_config_file_path(),
-            toml.as_bytes(),
-        )?;
+        atomically_write_file_sync(self.toml_path(), toml.as_bytes())?;
         Ok(())
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use tedge_test_utils::fs::TempTedgeDir;
+
+    use crate::new::TEdgeConfigReader;
+
+    use super::*;
+
+    #[test]
+    fn old_toml_can_be_read_in_its_entirety() {
+        let toml = r#"[device]
+key_path = "/tedge/device-key.pem"
+cert_path = "/tedge/device-cert.pem"
+type = "a-device"
+
+[c8y]
+url = "something.latest.stage.c8y.io"
+root_cert_path = "/c8y/root-cert.pem"
+smartrest_templates = [
+    "id1",
+    "id2",
+]
+
+[az]
+url = "something.azure.com"
+root_cert_path = "/az/root-cert.pem"
+mapper_timestamp = true
+
+[aws]
+url = "something.amazonaws.com"
+root_cert_path = "/aws/root-cert.pem"
+mapper_timestamp = false
+
+[mqtt]
+bind_address = "192.168.0.1"
+port = 1886
+client_host = "192.168.0.1"
+client_port = 1885
+client_ca_file = "/mqtt/ca.crt"
+client_ca_path = "/mqtt/ca"
+external_port = 8765
+external_bind_address = "0.0.0.0"
+external_bind_interface = "wlan0"
+external_capath = "/mqtt/external/ca.pem"
+external_certfile = "/mqtt/external/cert.pem"
+external_keyfile = "/mqtt/external/key.pem"
+
+[mqtt.client_auth]
+cert_file = "/mqtt/auth/cert.pem"
+key_file = "/mqtt/auth/key.pem"
+
+[http]
+port = 1234
+
+[software]
+default_plugin_type = "my-plugin"
+
+[tmp]
+path = "/tmp-path"
+
+[logs]
+path = "/logs-path"
+
+[run]
+path = "/run-path"
+lock_files = false
+
+[data]
+path = "/data-path"
+
+[firmware]
+child_update_timeout = 3429
+
+[service]
+type = "a-service-type""#;
+        let (_tempdir, config_location) = create_temp_tedge_config(toml).unwrap();
+        let toml_path = config_location.tedge_config_file_path();
+        let (dto, warnings) = TEdgeConfigRepository::new(config_location.clone())
+            .load_dto_with_warnings::<FileOnly>(toml_path)
+            .unwrap();
+
+        // Figment will warn us if we're not using a field. If we've migrated
+        // everything successfully, then no warnings will be emitted
+        assert_eq!(warnings, UnusedValueWarnings::default());
+
+        let reader = TEdgeConfigReader::from_dto(&dto, &config_location);
+
+        assert_eq!(reader.device.cert_path, "/tedge/device-cert.pem");
+        assert_eq!(reader.device.key_path, "/tedge/device-key.pem");
+        assert_eq!(reader.device.ty, "a-device");
+        assert_eq!(u16::from(reader.mqtt.bind.port), 1886);
+        assert_eq!(u16::from(reader.mqtt.client.port), 1885);
+    }
+
+    fn create_temp_tedge_config(
+        content: &str,
+    ) -> std::io::Result<(TempTedgeDir, TEdgeConfigLocation)> {
+        let dir = TempTedgeDir::new();
+        dir.file("tedge.toml").with_raw_content(content);
+        let config_location = TEdgeConfigLocation::from_custom_root(dir.path());
+        Ok((dir, config_location))
+    }
+}
diff --git a/crates/common/tedge_config/tests/test_tedge_config.rs b/crates/common/tedge_config/tests/test_tedge_config.rs
index 1d8b4b9f199..43dbd5a49c2 100644
--- a/crates/common/tedge_config/tests/test_tedge_config.rs
+++ b/crates/common/tedge_config/tests/test_tedge_config.rs
@@ -673,7 +673,7 @@ fn test_parse_config_empty_file() -> Result<(), TEdgeConfigError> {
     );
     assert_eq!(
         config.query(FirmwareChildUpdateTimeoutSetting)?,
-        Seconds(3600)
+        Seconds::from(3600)
     );
     assert_eq!(config.query(ServiceTypeSetting)?, "service".to_string());
     assert_eq!(config.query(TmpPathSetting)?, Utf8PathBuf::from("/tmp"));
@@ -727,7 +727,7 @@ fn test_parse_config_no_config_file() -> Result<(), TEdgeConfigError> {
 #[test]
 fn test_invalid_mqtt_port() -> Result<(), TEdgeConfigError> {
     let toml_conf = r#"
-[mqtt]
+[mqtt.bind]
 port = "1883"
 "#;
 
@@ -735,7 +735,7 @@ port = "1883"
     let toml_path = config_location.tedge_config_file_path().to_string();
     let result = TEdgeConfigRepository::new(config_location).load();
 
-    let expected_err = format!("invalid type: found string \"1883\", expected u16 for key \"mqtt.port\" in {toml_path} TOML file");
+    let expected_err = format!("invalid type: found string \"1883\", expected a nonzero u16 for key \"mqtt.bind.port\" in {toml_path} TOML file");
 
     match result {
         Err(error @ TEdgeConfigError::Figment(_)) => assert_eq!(error.to_string(), expected_err),
@@ -1050,7 +1050,7 @@ fn dummy_tedge_config_defaults() -> TEdgeConfigDefaults {
         default_mqtt_bind_address: IpAddress(IpAddr::V4(Ipv4Addr::LOCALHOST)),
         default_http_bind_address: IpAddress(IpAddr::V4(Ipv4Addr::LOCALHOST)),
         default_c8y_smartrest_templates: TemplatesSet::default(),
-        default_firmware_child_update_timeout: Seconds(3600),
+        default_firmware_child_update_timeout: Seconds::from(3600),
         default_service_type: String::from("service"),
         default_lock_files: Flag(true),
     }
diff --git a/crates/common/tedge_config_macros/Cargo.toml b/crates/common/tedge_config_macros/Cargo.toml
new file mode 100644
index 00000000000..f15e7ece9ad
--- /dev/null
+++ b/crates/common/tedge_config_macros/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "tedge_config_macros"
+version = "0.1.0"
+edition = "2021"
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+camino = { version = "*", features = ["serde1"] }
+doku = "0.21"
+once_cell = "1.17"
+serde = "1.0"
+tedge_config_macros-macro = { path = "macro" }
+thiserror = "1.0"
+url = "2.3"
+
+[dev-dependencies]
+tracing = "0.1"
diff --git a/crates/common/tedge_config_macros/examples/macro.rs b/crates/common/tedge_config_macros/examples/macro.rs
new file mode 100644
index 00000000000..15b76a15332
--- /dev/null
+++ b/crates/common/tedge_config_macros/examples/macro.rs
@@ -0,0 +1,213 @@
+use camino::Utf8PathBuf;
+use std::net::IpAddr;
+use std::net::Ipv4Addr;
+use std::num::NonZeroU16;
+use std::path::PathBuf;
+use tedge_config_macros::*;
+
+static DEFAULT_ROOT_CERT_PATH: &str = "/etc/ssl/certs";
+
+#[derive(thiserror::Error, Debug)]
+pub enum ReadError {
+    #[error(transparent)]
+    ConfigNotSet(#[from] ConfigNotSet),
+    #[error("Something went wrong: {0}")]
+    GenericError(String),
+}
+
+define_tedge_config! {
+    #[tedge_config(reader(skip))]
+    config: {
+        #[tedge_config(default(value = 1))]
+        version: u32,
+    },
+
+    device: {
+        #[tedge_config(readonly(
+            write_error = "\
+                The device id is read from the device certificate and cannot be set directly.\n\
+                To set 'device.id' to some <id>, you can use `tedge cert create --device-id <id>`.",
+            function = "device_id",
+        ))]
+        #[doku(as = "String")]
+        id: Result<String, ReadError>,
+
+        /// Path where the device's private key is stored
+        #[tedge_config(example = "/etc/tedge/device-certs/tedge-private-key.pem", default(function = "default_device_key"))]
+        #[doku(as = "PathBuf")]
+        key_path: Utf8PathBuf,
+
+        /// Path where the device's certificate is stored
+        #[tedge_config(example = "/etc/tedge/device-certs/tedge-certificate.pem", default(function = "default_device_cert"))]
+        #[doku(as = "PathBuf")]
+        cert_path: Utf8PathBuf,
+
+        /// The default device type
+        #[tedge_config(example = "thin-edge.io")]
+        #[tedge_config(rename = "type")]
+        device_type: String,
+    },
+
+    #[tedge_config(deprecated_name = "azure")] // for 0.1.0 compatibility
+    az: {
+        /// Endpoint URL of Azure IoT tenant
+        #[tedge_config(example = "myazure.azure-devices.net")]
+        url: ConnectUrl,
+
+        /// The path where Azure IoT root certificate(s) are stared
+        #[tedge_config(note = "The value can be a directory path as well as the path of the direct certificate file.")]
+        #[tedge_config(example = "/etc/tedge/az-trusted-root-certificates.pem", default(variable = "DEFAULT_ROOT_CERT_PATH"))]
+        #[doku(as = "PathBuf")]
+        root_cert_path: Utf8PathBuf,
+
+        mapper: {
+            /// Whether the Azure IoT mapper should add a timestamp or not
+            #[tedge_config(example = "true")]
+            #[tedge_config(default(value = true))]
+            timestamp: bool,
+        }
+    },
+
+    c8y: {
+        #[tedge_config(reader(private))]
+        url: String,
+
+        http: {
+            #[tedge_config(default(from_optional_key = "c8y.url"))]
+            url: String,
+        }
+    },
+
+    mqtt: {
+        bind: {
+            /// The address mosquitto binds to for internal use
+            #[tedge_config(example = "127.0.0.1", default(variable = "Ipv4Addr::LOCALHOST"))]
+            address: IpAddr,
+
+            /// The port mosquitto binds to for internal use
+            #[tedge_config(example = "1883", default(function = "default_mqtt_port"))]
+            #[doku(as = "u16")]
+            #[tedge_config(deprecated_key = "mqtt.bind.port")]
+            // This was originally u16, but I can't think of any way in which
+            // tedge could actually connect to mosquitto if it bound to a random
+            // free port, so I don't think 0 is *really* valid here
+            port: NonZeroU16,
+        },
+
+        client: {
+            /// The host that the thin-edge MQTT client should connect to
+            #[tedge_config(example = "localhost", default(value = "localhost"))]
+            host: String,
+
+            /// The port that the thin-edge MQTT client should connect to
+            #[tedge_config(default(from_key = "mqtt.bind.port"))]
+            #[doku(as = "u16")]
+            port: NonZeroU16,
+
+            auth: {
+                /// Path to the CA certificate used by MQTT clients to use when authenticating the MQTT broker
+                #[tedge_config(example = "/etc/mosquitto/ca_certificates/ca.crt")]
+                #[doku(as = "PathBuf")]
+                #[tedge_config(deprecated_name = "cafile")]
+                ca_file: Utf8PathBuf,
+
+                /// Path to the directory containing the CA certificates used by MQTT
+                /// clients when authenticating the MQTT broker
+                #[tedge_config(example = "/etc/mosquitto/ca_certificates")]
+                #[doku(as = "PathBuf")]
+                #[tedge_config(deprecated_name = "capath")]
+                ca_path: Utf8PathBuf,
+
+                /// Path to the client certficate
+                #[doku(as = "PathBuf")]
+                #[tedge_config(deprecated_name = "certfile")]
+                cert_file: Utf8PathBuf,
+
+                /// Path to the client private key
+                #[doku(as = "PathBuf")]
+                #[tedge_config(deprecated_name = "keyfile")]
+                key_file: Utf8PathBuf,
+            }
+        },
+
+        external: {
+            bind: {
+                /// The port mosquitto binds to for external use
+                #[tedge_config(example = "8883")]
+                port: u16,
+
+                /// The address mosquitto binds to for external use
+                #[tedge_config(example = "0.0.0.0")]
+                address: IpAddr,
+
+                /// Name of the network interface which mosquitto limits incoming connections on
+                #[tedge_config(example = "wlan0")]
+                interface: String,
+            },
+
+            /// Path to a file containing the PEM encoded CA certificates that are
+            /// trusted when checking incoming client certificates
+            #[tedge_config(example = "/etc/ssl/certs")]
+            #[doku(as = "PathBuf")]
+            #[tedge_config(deprecated_name = "capath")]
+            ca_path: Utf8PathBuf,
+
+            /// Path to the certificate file which is used by the external MQTT listener
+            #[tedge_config(note = "This setting shall be used together with `mqtt.external.key_file` for external connections.")]
+            #[tedge_config(example = "/etc/tedge/device-certs/tedge-certificate.pem")]
+            #[doku(as = "PathBuf")]
+            #[tedge_config(deprecated_name = "certfile")]
+            cert_file: Utf8PathBuf,
+
+            /// Path to the key file which is used by the external MQTT listener
+            #[tedge_config(note = "This setting shall be used together with `mqtt.external.cert_file` for external connections.")]
+            #[tedge_config(example = "/etc/tedge/device-certs/tedge-private-key.pem")]
+            #[doku(as = "PathBuf")]
+            #[tedge_config(deprecated_name = "keyfile")]
+            key_file: Utf8PathBuf,
+        }
+    }
+}
+
+fn device_id(_reader: &TEdgeConfigReader) -> Result<String, ReadError> {
+    Ok("dummy-device-id".to_owned())
+}
+
+fn default_device_key(location: &TEdgeConfigLocation) -> Utf8PathBuf {
+    location
+        .tedge_config_root_path()
+        .join("device-certs")
+        .join("tedge-private-key.pem")
+}
+
+fn default_device_cert(location: &TEdgeConfigLocation) -> Utf8PathBuf {
+    location
+        .tedge_config_root_path()
+        .join("device-certs")
+        .join("tedge-certificate.pem")
+}
+
+fn default_mqtt_port() -> NonZeroU16 {
+    NonZeroU16::try_from(1883).unwrap()
+}
+
+fn main() {
+    let mut dto = TEdgeConfigDto::default();
+    dto.mqtt.bind.address = Some(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)));
+
+    let config = TEdgeConfigReader::from_dto(&dto, &TEdgeConfigLocation);
+
+    // Typed reads
+    println!(
+        "Device id is {}.",
+        // We have to pass the config into try_read to avoid TEdgeConfigReader being
+        // self-referential
+        config.device.id.try_read(&config).as_ref().unwrap()
+    );
+    assert_eq!(u16::from(config.mqtt.bind.port), 1883);
+    assert_eq!(config.mqtt.external.bind.port.or_none(), None);
+    assert_eq!(
+        config.read_string(ReadableKey::DeviceId).unwrap(),
+        "dummy-device-id"
+    );
+}
diff --git a/crates/common/tedge_config_macros/examples/renaming_keys.rs b/crates/common/tedge_config_macros/examples/renaming_keys.rs
new file mode 100644
index 00000000000..27281775fa6
--- /dev/null
+++ b/crates/common/tedge_config_macros/examples/renaming_keys.rs
@@ -0,0 +1,56 @@
+use camino::Utf8PathBuf;
+use std::path::PathBuf;
+use tedge_config_macros::*;
+
+#[derive(thiserror::Error, Debug)]
+pub enum ReadError {
+    #[error(transparent)]
+    ConfigNotSet(#[from] ConfigNotSet),
+    #[error("Something went wrong: {0}")]
+    GenericError(String),
+}
+
+define_tedge_config! {
+    device: {
+        // Fields and groups can be renamed using #[tedge_config(rename)]
+        // this is useful
+        #[tedge_config(rename = "type")]
+        ty: String,
+
+    },
+
+    mqtt: {
+        client: {
+            auth: {
+                // Where we have renamed a field, we can use
+                // #[tedge_config(deprecated_name)] to create an alias that both serde and `tedge config`
+                // will understand
+                #[tedge_config(deprecated_name = "cafile")]
+                #[doku(as = "PathBuf")]
+                ca_file: Utf8PathBuf,
+            }
+        },
+
+        bind: {
+            // If we've moved a field, we can use
+            // #[tedge_config(deprecated_key)] to provide an alternative key for
+            // `tedge config` to accept. NB: this changes necessitates a toml
+            // migration as we cannot use serde aliases for a structural change
+            // like this
+            #[tedge_config(deprecated_key = "mqtt.port")]
+            port: u16,
+        }
+    }
+}
+
+fn main() {
+    let parsed_deprecated_key = "mqtt.port".parse::<ReadableKey>().unwrap();
+    let parsed_current_key = "mqtt.bind.port".parse::<ReadableKey>().unwrap();
+    assert_eq!(parsed_deprecated_key, parsed_current_key);
+    assert_eq!(parsed_deprecated_key.as_str(), "mqtt.bind.port");
+
+    let parsed_deprecated_key = "mqtt.client.auth.cafile".parse::<WritableKey>().unwrap();
+    let parsed_current_key = "mqtt.client.auth.ca_file".parse::<WritableKey>().unwrap();
+    assert_eq!(parsed_deprecated_key, parsed_current_key);
+    assert_eq!(parsed_deprecated_key.as_str(), "mqtt.client.auth.ca_file")
+}
diff --git a/crates/common/tedge_config_macros/impl/Cargo.toml b/crates/common/tedge_config_macros/impl/Cargo.toml
new file mode 100644
index 00000000000..6d3f8706998
--- /dev/null
+++ b/crates/common/tedge_config_macros/impl/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "tedge_config_macros-impl"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+darling = "0.20"
+heck = "0.4.1"
+proc-macro2 = "1"
+quote = "1"
+syn = { version = "2", features = ["full", "extra-traits"] }
diff --git a/crates/common/tedge_config_macros/impl/src/dto.rs b/crates/common/tedge_config_macros/impl/src/dto.rs
new file mode 100644
index 00000000000..a2336e3e210
--- /dev/null
+++ b/crates/common/tedge_config_macros/impl/src/dto.rs
@@ -0,0 +1,120 @@
+use proc_macro2::TokenStream;
+use quote::quote;
+use syn::parse_quote_spanned;
+use syn::spanned::Spanned;
+
+use crate::input::FieldOrGroup;
+use crate::prefixed_type_name;
+
+pub fn generate(
+    name: proc_macro2::Ident,
+    items: &[FieldOrGroup],
+    doc_comment: &str,
+) -> TokenStream {
+    let mut idents = Vec::new();
+    let mut tys = Vec::<syn::Type>::new();
+    let mut sub_dtos = Vec::new();
+    let mut preserved_attrs: Vec<Vec<&syn::Attribute>> = Vec::new();
+    let mut extra_attrs = Vec::new();
+
+    for item in items {
+        match item {
+            FieldOrGroup::Field(field) => {
+                if !field.dto().skip && field.read_only().is_none() {
+                    idents.push(field.ident());
+                    tys.push({
+                        let ty = field.ty();
+                        parse_quote_spanned!(ty.span()=> Option<#ty>)
+                    });
+                    sub_dtos.push(None);
+                    preserved_attrs.push(field.attrs().iter().filter(is_preserved).collect());
+                    extra_attrs.push(quote! {});
+                }
+            }
+            FieldOrGroup::Group(group) => {
+                if !group.dto.skip {
+                    let sub_dto_name = prefixed_type_name(&name, group);
+                    let is_default = format!("{sub_dto_name}::is_default");
+                    idents.push(&group.ident);
+                    tys.push(parse_quote_spanned!(group.ident.span()=> #sub_dto_name));
+                    sub_dtos.push(Some(generate(sub_dto_name, &group.contents, "")));
+                    preserved_attrs.push(group.attrs.iter().filter(is_preserved).collect());
+                    extra_attrs.push(quote! {
+                        #[serde(default)]
+                        #[serde(skip_serializing_if = #is_default)]
+                    });
+                }
+            }
+        }
+    }
+
+    quote! {
+        #[derive(Debug, Default, ::serde::Deserialize, ::serde::Serialize, PartialEq)]
+        // We will add more configurations in the future, so this is
+        // non_exhaustive (see
+        // https://doc.rust-lang.org/reference/attributes/type_system.html)
+        #[non_exhaustive]
+        #[doc = #doc_comment]
+        pub struct #name {
+            #(
+                // The fields are pub as that allows people to easily modify the
+                // dto via a mutable borrow
+                #(#preserved_attrs)*
+                #extra_attrs
+                pub #idents: #tys,
+            )*
+        }
+
+        impl #name {
+            fn is_default(&self) -> bool {
+                self == &Self::default()
+            }
+        }
+
+        #(#sub_dtos)*
+    }
+}
+
+fn is_preserved(attr: &&syn::Attribute) -> bool {
+    match &attr.meta {
+        // Maybe cfg is useful. Certainly seems sensible to preserve it
+        syn::Meta::List(list) => list.path.is_ident("serde") || list.path.is_ident("cfg"),
+        syn::Meta::NameValue(nv) => nv.path.is_ident("doc"),
+        _ => false,
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use syn::parse_quote;
+
+    use super::*;
+
+    #[test]
+    fn doc_comments_are_preserved() {
+        assert!(is_preserved(&&parse_quote!(
+            /// Test
+        )))
+    }
+
+    #[test]
+    fn serde_attributes_are_preserved() {
+        assert!(is_preserved(&&parse_quote!(
+            #[serde(alias = "something")]
+        )))
+    }
+
+    #[test]
+    fn unrecognised_attributes_are_not_preserved() {
+        assert!(!is_preserved(&&parse_quote!(
+            #[unknown_crate(unknown_bool)]
+        )))
+    }
+
+    #[test]
+    fn unrecognised_attributes_of_the_wrong_type_are_not_preserved() {
+        assert!(!is_preserved(&&parse_quote!(
+            #[unknown_attribute = "some value"]
+        )))
+    }
+}
diff --git a/crates/common/tedge_config_macros/impl/src/error.rs b/crates/common/tedge_config_macros/impl/src/error.rs
new file mode 100644
index 00000000000..53923ab1d02
--- /dev/null
+++ b/crates/common/tedge_config_macros/impl/src/error.rs
@@ -0,0 +1,80 @@
+use crate::optional_error::OptionalError;
+
+pub fn combine_errors<T>(
+    items: impl Iterator<Item = Result<T, syn::Error>>,
+) -> Result<Vec<T>, syn::Error> {
+    let mut error = OptionalError::default();
+    let mut successful_values = Vec::new();
+    for item in items {
+        match item {
+            Ok(value) => successful_values.push(value),
+            Err(e) => error.combine(e),
+        }
+    }
+    error.try_throw().and(Ok(successful_values))
+}
+
+// Based on https://stackoverflow.com/a/56264023
+pub fn extract_type_from_result(ty: &syn::Type) -> Option<(&syn::Type, &syn::Type)> {
+    use syn::GenericArgument;
+    use syn::Path;
+    use syn::PathArguments;
+    use syn::PathSegment;
+
+    fn extract_type_path(ty: &syn::Type) -> Option<&Path> {
+        match *ty {
+            syn::Type::Path(ref typepath) if typepath.qself.is_none() => Some(&typepath.path),
+            _ => None,
+        }
+    }
+
+    fn extract_result_segment(path: &Path) -> Option<&PathSegment> {
+        let idents_of_path = path.segments.iter().fold(String::new(), |mut acc, v| {
+            acc.push_str(&v.ident.to_string());
+            acc.push('|');
+            acc
+        });
+        vec!["Result|", "std|result|Result|", "core|result|Result|"]
+            .into_iter()
+            .find(|s| idents_of_path == *s)
+            .and_then(|_| path.segments.last())
+    }
+
+    extract_type_path(ty)
+        .and_then(extract_result_segment)
+        .and_then(|path_seg| {
+            let type_params = &path_seg.arguments;
+            // It should have only on angle-bracketed param ("<String>"):
+            match *type_params {
+                PathArguments::AngleBracketed(ref params) => {
+                    Some((params.args.first()?, params.args.last()?))
+                }
+                _ => None,
+            }
+        })
+        .and_then(|generic_arg| match generic_arg {
+            (GenericArgument::Type(ok), GenericArgument::Type(err)) => Some((ok, err)),
+            _ => None,
+        })
+}
+
+#[test]
+fn extract_type_from_different_results() {
+    use syn::parse_quote;
+    assert_eq!(
+        extract_type_from_result(&parse_quote!(Result<String, Error>)),
+        Some((&parse_quote!(String), &parse_quote!(Error)))
+    );
+    assert_eq!(
+        extract_type_from_result(&parse_quote!(::std::result::Result<String, Error>)),
+        Some((&parse_quote!(String), &parse_quote!(Error)))
+    );
+    assert_eq!(
+        extract_type_from_result(&parse_quote!(std::result::Result<String, Error>)),
+        Some((&parse_quote!(String), &parse_quote!(Error)))
+    );
+    assert_eq!(
+        extract_type_from_result(&parse_quote!(core::result::Result<String, Error>)),
+        Some((&parse_quote!(String), &parse_quote!(Error)))
+    );
+}
diff --git a/crates/common/tedge_config_macros/impl/src/input/mod.rs b/crates/common/tedge_config_macros/impl/src/input/mod.rs
new file mode 100644
index 00000000000..84a58dc4015
--- /dev/null
+++ b/crates/common/tedge_config_macros/impl/src/input/mod.rs
@@ -0,0 +1,4 @@
+mod parse;
+mod validate;
+
+pub use validate::*;
diff --git a/crates/common/tedge_config_macros/impl/src/input/parse.rs b/crates/common/tedge_config_macros/impl/src/input/parse.rs
new file mode 100644
index 00000000000..9e421127696
--- /dev/null
+++ b/crates/common/tedge_config_macros/impl/src/input/parse.rs
@@ -0,0 +1,173 @@
+//! The inital parsing logic
+//!
+//! This is designed to take a [proc_macro2::TokenStream] and turn it into
+//! something useful with the aid of [syn].
+use darling::util::SpannedValue;
+use darling::FromAttributes;
+use darling::FromField;
+use darling::FromMeta;
+use syn::parse::Parse;
+use syn::punctuated::Punctuated;
+use syn::Attribute;
+use syn::Token;
+
+#[derive(Debug)]
+pub struct Configuration {
+    pub groups: Punctuated<FieldOrGroup, Token![,]>,
+}
+
+impl Parse for Configuration {
+    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+        Ok(Self {
+            groups: input.parse_terminated(<_>::parse, Token![,])?,
+        })
+    }
+}
+
+#[derive(FromAttributes)]
+#[darling(attributes(tedge_config))]
+pub struct ConfigurationAttributes {
+    #[darling(default)]
+    pub dto: GroupDtoSettings,
+    #[darling(default)]
+    pub reader: ReaderSettings,
+    #[darling(default, multiple, rename = "deprecated_name")]
+    pub deprecated_names: Vec<SpannedValue<String>>,
+    #[darling(default)]
+    pub rename: Option<SpannedValue<String>>,
+}
+
+#[derive(Debug)]
+pub struct ConfigurationGroup {
+    pub attrs: Vec<syn::Attribute>,
+    pub dto: GroupDtoSettings,
+    pub reader: ReaderSettings,
+    pub deprecated_names: Vec<SpannedValue<String>>,
+    pub rename: Option<SpannedValue<String>>,
+    pub ident: syn::Ident,
+    pub colon_token: Token![:],
+    pub brace: syn::token::Brace,
+    pub content: Punctuated<FieldOrGroup, Token![,]>,
+}
+
+impl Parse for ConfigurationGroup {
+    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+        let content;
+        let attributes = input.call(Attribute::parse_outer)?;
+        let known_attributes = ConfigurationAttributes::from_attributes(&attributes)?;
+        Ok(ConfigurationGroup {
+            attrs: attributes.into_iter().filter(not_tedge_config).collect(),
+            dto: known_attributes.dto,
+            reader: known_attributes.reader,
+            deprecated_names: known_attributes.deprecated_names,
+            rename: known_attributes.rename,
+            ident: input.parse()?,
+            colon_token: input.parse()?,
+            brace: syn::braced!(content in input),
+            content: content.parse_terminated(<_>::parse, Token![,])?,
+        })
+    }
+}
+
+fn not_tedge_config(attr: &syn::Attribute) -> bool {
+    let is_tedge_config = match &attr.meta {
+        syn::Meta::List(list) => list.path.is_ident("tedge_config"),
+        _ => false,
+    };
+
+    !is_tedge_config
+}
+
+#[derive(Debug)]
+pub enum FieldOrGroup {
+    Field(ConfigurableField),
+    Group(ConfigurationGroup),
+}
+
+impl Parse for FieldOrGroup {
+    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+        let fork = input.fork();
+
+        fork.call(Attribute::parse_outer)?;
+        fork.parse::<syn::Ident>()?;
+        fork.parse::<Token![:]>()?;
+
+        let lookahead = fork.lookahead1();
+        if lookahead.peek(syn::token::Brace) {
+            input.parse().map(Self::Group)
+        } else {
+            input.parse().map(Self::Field)
+        }
+    }
+}
+
+#[derive(FromField, Debug)]
+#[darling(attributes(tedge_config), forward_attrs)]
+pub struct ConfigurableField {
+    pub attrs: Vec<syn::Attribute>,
+    #[darling(default)]
+    pub readonly: Option<ReadonlySettings>,
+    #[darling(default)]
+    pub dto: FieldDtoSettings,
+    #[darling(default)]
+    pub rename: Option<SpannedValue<String>>,
+    #[darling(multiple, rename = "deprecated_key")]
+    pub deprecated_keys: Vec<SpannedValue<String>>,
+    #[darling(multiple, rename = "deprecated_name")]
+    pub deprecated_names: Vec<SpannedValue<String>>,
+    #[darling(default)]
+    // TODO remove this or separate it from the group ones
+    pub reader: ReaderSettings,
+    #[darling(default)]
+    pub default: Option<FieldDefault>,
+    #[darling(default)]
+    pub note: Option<SpannedValue<String>>,
+    #[darling(multiple, rename = "example")]
+    pub examples: Vec<SpannedValue<String>>,
+    pub ident: Option<syn::Ident>,
+    pub ty: syn::Type,
+}
+
+#[derive(Debug, FromMeta, PartialEq, Eq)]
+pub enum FieldDefault {
+    Variable(syn::Path),
+    Function(syn::Expr),
+    FromKey(Punctuated<syn::Ident, syn::Token![.]>),
+    FromOptionalKey(Punctuated<syn::Ident, syn::Token![.]>),
+    Value(syn::Lit),
+    None,
+}
+
+impl Parse for ConfigurableField {
+    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+        Ok(Self::from_field(&input.call(syn::Field::parse_named)?)?)
+    }
+}
+
+#[derive(FromMeta, Debug, Default)]
+pub struct GroupDtoSettings {
+    #[darling(default)]
+    pub skip: bool,
+    #[darling(default)]
+    pub flatten: bool,
+}
+
+#[derive(FromMeta, Debug, Default)]
+pub struct FieldDtoSettings {
+    #[darling(default)]
+    pub skip: bool,
+}
+
+#[derive(FromMeta, Debug, Default)]
+pub struct ReaderSettings {
+    #[darling(default)]
+    pub private: bool,
+    #[darling(default)]
+    pub skip: bool,
+}
+
+#[derive(FromMeta, Debug)]
+pub struct ReadonlySettings {
+    pub write_error: String,
+    pub function: syn::Path,
+}
diff --git a/crates/common/tedge_config_macros/impl/src/input/validate.rs b/crates/common/tedge_config_macros/impl/src/input/validate.rs
new file mode 100644
index 00000000000..099f9b3b3fc
--- /dev/null
+++ b/crates/common/tedge_config_macros/impl/src/input/validate.rs
@@ -0,0 +1,609 @@
+use std::borrow::Cow;
+
+use darling::export::NestedMeta;
+use darling::util::SpannedValue;
+use heck::ToUpperCamelCase;
+use quote::format_ident;
+use syn::parse_quote_spanned;
+use syn::spanned::Spanned;
+
+use crate::error::combine_errors;
+use crate::optional_error::OptionalError;
+use crate::optional_error::SynResultExt;
+
+pub use super::parse::FieldDefault;
+pub use super::parse::FieldDtoSettings;
+pub use super::parse::GroupDtoSettings;
+pub use super::parse::ReaderSettings;
+use super::parse::ReadonlySettings;
+
+#[derive(Debug)]
+pub struct Configuration {
+    pub groups: Vec<FieldOrGroup>,
+}
+
+impl syn::parse::Parse for Configuration {
+    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+        super::parse::Configuration::parse(input)?.try_into()
+    }
+}
+
+impl TryFrom<super::parse::Configuration> for Configuration {
+    type Error = syn::Error;
+
+    fn try_from(value: super::parse::Configuration) -> Result<Self, Self::Error> {
+        Ok(Self {
+            groups: combine_errors(value.groups.into_iter().map(<_>::try_from))?,
+        })
+    }
+}
+
+#[derive(Debug)]
+pub struct ConfigurationGroup {
+    pub attrs: Vec<syn::Attribute>,
+    pub rename: Option<SpannedValue<String>>,
+    pub dto: GroupDtoSettings,
+    pub reader: ReaderSettings,
+    pub ident: syn::Ident,
+    pub contents: Vec<FieldOrGroup>,
+}
+
+impl TryFrom<super::parse::ConfigurationGroup> for ConfigurationGroup {
+    type Error = syn::Error;
+
+    fn try_from(mut value: super::parse::ConfigurationGroup) -> Result<Self, Self::Error> {
+        deny_attribute(
+            &value.attrs,
+            "serde",
+            "rename",
+            "tedge_config(rename)",
+            "rename a group",
+        )?;
+        deny_attribute(
+            &value.attrs,
+            "serde",
+            "alias",
+            "tedge_config(deprecated_name)",
+            "supply an alias for a group",
+        )?;
+
+        for name in value.deprecated_names {
+            // TODO similar errors to fields?
+            let name_str = name.as_str();
+            value
+                .attrs
+                .push(parse_quote_spanned! {name.span() => #[serde(alias = #name_str)]})
+        }
+
+        Ok(Self {
+            attrs: value.attrs,
+            rename: value.rename,
+            dto: value.dto,
+            reader: value.reader,
+            ident: value.ident,
+            contents: combine_errors(value.content.into_iter().map(<_>::try_from))?,
+        })
+    }
+}
+
+#[derive(Debug)]
+pub enum FieldOrGroup {
+    Field(ConfigurableField),
+    Group(ConfigurationGroup),
+}
+
+impl FieldOrGroup {
+    pub fn name(&self) -> Cow<str> {
+        let rename = match self {
+            Self::Group(group) => group.rename.as_ref().map(|s| s.as_str()),
+            Self::Field(field) => field.rename(),
+        };
+
+        rename.map_or_else(|| Cow::Owned(self.ident().to_string()), Cow::Borrowed)
+    }
+
+    pub fn ident(&self) -> &syn::Ident {
+        match self {
+            Self::Field(field) => field.ident(),
+            Self::Group(group) => &group.ident,
+        }
+    }
+
+    pub fn is_called(&self, target: &syn::Ident) -> bool {
+        self.ident() == target
+            || match self {
+                Self::Field(field) => field.rename().map_or(false, |rename| *target == rename),
+                // Groups don't support renaming at the moment
+                Self::Group(_) => false,
+            }
+    }
+
+    pub fn field(&self) -> Option<&ConfigurableField> {
+        match self {
+            Self::Field(field) => Some(field),
+            Self::Group(..) => None,
+        }
+    }
+
+    pub fn reader(&self) -> &ReaderSettings {
+        match self {
+            Self::Field(field) => field.reader(),
+            Self::Group(group) => &group.reader,
+        }
+    }
+}
+
+impl TryFrom<super::parse::FieldOrGroup> for FieldOrGroup {
+    type Error = syn::Error;
+    fn try_from(value: super::parse::FieldOrGroup) -> Result<Self, Self::Error> {
+        match value {
+            super::parse::FieldOrGroup::Field(field) => field.try_into().map(Self::Field),
+            super::parse::FieldOrGroup::Group(group) => group.try_into().map(Self::Group),
+        }
+    }
+}
+
+#[derive(Debug)]
+pub enum ConfigurableField {
+    ReadOnly(ReadOnlyField),
+    ReadWrite(ReadWriteField),
+}
+
+#[derive(Debug)]
+pub struct ReadOnlyField {
+    pub attrs: Vec<syn::Attribute>,
+    pub deprecated_keys: Vec<SpannedValue<String>>,
+    pub readonly: ReadonlySettings,
+    pub rename: Option<SpannedValue<String>>,
+    pub dto: FieldDtoSettings,
+    pub reader: ReaderSettings,
+    pub ident: syn::Ident,
+    pub ty: syn::Type,
+}
+
+impl ReadOnlyField {
+    pub fn lazy_reader_name(&self, parents: &[syn::Ident]) -> syn::Ident {
+        format_ident!(
+            "LazyReader{}{}",
+            parents
+                .iter()
+                .map(|p| p.to_string().to_upper_camel_case())
+                .collect::<Vec<_>>()
+                .join("."),
+            self.rename()
+                .map(<_>::to_owned)
+                .unwrap_or_else(|| self.ident.to_string())
+                .to_upper_camel_case()
+        )
+    }
+
+    pub fn rename(&self) -> Option<&str> {
+        Some(self.rename.as_ref()?.as_str())
+    }
+}
+
+#[derive(Debug)]
+pub struct ReadWriteField {
+    pub attrs: Vec<syn::Attribute>,
+    pub deprecated_keys: Vec<SpannedValue<String>>,
+    pub rename: Option<SpannedValue<String>>,
+    pub dto: FieldDtoSettings,
+    pub reader: ReaderSettings,
+    pub examples: Vec<SpannedValue<String>>,
+    pub ident: syn::Ident,
+    pub ty: syn::Type,
+    pub default: FieldDefault,
+}
+
+impl ConfigurableField {
+    pub fn attrs(&self) -> &[syn::Attribute] {
+        match self {
+            Self::ReadOnly(ReadOnlyField { attrs, .. })
+            | Self::ReadWrite(ReadWriteField { attrs, .. }) => attrs,
+        }
+    }
+
+    pub fn has_guaranteed_default(&self) -> bool {
+        match self {
+            Self::ReadWrite(_) => !self.is_optional(),
+            Self::ReadOnly(..) => false,
+        }
+    }
+
+    pub fn is_optional(&self) -> bool {
+        matches!(
+            self,
+            Self::ReadWrite(ReadWriteField {
+                default: FieldDefault::FromOptionalKey(_) | FieldDefault::None,
+                ..
+            })
+        )
+    }
+
+    pub fn ident(&self) -> &syn::Ident {
+        match self {
+            Self::ReadOnly(ReadOnlyField { ident, .. })
+            | Self::ReadWrite(ReadWriteField { ident, .. }) => ident,
+        }
+    }
+
+    pub fn rename(&self) -> Option<&str> {
+        match self {
+            Self::ReadOnly(ReadOnlyField { rename, .. })
+            | Self::ReadWrite(ReadWriteField { rename, .. }) => Some(rename.as_ref()?.as_str()),
+        }
+    }
+
+    pub fn ty(&self) -> &syn::Type {
+        match self {
+            Self::ReadOnly(ReadOnlyField { ty, .. })
+            | Self::ReadWrite(ReadWriteField { ty, .. }) => ty,
+        }
+    }
+
+    pub fn dto(&self) -> &FieldDtoSettings {
+        match self {
+            Self::ReadOnly(ReadOnlyField { dto, .. })
+            | Self::ReadWrite(ReadWriteField { dto, .. }) => dto,
+        }
+    }
+
+    #[allow(unused)]
+    pub fn reader(&self) -> &ReaderSettings {
+        match self {
+            Self::ReadOnly(ReadOnlyField { reader, .. })
+            | Self::ReadWrite(ReadWriteField { reader, .. }) => reader,
+        }
+    }
+
+    pub fn read_write(&self) -> Option<&ReadWriteField> {
+        match self {
+            Self::ReadWrite(field) => Some(field),
+            Self::ReadOnly(_) => None,
+        }
+    }
+
+    pub fn read_only(&self) -> Option<&ReadOnlyField> {
+        match self {
+            Self::ReadOnly(field) => Some(field),
+            Self::ReadWrite(_) => None,
+        }
+    }
+
+    pub fn deprecated_keys(&self) -> impl Iterator<Item = &str> {
+        let keys = match self {
+            Self::ReadOnly(field) => &field.deprecated_keys,
+            Self::ReadWrite(field) => &field.deprecated_keys,
+        };
+        keys.iter().map(|key| key.as_str())
+    }
+}
+
+impl TryFrom<super::parse::ConfigurableField> for ConfigurableField {
+    type Error = syn::Error;
+    fn try_from(mut value: super::parse::ConfigurableField) -> Result<Self, Self::Error> {
+        let mut custom_errors = OptionalError::default();
+
+        let attrs = &value.attrs;
+        deny_attribute(
+            attrs,
+            "serde",
+            "rename",
+            "tedge_config(rename)",
+            "rename a field",
+        )
+        .append_err_to(&mut custom_errors);
+        deny_attribute(
+            attrs,
+            "serde",
+            "alias",
+            "tedge_config(deprecated_name)",
+            "create an alias for a field",
+        )
+        .append_err_to(&mut custom_errors);
+        deny_attribute(
+            attrs,
+            "doku",
+            "example",
+            "tedge_config(example)",
+            "supply an example value for a field",
+        )
+        .append_err_to(&mut custom_errors);
+
+        if let Some(renamed_to) = &value.rename {
+            let span = renamed_to.span();
+            let literal = renamed_to.as_str();
+            value
+                .attrs
+                .push(parse_quote_spanned!(span=> #[serde(rename = #literal)]))
+        }
+
+        for name in value.deprecated_names {
+            let name_str = name.as_str();
+            if name.contains('.') {
+                custom_errors.combine(syn::Error::new(
+                    name.span(),
+                    format!("this a path rather than a field or group name. Did you mean to use #[tedge_config(deprecated_key = \"{name_str}\")] instead?")
+                ));
+            }
+            value
+                .attrs
+                .push(parse_quote_spanned! {name.span()=> #[serde(alias = #name_str)]})
+        }
+
+        for key in &value.deprecated_keys {
+            if !key.contains('.') {
+                custom_errors.combine(syn::Error::new(
+                    key.span(),
+                    format!("this is just a field or group name, not a key (which would contain one or more `.`s). Did you mean to use #[tedge_config(deprecated_name = \"{}\"] instead?", key.as_str())
+                ));
+            }
+        }
+
+        for example in &value.examples {
+            let example_str = example.as_str();
+            value
+                .attrs
+                .push(parse_quote_spanned! {example.span()=> #[doku(example = #example_str)]});
+        }
+
+        if let Some(note) = value.note {
+            value.attrs.push(tedge_note_to_doku_meta(&note));
+        }
+
+        custom_errors.try_throw()?;
+
+        if let Some(readonly) = value.readonly {
+            Ok(Self::ReadOnly(ReadOnlyField {
+                attrs: value.attrs,
+                deprecated_keys: value.deprecated_keys,
+                rename: value.rename,
+                ident: value.ident.unwrap(),
+                readonly,
+                ty: value.ty,
+                dto: value.dto,
+                reader: value.reader,
+            }))
+        } else {
+            Ok(Self::ReadWrite(ReadWriteField {
+                attrs: value.attrs,
+                deprecated_keys: value.deprecated_keys,
+                rename: value.rename,
+                examples: value.examples,
+                ident: value.ident.unwrap(),
+                ty: value.ty,
+                dto: value.dto,
+                reader: value.reader,
+                default: value.default.unwrap_or(FieldDefault::None),
+            }))
+        }
+    }
+}
+
+fn deny_attribute(
+    attrs: &[syn::Attribute],
+    krate: &str,
+    attribute: &str,
+    our_name: &str,
+    action: &str,
+) -> Result<(), syn::Error> {
+    attrs
+        .iter()
+        .filter(|attr| attr.path().is_ident(krate))
+        .filter_map(|attr| attr.meta.require_list().ok())
+        .filter_map(|attr| darling::ast::NestedMeta::parse_meta_list(attr.tokens.clone()).ok())
+        .flatten()
+        .filter_map(|attr| match attr {
+            NestedMeta::Meta(m) => Some(m),
+            _ => None,
+        })
+        .filter_map(|meta| Some(meta.require_name_value().ok()?.to_owned()))
+        .filter(|attr| attr.path.is_ident(attribute))
+        .map(|attr| {
+            syn::Error::new(
+                attr.span(),
+                format!("use #[{our_name}] instead of #[{krate}({attribute})] to {action}"),
+            )
+        })
+        .fold(OptionalError::default(), |errors, e| {
+            errors.combine_owned(e)
+        })
+        .try_throw()
+}
+
+fn tedge_note_to_doku_meta(note: &SpannedValue<String>) -> syn::Attribute {
+    let meta = format!("note = {}", note.as_str());
+    parse_quote_spanned!(note.span()=> #[doku(meta(#meta))])
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use proc_macro2::Span;
+    use quote::quote;
+    use syn::parse_quote;
+
+    #[test]
+    fn doku_examples_are_denied() {
+        let input: super::super::parse::Configuration = syn::parse2(quote! {
+            device: {
+                #[doku(example = "test")]
+                id: String,
+            },
+        })
+        .unwrap();
+
+        assert!(Configuration::try_from(input).is_err());
+    }
+
+    #[test]
+    fn tedge_note_is_converted_to_doku_meta() {
+        let note = SpannedValue::new("A note".to_owned(), Span::call_site());
+        assert_eq!(
+            tedge_note_to_doku_meta(&note),
+            parse_quote!(
+                #[doku(meta("note = A note"))]
+            )
+        );
+    }
+
+    #[test]
+    fn serde_rename_is_denied_for_fields() {
+        let input: super::super::parse::Configuration = syn::parse2(quote! {
+            device: {
+                #[serde(rename = "type")]
+                ty: String,
+            },
+        })
+        .unwrap();
+
+        let error = Configuration::try_from(input).unwrap_err();
+        assert!(error.to_string().contains("#[tedge_config(rename)]"))
+    }
+
+    #[test]
+    fn serde_alias_is_denied_for_fields() {
+        let input: super::super::parse::Configuration = syn::parse2(quote! {
+            device: {
+                #[serde(alias = "type")]
+                ty: String,
+            },
+        })
+        .unwrap();
+
+        let error = Configuration::try_from(input).unwrap_err();
+        assert!(error
+            .to_string()
+            .contains("#[tedge_config(deprecated_name)]"))
+    }
+
+    #[test]
+    fn serde_alias_is_denied_for_groups() {
+        let input: super::super::parse::Configuration = syn::parse2(quote! {
+            #[serde(alias = "dev")]
+            device: {
+                ty: String,
+            },
+        })
+        .unwrap();
+
+        let error = Configuration::try_from(input).unwrap_err();
+        assert!(error
+            .to_string()
+            .contains("#[tedge_config(deprecated_name)]"))
+    }
+
+    #[test]
+    fn deprecated_key_accepts_valid_keys_for_fields() {
+        let input: super::super::parse::Configuration = syn::parse2(quote! {
+            mqtt: {
+                bind: {
+                    #[tedge_config(deprecated_key = "mqtt.port")]
+                    port: u16,
+                }
+            },
+        })
+        .unwrap();
+
+        Configuration::try_from(input).unwrap();
+    }
+
+    #[test]
+    fn deprecated_name_accepts_valid_names_for_fields() {
+        let input: super::super::parse::Configuration = syn::parse2(quote! {
+            mqtt: {
+                auth: {
+                    #[tedge_config(deprecated_name = "cafile")]
+                    ca_file: u16,
+                }
+            },
+        })
+        .unwrap();
+
+        Configuration::try_from(input).unwrap();
+    }
+
+    #[test]
+    fn rename_accepts_valid_keys_for_groups() {
+        let input: super::super::parse::Configuration = syn::parse2(quote! {
+            mqtt: {
+                #[tedge_config(rename = "notbind")]
+                bind: {
+                    port: u16,
+                }
+            },
+        })
+        .unwrap();
+
+        Configuration::try_from(input).unwrap();
+    }
+
+    #[test]
+    fn deprecated_name_accepts_valid_names_for_groups() {
+        let input: super::super::parse::Configuration = syn::parse2(quote! {
+            mqtt: {
+                #[tedge_config(deprecated_name = "old_auth")]
+                auth: {
+                    ca_file: u16,
+                }
+            },
+        })
+        .unwrap();
+
+        Configuration::try_from(input).unwrap();
+    }
+
+    #[test]
+    fn group_name_is_derived_from_ident_if_not_renamed() {
+        let input: super::super::parse::Configuration = syn::parse2(quote! {
+            c8y: {
+                url: String,
+            }
+        })
+        .unwrap();
+
+        let configuration = Configuration::try_from(input).unwrap();
+
+        assert_eq!(configuration.groups[0].name(), "c8y")
+    }
+
+    #[test]
+    fn group_can_be_renamed() {
+        let input: super::super::parse::Configuration = syn::parse2(quote! {
+            #[tedge_config(rename = "cumulocity")]
+            c8y: {
+                url: String,
+            }
+        })
+        .unwrap();
+
+        let configuration = Configuration::try_from(input).unwrap();
+
+        assert_eq!(configuration.groups[0].name(), "cumulocity")
+    }
+
+    #[test]
+    fn field_name_is_derived_from_ident_if_not_renamed() {
+        let input: super::super::parse::ConfigurableField = syn::parse2(quote! {
+            ty: String
+        })
+        .unwrap();
+
+        let field = FieldOrGroup::Field(ConfigurableField::try_from(input).unwrap());
+
+        assert_eq!(field.name(), "ty")
+    }
+
+    #[test]
+    fn field_can_be_renamed() {
+        let input: super::super::parse::ConfigurableField = syn::parse2(quote! {
+            #[tedge_config(rename = "type")]
+            ty: String
+        })
+        .unwrap();
+
+        let field = FieldOrGroup::Field(ConfigurableField::try_from(input).unwrap());
+
+        assert_eq!(field.name(), "type")
+    }
+}
diff --git a/crates/common/tedge_config_macros/impl/src/lib.rs b/crates/common/tedge_config_macros/impl/src/lib.rs
new file mode 100644
index 00000000000..39a2ddafaaf
--- /dev/null
+++ b/crates/common/tedge_config_macros/impl/src/lib.rs
@@ -0,0 +1,191 @@
+//! This crate implements the macro for `tedge_config_macros` and should not be used directly.
+
+use heck::ToUpperCamelCase;
+use optional_error::OptionalError;
+use proc_macro2::Span;
+use proc_macro2::TokenStream;
+use quote::quote;
+use quote::quote_spanned;
+
+mod dto;
+mod error;
+mod input;
+mod optional_error;
+mod query;
+mod reader;
+
+#[doc(hidden)]
+pub fn generate_configuration(tokens: TokenStream) -> Result<TokenStream, syn::Error> {
+    let input: input::Configuration = syn::parse2(tokens)?;
+
+    let mut error = OptionalError::default();
+    let fields_with_keys = input
+        .groups
+        .iter()
+        .flat_map(|group| match group {
+            input::FieldOrGroup::Group(group) => unfold_group(Vec::new(), group),
+            input::FieldOrGroup::Field(field) => {
+                error.combine(syn::Error::new(
+                    field.ident().span(),
+                    "top level fields are not supported",
+                ));
+                vec![]
+            }
+        })
+        .collect::<Vec<_>>();
+    error.try_throw()?;
+
+    let example_tests = fields_with_keys
+        .iter()
+        .filter_map(|(key, field)| Some((key, field.read_write()?)))
+        .flat_map(|(key, field)| {
+            let ty = &field.ty;
+            field.examples.iter().enumerate().map(move |(n, example)| {
+                let name = quote::format_ident!(
+                    "example_value_can_be_deserialized_for_{}_example_{n}",
+                    key.join("_")
+                );
+                let span = example.span();
+                let example = example.as_ref();
+                let expect_message = format!(
+                    "Example value {example:?} for '{}' could not be deserialized",
+                    key.join(".")
+                );
+                quote_spanned! {span=>
+                    #[test]
+                    fn #name() {
+                        #example.parse::<#ty>().expect(#expect_message);
+                    }
+                }
+            })
+        })
+        .collect::<Vec<_>>();
+
+    let reader_name = proc_macro2::Ident::new("TEdgeConfigReader", Span::call_site());
+    let dto_doc_comment = format!(
+        "A data-transfer object, designed for reading and writing to
+        `tedge.toml`
+\n\
+        All the configurations inside this are optional to represent whether
+        the value is or isn't configured in the TOML file. Any defaults are
+        populated when this is converted to [{reader_name}] (via
+        [from_dto]({reader_name}::from_dto)).
+\n\
+        For simplicity when using this struct, only the fields are optional.
+        Any configuration groups (e.g. `device`, `c8y`, `mqtt.external`) are
+        always present. Groups that have no value set will be omitted in the
+        serialized output to avoid polluting `tedge.toml`."
+    );
+
+    let dto = dto::generate(
+        proc_macro2::Ident::new("TEdgeConfigDto", Span::call_site()),
+        &input.groups,
+        &dto_doc_comment,
+    );
+
+    let reader_doc_comment = "A struct to read configured values from, designed to be accessed only
+        via an immutable borrow
+\n\
+        The configurations inside this struct are optional only if the field
+        does not have a default value configured. This ensures that thin-edge
+        code only needs to handle possible errors where a field may not be
+        set.
+\n\
+        Where fields are optional, they are stored using [OptionalConfig] to
+        produce a descriptive error message that directs the user to set the
+        relevant key.";
+    let reader = reader::try_generate(reader_name, &input.groups, reader_doc_comment)?;
+
+    let enums = query::generate_writable_keys(&input.groups);
+
+    Ok(quote! {
+        #(#example_tests)*
+        #dto
+        #reader
+        #enums
+    })
+}
+
+fn unfold_group(
+    mut name: Vec<String>,
+    group: &input::ConfigurationGroup,
+) -> Vec<(Vec<String>, &input::ConfigurableField)> {
+    let mut output = Vec::new();
+    name.push(group.ident.to_string());
+    for field_or_group in &group.contents {
+        match field_or_group {
+            input::FieldOrGroup::Field(field) => {
+                let mut name = name.clone();
+                name.push(
+                    field
+                        .rename()
+                        .map(<_>::to_owned)
+                        .unwrap_or_else(|| field.ident().to_string()),
+                );
+                output.push((name, field))
+            }
+            input::FieldOrGroup::Group(group) => {
+                output.append(&mut unfold_group(name.clone(), group));
+            }
+        }
+    }
+
+    output
+}
+
+fn prefixed_type_name(
+    start: &proc_macro2::Ident,
+    group: &input::ConfigurationGroup,
+) -> proc_macro2::Ident {
+    quote::format_ident!(
+        "{start}{}",
+        group.ident.to_string().to_upper_camel_case(),
+        span = group.ident.span()
+    )
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    // TODO should these move to parse
+    #[test]
+    fn parse_basic_configuration_with_attributes() {
+        assert!(generate_configuration(quote! {
+            device: {
+                /// The id of the device
+                #[tedge_config(readonly(write_error = "Device id is derived from the certificate and cannot be written to", function = "device_id"))]
+                id: String,
+                /// The key path
+                #[tedge_config(example = "test")]
+                #[tedge_config(example = "tes2")]
+                key_path: Utf8Path,
+            }
+        })
+        .is_ok());
+    }
+
+    #[test]
+    fn parse_nested_groups() {
+        assert!(generate_configuration(quote! {
+            device: {
+                nested: {
+                    #[tedge_config(rename = "type")]
+                    ty: String,
+                },
+            },
+        })
+        .is_ok());
+    }
+
+    #[test]
+    fn serde_rename_is_not_allowd() {
+        assert!(generate_configuration(quote! {
+            device: {
+                #[serde(rename = "type")]
+                ty: String,
+            },
+        })
+        .is_err());
+    }
+}
diff --git a/crates/common/tedge_config_macros/impl/src/optional_error.rs b/crates/common/tedge_config_macros/impl/src/optional_error.rs
new file mode 100644
index 00000000000..a58eb61b338
--- /dev/null
+++ b/crates/common/tedge_config_macros/impl/src/optional_error.rs
@@ -0,0 +1,87 @@
+use syn::Error;
+
+/// Represents an `Option<syn::Error>`.
+#[derive(Default)]
+pub struct OptionalError(Option<Error>);
+
+impl OptionalError {
+    /// Removes the contained [error](Error) and returns it, if any.
+    pub fn take(&mut self) -> Option<Error> {
+        self.0.take()
+    }
+
+    /// Combine the given [error](Error) with the existing one,
+    /// initializing it if none currently exists.
+    pub fn combine(&mut self, error: Error) {
+        match self.0 {
+            None => self.0 = Some(error),
+            Some(ref mut prev) => prev.combine(error),
+        }
+    }
+
+    /// Combine the given [error](Error) with the existing one,
+    /// initializing it if none currently exists.
+    pub fn combine_owned(mut self, error: Error) -> Self {
+        match self.0 {
+            None => self.0 = Some(error),
+            Some(ref mut prev) => prev.combine(error),
+        };
+        self
+    }
+
+    /// Returns a [`Result`] with the contained [error](Error), if any.
+    ///
+    /// This can be used for quick and easy early returns.
+    pub fn try_throw(self) -> Result<(), Error> {
+        match self.0 {
+            None => Ok(()),
+            Some(err) => Err(err),
+        }
+    }
+}
+
+pub trait SynResultExt: Sized {
+    fn append_err_to(self, errors: &mut OptionalError);
+}
+
+impl SynResultExt for Result<(), syn::Error> {
+    fn append_err_to(self, errors: &mut OptionalError) {
+        if let Err(error) = self {
+            errors.combine(error);
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use proc_macro2::Span;
+
+    #[test]
+    fn should_combine() {
+        let mut collector = OptionalError(Some(Error::new(Span::call_site(), "First Error")));
+        collector.combine(Error::new(Span::call_site(), "Second Error"));
+
+        let expected = r#":: core :: compile_error ! { "First Error" } :: core :: compile_error ! { "Second Error" }"#;
+        let received = collector
+            .try_throw()
+            .expect_err("expected error")
+            .to_compile_error()
+            .to_string();
+        assert_eq!(expected, received);
+    }
+
+    #[test]
+    fn should_take() {
+        let mut collector = OptionalError(Some(Error::new(Span::call_site(), "First Error")));
+        let existing = collector.take();
+
+        let expected = r#":: core :: compile_error ! { "First Error" }"#;
+        let received = existing
+            .expect("expected error")
+            .to_compile_error()
+            .to_string();
+        assert_eq!(expected, received);
+        assert!(collector.try_throw().is_ok());
+    }
+}
diff --git a/crates/common/tedge_config_macros/impl/src/query.rs b/crates/common/tedge_config_macros/impl/src/query.rs
new file mode 100644
index 00000000000..3ddf3ba5d9c
--- /dev/null
+++ b/crates/common/tedge_config_macros/impl/src/query.rs
@@ -0,0 +1,410 @@
+use heck::ToUpperCamelCase;
+use proc_macro2::TokenStream;
+use quote::quote;
+use quote::quote_spanned;
+use std::collections::VecDeque;
+use syn::parse_quote;
+
+use crate::error::extract_type_from_result;
+use crate::input::ConfigurableField;
+use crate::input::FieldOrGroup;
+
+pub fn generate_writable_keys(items: &[FieldOrGroup]) -> TokenStream {
+    let paths = configuration_paths_from(items);
+    let (readonly_variant, write_error) = paths
+        .iter()
+        .filter_map(|field| {
+            Some((
+                variant_name(field),
+                field
+                    .back()?
+                    .field()?
+                    .read_only()?
+                    .readonly
+                    .write_error
+                    .as_str(),
+            ))
+        })
+        .unzip::<_, _, Vec<_>, Vec<_>>();
+    let readable_args = configuration_strings(paths.iter());
+    let readonly_args = configuration_strings(paths.iter().filter(|path| !is_read_write(path)));
+    let writable_args = configuration_strings(paths.iter().filter(|path| is_read_write(path)));
+    let readable_keys = keys_enum(parse_quote!(ReadableKey), &readable_args, "read from");
+    let readonly_keys = keys_enum(
+        parse_quote!(ReadOnlyKey),
+        &readonly_args,
+        "read from, but not written to,",
+    );
+    let writable_keys = keys_enum(parse_quote!(WritableKey), &writable_args, "written to");
+    let fromstr_readable = generate_fromstr_readable(parse_quote!(ReadableKey), &readable_args);
+    let fromstr_readonly = generate_fromstr_readable(parse_quote!(ReadOnlyKey), &readonly_args);
+    let fromstr_writable = generate_fromstr_writable(parse_quote!(WritableKey), &writable_args);
+    let read_string = generate_string_readers(&paths);
+    let write_string = generate_string_writers(
+        &paths
+            .iter()
+            .filter(|path| is_read_write(path))
+            .cloned()
+            .collect::<Vec<_>>(),
+    );
+    let (static_alias, updated_key) = deprecated_keys(paths.iter());
+
+    quote! {
+        #readable_keys
+        #readonly_keys
+        #writable_keys
+        #fromstr_readable
+        #fromstr_readonly
+        #fromstr_writable
+        #read_string
+        #write_string
+
+        #[derive(::thiserror::Error, Debug)]
+        /// An error encountered when writing to a configuration value from a
+        /// string
+        pub enum WriteError {
+            #[error("Failed to parse input")]
+            ParseValue(#[from] Box<dyn ::std::error::Error + Send + Sync>),
+        }
+
+        impl ReadOnlyKey {
+            fn write_error(self) -> &'static str {
+                match self {
+                    #(
+                        Self::#readonly_variant => #write_error,
+                    )*
+                }
+            }
+        }
+
+        #[derive(Debug, ::thiserror::Error)]
+        /// An error encountered when parsing a configuration key from a string
+        pub enum ParseKeyError {
+            #[error("{}", .0.write_error())]
+            ReadOnly(ReadOnlyKey),
+            #[error("Unknown key: '{0}'")]
+            Unrecognised(String),
+        }
+
+        fn replace_aliases(key: String) -> String {
+            use ::once_cell::sync::Lazy;
+            use ::std::borrow::Cow;
+            use ::std::collections::HashMap;
+            use ::doku::*;
+
+            static ALIASES: Lazy<HashMap<Cow<'static, str>, Cow<'static, str>>> = Lazy::new(|| {
+                let ty = TEdgeConfigReader::ty();
+                let TypeKind::Struct { fields, transparent: false } = ty.kind else { panic!("Expected struct but got {:?}", ty.kind) };
+                let Fields::Named { fields } = fields else { panic!("Expected named fields but got {:?}", fields)};
+                let mut aliases = struct_field_aliases(None, &fields);
+                #(
+                    if let Some(alias) = aliases.insert(Cow::Borrowed(#static_alias), Cow::Borrowed(ReadableKey::#updated_key.as_str())) {
+                        panic!("Duplicate configuration alias for '{}'. It maps to both '{}' and '{}'. Perhaps you provided an incorrect `deprecated_key` for one of these configurations?", #static_alias, alias, ReadableKey::#updated_key.as_str());
+                    }
+                )*
+                aliases
+            });
+
+            ALIASES
+                .get(&Cow::Borrowed(key.as_str()))
+                .map(|c| c.clone().into_owned())
+                .unwrap_or(key)
+        }
+
+        fn warn_about_deprecated_key(deprecated_key: String, updated_key: &'static str) {
+            use ::once_cell::sync::Lazy;
+            use ::std::sync::Mutex;
+            use ::std::collections::HashSet;
+
+            static WARNINGS: Lazy<Mutex<HashSet<String>>> = Lazy::new(<_>::default);
+
+            let warning = format!("The key '{}' is deprecated. Use '{}' instead.", deprecated_key, updated_key);
+            if WARNINGS.lock().unwrap().insert(deprecated_key) {
+                ::tracing::warn!("{}", warning);
+            }
+        }
+    }
+}
+
+fn configuration_strings<'a>(
+    variants: impl Iterator<Item = &'a VecDeque<&'a FieldOrGroup>>,
+) -> (Vec<String>, Vec<syn::Ident>) {
+    variants
+        .map(|segments| {
+            (
+                segments
+                    .iter()
+                    .map(|variant| variant.name())
+                    .collect::<Vec<_>>()
+                    .join("."),
+                variant_name(segments),
+            )
+        })
+        .unzip()
+}
+
+fn deprecated_keys<'a>(
+    variants: impl Iterator<Item = &'a VecDeque<&'a FieldOrGroup>>,
+) -> (Vec<&'a str>, Vec<syn::Ident>) {
+    variants
+        .flat_map(|segments| {
+            segments
+                .back()
+                .unwrap()
+                .field()
+                .unwrap()
+                .deprecated_keys()
+                .map(|key| (key, variant_name(segments)))
+        })
+        .unzip()
+}
+
+fn generate_fromstr(
+    type_name: syn::Ident,
+    (configuration_string, variant_name): &(Vec<String>, Vec<syn::Ident>),
+    error_case: syn::Arm,
+) -> TokenStream {
+    let simplified_configuration_string = configuration_string
+        .iter()
+        .map(|s| s.replace('.', "_"))
+        .zip(variant_name.iter())
+        .map(|(s, v)| quote_spanned!(v.span()=> #s));
+
+    quote! {
+        impl ::std::str::FromStr for #type_name {
+            type Err = ParseKeyError;
+            fn from_str(value: &str) -> Result<Self, Self::Err> {
+                // If we get an unreachable pattern, it means we have the same key twice
+                #[deny(unreachable_patterns)]
+                match replace_aliases(value.to_owned()).replace(".", "_").as_str() {
+                    #(
+                        #simplified_configuration_string => {
+                            if (value != #configuration_string) {
+                                warn_about_deprecated_key(value.to_owned(), #configuration_string);
+                            }
+                            Ok(Self::#variant_name)
+                        },
+                    )*
+                    #error_case
+                }
+            }
+        }
+    }
+}
+
+fn generate_fromstr_readable(
+    type_name: syn::Ident,
+    fields: &(Vec<String>, Vec<syn::Ident>),
+) -> TokenStream {
+    generate_fromstr(
+        type_name,
+        fields,
+        parse_quote! { _ => Err(ParseKeyError::Unrecognised(value.to_owned())) },
+    )
+}
+
+// TODO test the error messages actually appear
+fn generate_fromstr_writable(
+    type_name: syn::Ident,
+    fields: &(Vec<String>, Vec<syn::Ident>),
+) -> TokenStream {
+    generate_fromstr(
+        type_name,
+        fields,
+        parse_quote! {
+            _ => if let Ok(key) = <ReadOnlyKey as ::std::str::FromStr>::from_str(value) {
+                Err(ParseKeyError::ReadOnly(key))
+            } else {
+                Err(ParseKeyError::Unrecognised(value.to_owned()))
+            },
+        },
+    )
+}
+
+fn keys_enum(
+    type_name: syn::Ident,
+    (configuration_string, variant_name): &(Vec<String>, Vec<syn::Ident>),
+    doc_fragment: &'static str,
+) -> TokenStream {
+    let as_str_example = variant_name
+        .iter()
+        .zip(configuration_string.iter())
+        .map(|(ident, value)| format!("assert_eq!({type_name}::{ident}.as_str(), \"{value}\");\n"))
+        .take(10)
+        .collect::<Vec<_>>();
+    let as_str_example = (!as_str_example.is_empty()).then(|| {
+        quote! {
+            /// ```compile_fail
+            /// // This doctest is compile_fail because we have no way import the
+            /// // current type, but the example is still valuable
+            #(
+                #[doc = #as_str_example]
+            )*
+            /// ```
+        }
+    });
+    let type_name_str = type_name.to_string();
+
+    quote! {
+        #[derive(Copy, Clone, Debug, PartialEq, Eq)]
+        #[non_exhaustive]
+        #[allow(unused)]
+        #[doc = concat!("A key that can be *", #doc_fragment, "* the configuration\n\n")]
+        #[doc = concat!("This can be converted to `&'static str` using [`", #type_name_str, "::as_str`], and")]
+        #[doc = "parsed using [`FromStr`](::std::str::FromStr). The `FromStr` implementation also"]
+        #[doc = "automatically emits warnings about deprecated keys. It also implements [Display](std::fmt::Display),"]
+        #[doc = "so you can also use it in format strings."]
+        pub enum #type_name {
+            #(
+                #[doc = concat!("`", #configuration_string, "`")]
+                #variant_name,
+            )*
+        }
+
+        impl #type_name {
+            /// Converts this key to the canonical key used by `tedge config` and `tedge.toml`
+            #as_str_example
+            pub fn as_str(self) -> &'static str {
+                match self {
+                    #(
+                        Self::#variant_name => #configuration_string,
+                    )*
+                }
+            }
+
+            /// Iterates through all the variants of this enum
+            pub fn iter() -> impl Iterator<Item = Self> {
+                [
+                    #(
+                        Self::#variant_name,
+                    )*
+                ].into_iter()
+            }
+        }
+
+        impl ::std::fmt::Display for #type_name {
+            fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> {
+                self.as_str().fmt(f)
+            }
+        }
+    }
+}
+
+fn generate_string_readers(paths: &[VecDeque<&FieldOrGroup>]) -> TokenStream {
+    let variant_names = paths.iter().map(variant_name);
+    let arms = paths
+        .iter()
+        .zip(variant_names)
+        .map(|(path, variant_name)| -> syn::Arm {
+            let field = path
+                .back()
+                .expect("Path must have a back as it is nonempty")
+                .field()
+                .expect("Back of path is guaranteed to be a field");
+            let segments = path.iter().map(|thing| thing.ident());
+            if field.read_only().is_some() {
+                if extract_type_from_result(field.ty()).is_some() {
+                    parse_quote! {
+                        // Probably where the compiler error appears
+                        // TODO why do we need to unwrap
+                        ReadableKey::#variant_name => Ok(self.#(#segments).*.try_read(self)?.to_string()),
+                    }
+                } else {
+                    parse_quote! {
+                        // Probably where the compiler error appears
+                        // TODO why do we need to unwrap
+                        ReadableKey::#variant_name => Ok(self.#(#segments).*.read(self).to_string()),
+                    }
+                }
+            } else if field.has_guaranteed_default() {
+                parse_quote! {
+                    ReadableKey::#variant_name => Ok(self.#(#segments).*.to_string()),
+                }
+            } else {
+                parse_quote! {
+                    ReadableKey::#variant_name => Ok(self.#(#segments).*.or_config_not_set()?.to_string()),
+                }
+            }
+        });
+    quote! {
+        impl TEdgeConfigReader {
+            pub fn read_string(&self, key: ReadableKey) -> Result<String, ReadError> {
+                match key {
+                    #(#arms)*
+                }
+            }
+        }
+    }
+}
+
+fn generate_string_writers(paths: &[VecDeque<&FieldOrGroup>]) -> TokenStream {
+    let variant_names = paths.iter().map(variant_name);
+    let (update_arms, unset_arms): (Vec<syn::Arm>, Vec<syn::Arm>) = paths
+        .iter()
+        .zip(variant_names)
+        .map(|(path, variant_name)| {
+            let segments = path.iter().map(|thing| thing.ident()).collect::<Vec<_>>();
+
+            (
+                // TODO this should probably be spanned to the field type
+                parse_quote! {
+                    WritableKey::#variant_name => self.#(#segments).* = Some(value.parse().map_err(|e| WriteError::ParseValue(Box::new(e)))?),
+                },
+                parse_quote! {
+                    WritableKey::#variant_name => self.#(#segments).* = None,
+                }
+            )
+        }).unzip();
+    quote! {
+        impl TEdgeConfigDto {
+            pub fn try_update_str(&mut self, key: WritableKey, value: &str) -> Result<(), WriteError> {
+                match key {
+                    #(#update_arms)*
+                };
+                Ok(())
+            }
+
+            pub fn unset_key(&mut self, key: WritableKey) {
+                match key {
+                    #(#unset_arms)*
+                }
+            }
+        }
+    }
+}
+
+fn variant_name(segments: &VecDeque<&FieldOrGroup>) -> syn::Ident {
+    syn::Ident::new(
+        &segments
+            .iter()
+            .map(|segment| segment.name().to_upper_camel_case())
+            .collect::<String>(),
+        segments.iter().last().unwrap().ident().span(),
+    )
+}
+
+/// Generates a list of the toml paths for each of the keys in the provided
+/// configuration
+fn configuration_paths_from(items: &[FieldOrGroup]) -> Vec<VecDeque<&FieldOrGroup>> {
+    let mut res = vec![];
+    for item in items.iter().filter(|item| !item.reader().skip) {
+        match item {
+            FieldOrGroup::Field(_) => res.push(VecDeque::from([item])),
+            FieldOrGroup::Group(group) => {
+                for mut fields in configuration_paths_from(&group.contents) {
+                    fields.push_front(item);
+                    res.push(fields);
+                }
+            }
+        }
+    }
+    res
+}
+
+/// Checks if the field for the given path is read write
+fn is_read_write(path: &VecDeque<&FieldOrGroup>) -> bool {
+    matches!(
+        path.back(), // the field
+        Some(FieldOrGroup::Field(ConfigurableField::ReadWrite(_))),
+    )
+}
diff --git a/crates/common/tedge_config_macros/impl/src/reader.rs b/crates/common/tedge_config_macros/impl/src/reader.rs
new file mode 100644
index 00000000000..acfac9827d1
--- /dev/null
+++ b/crates/common/tedge_config_macros/impl/src/reader.rs
@@ -0,0 +1,357 @@
+//! Generation for the configuration readers
+//!
+//! When reading the configuration, we want to see default values if nothing has
+//! been configured
+use std::iter;
+
+use proc_macro2::TokenStream;
+use quote::quote;
+use quote::quote_spanned;
+use syn::parse_quote;
+use syn::parse_quote_spanned;
+use syn::punctuated::Punctuated;
+use syn::spanned::Spanned;
+use syn::Token;
+
+use crate::error::extract_type_from_result;
+use crate::input::ConfigurableField;
+use crate::input::FieldDefault;
+use crate::input::FieldOrGroup;
+use crate::optional_error::OptionalError;
+use crate::prefixed_type_name;
+
+pub fn try_generate(
+    root_name: proc_macro2::Ident,
+    items: &[FieldOrGroup],
+    doc_comment: &str,
+) -> syn::Result<TokenStream> {
+    let structs = generate_structs(&root_name, items, Vec::new(), doc_comment)?;
+    let conversions = generate_conversions(&root_name, items, vec![], items)?;
+    Ok(quote! {
+        #structs
+        #conversions
+    })
+}
+
+fn generate_structs(
+    name: &proc_macro2::Ident,
+    items: &[FieldOrGroup],
+    parents: Vec<syn::Ident>,
+    doc_comment: &str,
+) -> syn::Result<TokenStream> {
+    let mut idents = Vec::new();
+    let mut tys = Vec::<syn::Type>::new();
+    let mut sub_readers = Vec::new();
+    let mut attrs: Vec<Vec<syn::Attribute>> = Vec::new();
+    let mut lazy_readers = Vec::new();
+    let mut vis: Vec<syn::Visibility> = Vec::new();
+
+    for item in items {
+        match item {
+            FieldOrGroup::Field(field) => {
+                let ty = field.ty();
+                attrs.push(field.attrs().to_vec());
+                idents.push(field.ident());
+                if field.is_optional() {
+                    tys.push(parse_quote_spanned!(ty.span()=> OptionalConfig<#ty>));
+                } else if let Some(field) = field.read_only() {
+                    let name = field.lazy_reader_name(&parents);
+                    tys.push(parse_quote_spanned!(field.ty.span()=> #name));
+                    lazy_readers.push((name, &field.ty, &field.readonly.function));
+                } else {
+                    tys.push(ty.to_owned());
+                }
+                sub_readers.push(None);
+                vis.push(match field.reader().private {
+                    true => parse_quote!(),
+                    false => parse_quote!(pub),
+                });
+            }
+            FieldOrGroup::Group(group) if !group.reader.skip => {
+                let sub_reader_name = prefixed_type_name(name, group);
+                idents.push(&group.ident);
+                tys.push(parse_quote_spanned!(group.ident.span()=> #sub_reader_name));
+                let mut parents = parents.clone();
+                parents.push(group.ident.clone());
+                sub_readers.push(Some(generate_structs(
+                    &sub_reader_name,
+                    &group.contents,
+                    parents,
+                    "",
+                )?));
+                attrs.push(group.attrs.to_vec());
+                vis.push(match group.reader.private {
+                    true => parse_quote!(),
+                    false => parse_quote!(pub),
+                });
+            }
+            FieldOrGroup::Group(_) => {
+                // Skipped
+            }
+        }
+    }
+
+    let lazy_reader_impls = lazy_readers
+        .iter()
+        .map(|(name, ty, function)| -> syn::ItemImpl {
+            if let Some((ok, err)) = extract_type_from_result(ty) {
+                parse_quote_spanned! {name.span()=>
+                    impl #name {
+                        // TODO don't just guess we're called tedgeconfigreader
+                        pub fn try_read(&self, reader: &TEdgeConfigReader) -> Result<&#ok, #err> {
+                            self.0.get_or_try_init(|| #function(reader))
+                        }
+                    }
+                }
+            } else {
+                parse_quote_spanned! {name.span()=>
+                    impl #name {
+                        // TODO don't just guess we're called tedgeconfigreader
+                        pub fn read(&self, reader: &TEdgeConfigReader) -> &#ty {
+                            self.0.get_or_init(|| #function(reader))
+                        }
+                    }
+                }
+            }
+        });
+
+    let (lr_names, lr_tys): (Vec<_>, Vec<_>) = lazy_readers
+        .iter()
+        .map(|(name, ty, _)| match extract_type_from_result(ty) {
+            Some((ok, _err)) => (name, ok),
+            None => (name, *ty),
+        })
+        .unzip();
+
+    Ok(quote! {
+        #[derive(::doku::Document, ::serde::Serialize, Debug)]
+        #[non_exhaustive]
+        #[doc = #doc_comment]
+        pub struct #name {
+            #(
+                #(#attrs)*
+                #vis #idents: #tys,
+            )*
+        }
+
+        #(
+            #[derive(::serde::Serialize, Clone, Debug, Default)]
+            #[serde(into = "()")]
+            pub struct #lr_names(::once_cell::sync::OnceCell<#lr_tys>);
+
+            impl From<#lr_names> for () {
+                fn from(_: #lr_names) -> () {}
+            }
+
+            #lazy_reader_impls
+        )*
+
+        #(#sub_readers)*
+    })
+}
+
+fn find_field<'a>(
+    mut fields: &'a [FieldOrGroup],
+    key: &Punctuated<syn::Ident, Token![.]>,
+) -> syn::Result<&'a ConfigurableField> {
+    let mut current_field = None;
+    for (i, segment) in key.iter().enumerate() {
+        let target = fields
+            .iter()
+            .find(|field| field.is_called(segment))
+            .ok_or_else(|| {
+                syn::Error::new(
+                    segment.span(),
+                    format!(
+                        "no field named `{segment}` {}",
+                        current_field.map_or_else(
+                            || "at top level of configuration".to_owned(),
+                            |field: &FieldOrGroup| format!("in {}", field.ident())
+                        )
+                    ),
+                )
+            })?;
+
+        let is_last_segment = i == key.len() - 1;
+        match target {
+            FieldOrGroup::Group(group) => fields = &group.contents,
+            FieldOrGroup::Field(_) if is_last_segment => (),
+            _ => {
+                let string_path = key.iter().map(<_>::to_string).collect::<Vec<_>>();
+                let (successful_segments, subfields) = string_path.split_at(i + 1);
+                let successful_segments = successful_segments.join(".");
+                let subfields = subfields.join(".");
+                return Err(syn::Error::new(
+                    segment.span(),
+                    format!("cannot access `{subfields}` because `{successful_segments}` is a configuration field, not a group"),
+                ));
+            }
+        };
+        current_field = Some(target);
+    }
+
+    match current_field {
+        // TODO test this appears
+        None => Err(syn::Error::new(key.span(), "key is empty")),
+        Some(FieldOrGroup::Group(_)) => Err(syn::Error::new(
+            key.span(),
+            // TODO test this too
+            "path points to a group of fields, not a single field",
+        )),
+        Some(FieldOrGroup::Field(f)) => Ok(f),
+    }
+}
+
+fn reader_value_for_field<'a>(
+    field: &'a ConfigurableField,
+    parents: &[syn::Ident],
+    root_fields: &[FieldOrGroup],
+    mut observed_keys: Vec<&'a Punctuated<syn::Ident, Token![.]>>,
+) -> syn::Result<TokenStream> {
+    let name = field.ident();
+    Ok(match field {
+        ConfigurableField::ReadWrite(field) => {
+            let key = parents
+                .iter()
+                .map(|p| p.to_string())
+                .chain(iter::once(name.to_string()))
+                .collect::<Vec<_>>()
+                .join(".");
+            match &field.default {
+                FieldDefault::None => quote! {
+                    match &dto.#(#parents).*.#name {
+                        None => OptionalConfig::Empty(#key),
+                        Some(value) => OptionalConfig::Present { value: value.clone(), key: #key },
+                    }
+                },
+                FieldDefault::FromKey(key) if observed_keys.contains(&key) => {
+                    let string_paths = observed_keys
+                        .iter()
+                        .map(|path| {
+                            path.iter()
+                                .map(<_>::to_string)
+                                .collect::<Vec<_>>()
+                                .join(".")
+                        })
+                        .collect::<Vec<_>>();
+                    let error =
+                        format!("this path's default is part of a cycle ({string_paths:?})");
+                    // Safe to unwrap the error since observed_paths.len() >= 1
+                    return Err(observed_keys
+                        .into_iter()
+                        .map(|path| syn::Error::new(path.span(), &error))
+                        .fold(OptionalError::default(), |mut errors, error| {
+                            errors.combine(error);
+                            errors
+                        })
+                        .take()
+                        .unwrap());
+                }
+                FieldDefault::FromKey(default_key) | FieldDefault::FromOptionalKey(default_key) => {
+                    observed_keys.push(default_key);
+                    let default = reader_value_for_field(
+                        find_field(root_fields, default_key)?,
+                        &default_key
+                            .iter()
+                            .take(default_key.len() - 1)
+                            .map(<_>::to_owned)
+                            .collect::<Vec<_>>(),
+                        root_fields,
+                        observed_keys,
+                    )?;
+
+                    let (default, value) =
+                        if matches!(&field.default, FieldDefault::FromOptionalKey(_)) {
+                            (
+                                quote!(#default.map(|v| v.into())),
+                                quote!(OptionalConfig::Present { value: value.clone(), key: #key }),
+                            )
+                        } else {
+                            (quote!(#default.into()), quote!(value.clone()))
+                        };
+
+                    quote_spanned! {name.span()=>
+                        match &dto.#(#parents).*.#name {
+                            Some(value) => #value,
+                            None => #default,
+                        }
+                    }
+                }
+                FieldDefault::Function(function) => quote_spanned! {function.span()=>
+                    match &dto.#(#parents).*.#name {
+                        None => TEdgeConfigDefault::<TEdgeConfigDto, _>::call(#function, dto, location),
+                        Some(value) => value.clone(),
+                    }
+                },
+                FieldDefault::Value(default) => quote_spanned! {name.span()=>
+                    match &dto.#(#parents).*.#name {
+                        None => #default.into(),
+                        Some(value) => value.clone(),
+                    }
+                },
+                FieldDefault::Variable(default) => quote_spanned! {name.span()=>
+                    match &dto.#(#parents).*.#name {
+                        None => #default.into(),
+                        Some(value) => value.clone(),
+                    }
+                },
+            }
+        }
+        ConfigurableField::ReadOnly(field) => {
+            let name = field.lazy_reader_name(parents);
+            quote! {
+                #name::default()
+            }
+        }
+    })
+}
+
+/// Generate the conversion methods from DTOs to Readers
+fn generate_conversions(
+    name: &proc_macro2::Ident,
+    items: &[FieldOrGroup],
+    parents: Vec<syn::Ident>,
+    root_fields: &[FieldOrGroup],
+) -> syn::Result<TokenStream> {
+    let mut field_conversions = Vec::new();
+    let mut rest = Vec::new();
+
+    for item in items {
+        match item {
+            FieldOrGroup::Field(field) => {
+                let name = field.ident();
+                let value = reader_value_for_field(field, &parents, root_fields, Vec::new())?;
+                field_conversions.push(quote!(#name: #value));
+            }
+            FieldOrGroup::Group(group) if !group.reader.skip => {
+                let sub_reader_name = prefixed_type_name(name, group);
+                let name = &group.ident;
+
+                let mut parents = parents.clone();
+                parents.push(group.ident.clone());
+                field_conversions.push(quote!(#name: #sub_reader_name::from_dto(dto, location)));
+                let sub_conversions =
+                    generate_conversions(&sub_reader_name, &group.contents, parents, root_fields)?;
+                rest.push(sub_conversions);
+            }
+            FieldOrGroup::Group(_) => {
+                // Skipped
+            }
+        }
+    }
+
+    Ok(quote! {
+        impl #name {
+            #[allow(unused, clippy::clone_on_copy, clippy::useless_conversion)]
+            #[automatically_derived]
+            /// Converts the provided [TEdgeConfigDto] into a reader
+            pub fn from_dto(dto: &TEdgeConfigDto, location: &TEdgeConfigLocation) -> Self {
+                Self {
+                    #(#field_conversions),*
+                }
+            }
+        }
+
+        #(#rest)*
+    })
+}
diff --git a/crates/common/tedge_config_macros/macro/Cargo.toml b/crates/common/tedge_config_macros/macro/Cargo.toml
new file mode 100644
index 00000000000..0480ce03d3a
--- /dev/null
+++ b/crates/common/tedge_config_macros/macro/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "tedge_config_macros-macro"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[lib]
+proc-macro = true
+
+[dependencies]
+tedge_config_macros-impl = { version = "=0.1.0", path = "../impl" }
+syn = { version = "1", features = ["full", "extra-traits"] }
+quote = "1"
+proc-macro2 = "1"
diff --git a/crates/common/tedge_config_macros/macro/src/lib.rs b/crates/common/tedge_config_macros/macro/src/lib.rs
new file mode 100644
index 00000000000..e74340ffcdc
--- /dev/null
+++ b/crates/common/tedge_config_macros/macro/src/lib.rs
@@ -0,0 +1,15 @@
+//! This crate implements the macro for `tedge_config_macros` and should not be used directly.
+extern crate proc_macro;
+
+use proc_macro::TokenStream;
+use syn::parse_macro_input;
+
+#[proc_macro]
+pub fn define_tedge_config(item: TokenStream) -> TokenStream {
+    let item = parse_macro_input!(item as proc_macro2::TokenStream);
+
+    match tedge_config_macros_impl::generate_configuration(item) {
+        Ok(tokens) => tokens.into(),
+        Err(err) => TokenStream::from(err.to_compile_error()),
+    }
+}
diff --git a/crates/common/tedge_config_macros/src/all_or_nothing.rs b/crates/common/tedge_config_macros/src/all_or_nothing.rs
new file mode 100644
index 00000000000..6eb9e8f17d0
--- /dev/null
+++ b/crates/common/tedge_config_macros/src/all_or_nothing.rs
@@ -0,0 +1,172 @@
+use crate::OptionalConfig;
+
+/// An abstraction over "all or nothing" configurations
+///
+/// This is designed to be used with [all_or_nothing] to generate helpful error
+/// messages in cases where configuration values are mutually optional. See
+/// [all_or_nothing] for more information.
+pub trait MultiOption {
+    type Output;
+    fn extract_all(self) -> Result<Option<Self::Output>, PartialConfiguration>;
+}
+
+/// The keys which were and weren't provided as part of an all or nothing group
+pub struct PartialConfiguration {
+    present: Vec<&'static str>,
+    missing: Vec<&'static str>,
+}
+
+impl PartialConfiguration {
+    fn error_message(&self) -> String {
+        let mut all_settings = self.present.clone();
+        all_settings.append(&mut self.missing.clone());
+        let present = &self.present;
+        let missing = &self.missing;
+
+        format!(
+            "The thin-edge configuration is invalid. The settings {all_settings:?} \
+            must either all be configured, or all unset. Currently {present:?} are \
+            set, and {missing:?} are unset."
+        )
+    }
+}
+
+impl<T, U> MultiOption for (OptionalConfig<T>, OptionalConfig<U>) {
+    type Output = (T, U);
+    fn extract_all(self) -> Result<Option<Self::Output>, PartialConfiguration> {
+        use OptionalConfig::*;
+        match self {
+            (Present { value: t, .. }, Present { value: u, .. }) => Ok(Some((t, u))),
+            (Empty(..), Empty(..)) => Ok(None),
+            (t, u) => {
+                let present = [t.key_if_present(), u.key_if_present()]
+                    .into_iter()
+                    .flatten()
+                    .collect::<Vec<_>>();
+                let missing = [t.key_if_empty(), u.key_if_empty()]
+                    .into_iter()
+                    .flatten()
+                    .collect::<Vec<_>>();
+                Err(PartialConfiguration { present, missing })
+            }
+        }
+    }
+}
+
+/// Combine a set of optional configurations into a single option
+///  
+/// # Errors
+/// This will fail in the case that some, but not all the configurations are provided.
+///
+/// ```
+/// use tedge_config_macros::*;
+/// use camino::Utf8PathBuf;
+/// use std::path::PathBuf;
+///
+/// #[derive(thiserror::Error, Debug)]
+/// pub enum ReadError {
+///     #[error(transparent)]
+///     ConfigNotSet(#[from] ConfigNotSet),
+/// }
+///
+/// define_tedge_config! {
+///     mqtt: {
+///         auth: {
+///             #[doku(as = "PathBuf")]
+///             cert_file: Utf8PathBuf,
+///
+///             #[doku(as = "PathBuf")]
+///             key_file: Utf8PathBuf,
+///         }
+///     }
+/// }
+///
+/// let mut dto = TEdgeConfigDto::default();
+/// let reader = TEdgeConfigReader::from_dto(&dto, &TEdgeConfigLocation::default());
+/// assert!(matches!(
+///     all_or_nothing((reader.mqtt.auth.cert_file.as_ref(), reader.mqtt.auth.key_file.as_ref())),
+///     Ok(None)
+/// ));
+///
+/// dto.mqtt.auth.cert_file = Some("/etc/tedge/mqtt-certs/auth.crt".into());
+/// let reader = TEdgeConfigReader::from_dto(&dto, &TEdgeConfigLocation::default());
+/// assert!(matches!(
+///     all_or_nothing((reader.mqtt.auth.cert_file.as_ref(), reader.mqtt.auth.key_file.as_ref())),
+///     Err(_)
+/// ));
+///
+/// dto.mqtt.auth.key_file = Some("/etc/tedge/mqtt-certs/key.cert".into());
+/// let reader = TEdgeConfigReader::from_dto(&dto, &TEdgeConfigLocation::default());
+/// assert!(matches!(
+///     all_or_nothing((reader.mqtt.auth.cert_file.as_ref(), reader.mqtt.auth.key_file.as_ref())),
+///     Ok(Some((_, _)))
+/// ));
+/// ```
+pub fn all_or_nothing<Configs: MultiOption>(
+    input: Configs,
+) -> Result<Option<Configs::Output>, String> {
+    input
+        .extract_all()
+        .map_err(|partial_config| partial_config.error_message())
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn all_or_nothing_returns_some_when_both_values_are_configured() {
+        assert_eq!(
+            all_or_nothing((
+                OptionalConfig::Present {
+                    value: "first",
+                    key: "test.key"
+                },
+                OptionalConfig::Present {
+                    value: "second",
+                    key: "test.key2"
+                }
+            )),
+            Ok(Some(("first", "second")))
+        )
+    }
+
+    #[test]
+    fn all_or_nothing_returns_none_when_both_values_when_neither_value_is_configured() {
+        assert_eq!(
+            all_or_nothing((
+                OptionalConfig::<String>::Empty("first.key"),
+                OptionalConfig::<String>::Empty("second.key")
+            )),
+            Ok(None)
+        )
+    }
+
+    #[test]
+    fn all_or_nothing_returns_an_error_if_only_the_first_value_is_configured() {
+        assert!(matches!(
+            all_or_nothing((
+                OptionalConfig::Present {
+                    value: "test",
+                    key: "first.key"
+                },
+                OptionalConfig::<String>::Empty("second.key")
+            )),
+            Err(_)
+        ))
+    }
+
+    #[test]
+    fn all_or_nothing_returns_an_error_if_only_the_second_value_is_configured() {
+        assert!(matches!(
+            all_or_nothing((
+                OptionalConfig::<String>::Empty("first.key"),
+                OptionalConfig::Present {
+                    value: "test",
+                    key: "second.key"
+                },
+            )),
+            Err(_)
+        ))
+    }
+}
diff --git a/crates/common/tedge_config_macros/src/connect_url.rs b/crates/common/tedge_config_macros/src/connect_url.rs
new file mode 100644
index 00000000000..c3a59f4da62
--- /dev/null
+++ b/crates/common/tedge_config_macros/src/connect_url.rs
@@ -0,0 +1,99 @@
+use std::convert::TryFrom;
+use std::fmt;
+use std::str::FromStr;
+use url::Host;
+
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Eq, PartialEq)]
+#[serde(try_from = "String", into = "String")]
+pub struct ConnectUrl {
+    input: String,
+    host: Host,
+}
+
+impl doku::Document for ConnectUrl {
+    fn ty() -> doku::Type {
+        String::ty()
+    }
+}
+
+#[derive(thiserror::Error, Debug)]
+#[error(
+    "Provided URL: '{input}' contains scheme or port.
+         Provided URL should contain only domain, eg: 'subdomain.cumulocity.com'."
+)]
+pub struct InvalidConnectUrl {
+    input: String,
+    error: url::ParseError,
+}
+
+impl TryFrom<String> for ConnectUrl {
+    type Error = InvalidConnectUrl;
+
+    fn try_from(input: String) -> Result<Self, Self::Error> {
+        match Host::parse(&input) {
+            Ok(host) => Ok(Self { input, host }),
+            Err(error) => Err(InvalidConnectUrl { input, error }),
+        }
+    }
+}
+
+impl FromStr for ConnectUrl {
+    type Err = InvalidConnectUrl;
+
+    fn from_str(input: &str) -> Result<Self, Self::Err> {
+        ConnectUrl::try_from(input.to_string())
+    }
+}
+
+impl ConnectUrl {
+    pub fn as_str(&self) -> &str {
+        self.input.as_str()
+    }
+}
+
+impl fmt::Display for ConnectUrl {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        self.input.fmt(f)
+    }
+}
+
+impl From<ConnectUrl> for String {
+    fn from(val: ConnectUrl) -> Self {
+        val.input
+    }
+}
+
+impl From<ConnectUrl> for Host {
+    fn from(val: ConnectUrl) -> Self {
+        val.host
+    }
+}
+
+#[test]
+fn connect_url_from_string_should_return_err_provided_address_with_port() {
+    let input = "test.address.com:8883";
+
+    assert!(ConnectUrl::from_str(input).is_err());
+}
+
+#[test]
+fn connect_url_from_string_should_return_err_provided_address_with_scheme_http() {
+    let input = "http://test.address.com";
+
+    assert!(ConnectUrl::from_str(input).is_err());
+}
+
+#[test]
+fn connect_url_from_string_should_return_err_provided_address_with_port_and_http() {
+    let input = "http://test.address.com:8883";
+
+    assert!(ConnectUrl::from_str(input).is_err());
+}
+
+#[test]
+fn connect_url_from_string_should_return_string() {
+    let input = "test.address.com";
+    let expected = "test.address.com";
+
+    assert_eq!(ConnectUrl::from_str(input).unwrap().as_str(), expected);
+}
diff --git a/crates/common/tedge_config_macros/src/default.rs b/crates/common/tedge_config_macros/src/default.rs
new file mode 100644
index 00000000000..74c97a8a487
--- /dev/null
+++ b/crates/common/tedge_config_macros/src/default.rs
@@ -0,0 +1,63 @@
+use camino::Utf8Path;
+
+/// An abstraction over the possible default functions for tedge config values
+///
+/// Some configuration defaults are relative to the config location, and
+/// this trait allows us to pass that in, or the DTO, both, or neither!
+pub trait TEdgeConfigDefault<T, Args> {
+    type Output;
+    fn call(self, data: &T, location: &TEdgeConfigLocation) -> Self::Output;
+}
+
+/// A dummy tedge config location
+///
+/// In practice, we use `TEdgeConfigLocation` from the `tedge_config` crate.
+/// This is defined simply to allow us to create examples in this crate.
+#[derive(Default)]
+pub struct TEdgeConfigLocation;
+
+impl TEdgeConfigLocation {
+    pub fn tedge_config_root_path(&self) -> &Utf8Path {
+        "/etc/tedge".into()
+    }
+}
+
+impl<F, Out, T> TEdgeConfigDefault<T, ()> for F
+where
+    F: FnOnce() -> Out + Clone,
+{
+    type Output = Out;
+    fn call(self, _: &T, _: &TEdgeConfigLocation) -> Self::Output {
+        (self)()
+    }
+}
+
+impl<F, Out, T> TEdgeConfigDefault<T, &T> for F
+where
+    F: FnOnce(&T) -> Out + Clone,
+{
+    type Output = Out;
+    fn call(self, data: &T, _location: &TEdgeConfigLocation) -> Self::Output {
+        (self)(data)
+    }
+}
+
+impl<F, Out, T> TEdgeConfigDefault<T, (&TEdgeConfigLocation,)> for F
+where
+    F: FnOnce(&TEdgeConfigLocation) -> Out + Clone,
+{
+    type Output = Out;
+    fn call(self, _data: &T, location: &TEdgeConfigLocation) -> Self::Output {
+        (self)(location)
+    }
+}
+
+impl<F, Out, T> TEdgeConfigDefault<T, (&T, &TEdgeConfigLocation)> for F
+where
+    F: FnOnce(&T, &TEdgeConfigLocation) -> Out + Clone,
+{
+    type Output = Out;
+    fn call(self, data: &T, location: &TEdgeConfigLocation) -> Self::Output {
+        (self)(data, location)
+    }
+}
diff --git a/crates/common/tedge_config_macros/src/define_tedge_config_docs.md b/crates/common/tedge_config_macros/src/define_tedge_config_docs.md
new file mode 100644
index 00000000000..11b351fd76c
--- /dev/null
+++ b/crates/common/tedge_config_macros/src/define_tedge_config_docs.md
@@ -0,0 +1,577 @@
+Defines the necessary structures to create a tedge config struct
+
+For a complete example of its usage, see the `macros.rs` file inside the
+examples folder of this crate.
+
+# Output
+This macro outputs a few different types:
+- `TEdgeConfigDto` ([example](example::TEdgeConfigDto)) --- A data-transfer
+  object, used for reading and writing to toml
+- `TEdgeConfigReader` ([example](example::TEdgeConfigReader)) --- A struct to
+  read configured values from, populating values with defaults if they exist
+- `ReadableKey` ([example](example::ReadableKey)) --- An enum of all the
+  possible keys that can be read from the configuration, with
+  [`FromStr`](std::str::FromStr) and [`Display`](std::fmt::Display)
+  implementations.
+- `WritableKey` ([example](example::WritableKey)) --- An enum of all the
+  possible keys that can be written to the configuration, with
+  [`FromStr`](std::str::FromStr) and [`Display`](std::fmt::Display)
+  implementations.
+- `ReadOnlyKey` ([example](example::ReadOnlyKey)) --- An enum of all the
+  possible keys that can be read from, but not written to, the configuration,
+  with [`FromStr`](std::str::FromStr) and [`Display`](std::fmt::Display)
+  implementations.
+
+# Attributes
+## `#[tedge_config(...)]` attributes
+| Attribute                                     | Supported for                                 | Summary                                                                   |
+| --------------------------------------------- | --------------------------------------------- | ------------------------------------------------------------------------- |
+| [`rename`](#rename)                           | fields/groups                                 | Renames a field or group in serde and the `tedge config` key              |
+| [`deprecated_name`](#dep-name)                | fields/groups                                 | Adds an alias for a field or group in serde/`tedge config`                |
+| [`deprecated_key`](#dep-key)                  | fields                                        | Adds an alias for the field or group in serde/`tedge config`              |
+| **Doc comments**                              | [fields](#docs-fields)/[groups](#docs-groups) | Adds a description of a key in `tedge config` docs                        |
+| [`example`](#examples)                        | fields                                        | Adds an example value to `tedge config` docs                              |
+| [`note`](#notes)                              | fields                                        | Adds a highlighted note to `tedge config` docs                            |
+| [`reader(skip)`](#reader-skip)                | groups                                        | Omits a group from the reader struct entirely                             |
+| [`reader(private)`](#reader-priv)             | fields/groups                                 | Stops the field from the reader struct being marked with `pub`            |
+| [`default(value)`](#default-lit)              | fields                                        | Sets the default value for a field from a literal                         |
+| [`default(variable)`](#default-var)           | fields                                        | Sets the default value for a field from a variable                        |
+| [`default(from_key)`](#from-key)              | fields                                        | Sets the default value for a field to the value of another field          |
+| [`default(from_optional_key)`](#from-opt-key) | fields                                        | Sets the default value for a field to the value of another field          |
+| [`default(function)`](#default-fn)            | fields                                        | Specifies a function that will be used to compute a field's default value |
+| [`readonly(...)`](#readonly)                  | fields                                        | Marks a field as read-only                                                |
+
+## Other attributes
+### `#[doku(as = "...")]`
+Some types are not known to [`doku`], and therefore do not implement
+[`doku::Document`] (which is required for all fields). In order to resolve this
+error, you can use a `#[doku(as = "...")]` attribute like so:
+
+```rust
+# use tedge_config_macros::*;
+# #[derive(::thiserror::Error, Debug)]
+# pub enum ReadError { #[error(transparent)] NotSet(#[from] ConfigNotSet)}
+use camino::Utf8PathBuf;
+use std::path::PathBuf;
+
+define_tedge_config! {
+  device: {
+    #[doku(as = "PathBuf")]
+    cert_path: Utf8PathBuf,
+  }
+}
+```
+
+The actual type information isn't currently used for anything, but it's usually
+possible to find a close match. Custom types can also implement the trait very
+easily.
+
+```rust
+# use tedge_config_macros::*;
+# #[derive(::thiserror::Error, Debug)]
+# pub enum ReadError { #[error(transparent)] NotSet(#[from] ConfigNotSet)}
+#
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Eq, PartialEq)]
+#[serde(transparent)]
+pub struct ConnectUrl(String); 
+
+impl doku::Document for ConnectUrl {
+    fn ty() -> doku::Type {
+        // Just call `ty()` on a similar enough type
+        String::ty()
+    }
+}
+
+define_tedge_config! {
+  c8y: {
+    // We now don't need `#[doku(as = "String")]`
+    url: ConnectUrl,
+  }
+}
+#
+# impl std::str::FromStr for ConnectUrl {
+#   type Err = std::convert::Infallible;
+#   fn from_str(s: &str) -> Result<Self, Self::Err> { Ok(Self(s.to_owned())) }
+# }
+# impl std::fmt::Display for ConnectUrl {
+#   fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+#       self.0.fmt(f)
+#   }
+# }
+```
+
+## Denied attributes
+Some `tedge_config` attributes are translated under the hood to `serde` or
+`doku` attributes, as well as adding some additional behaviour (e.g. [example
+tests](#example-tests)). To avoid these attributes being used directly,
+`define_tedge_config!` will emit a compiler error if you attempt to use these
+attributes.
+
+```rust compile_fail
+# use tedge_config_macros::*;
+# #[derive(::thiserror::Error, Debug)]
+# pub enum ReadError { #[error(transparent)] NotSet(#[from] ConfigNotSet)}
+#
+define_tedge_config! {
+  device: {
+    #[serde(rename = "type")]
+    ty: String,
+  }
+}
+```
+
+## Naming
+### <a name="rename"></a>Customising config keys: `#[tedge_config(rename = "new_name")]`
+```rust
+# use tedge_config_macros::*;
+# #[derive(::thiserror::Error, Debug)]
+# pub enum ReadError { #[error(transparent)] NotSet(#[from] ConfigNotSet)}
+#
+define_tedge_config! {
+  device: {
+    #[tedge_config(rename = "type")]
+    ty: String,
+  }
+}
+
+assert!("device.ty".parse::<WritableKey>().is_err());
+assert!("device.type".parse::<WritableKey>().is_ok());
+```
+
+Fields and groups can be renamed (i.e. such that the field or group name as
+observed by serde and `tedge config` does not match the Rust identifier) using
+the `rename` attribute. This is generally useful for keys like `device.type`,
+where the `type` field conflicts with the Rust `type` keyword. Instead we can
+call the field `ty` and rename it to `type` as shown above (which is more
+ergonomic and idiomatic than using a [raw
+identifier](https://doc.rust-lang.org/rust-by-example/compatibility/raw_identifiers.html)
+to solve the problem instead).
+
+### <a name="dep-name"></a>Deprecated names: `#[tedge_config(deprecated_name = "old_name")]`
+```rust
+# use tedge_config_macros::*;
+# #[derive(::thiserror::Error, Debug)]
+# pub enum ReadError { #[error(transparent)] NotSet(#[from] ConfigNotSet)}
+#
+define_tedge_config! {
+  group: {
+    #[tedge_config(deprecated_name = "old_name")]
+    new_name: String,
+  }
+}
+
+assert_eq!(
+  "group.old_name".parse::<WritableKey>().unwrap(),
+  "group.new_name".parse::<WritableKey>().unwrap(),
+);
+```
+
+Fields and groups can be updated in a backwards compatible manner (i.e. the name
+can be changed between different thin-edge.io versions)  with the
+`deprecated_name` attribute. This is useful for e.g. renaming the `azure` group
+to `az`, or renaming a field to include an `_` when it previously didn't.
+
+The alias that is created will apply to keys supplied to `tedge config`, values
+read from `tedge.toml` and `TEDGE_` environment variables, and the field will be
+automatically renamed in `tedge.toml` next time `tedge` writes to `tedge.toml`.
+
+### <a name="dep-key"></a>Deprecated keys: `#[tedge_config(deprecated_key = "some.old.key")]`
+```rust
+# use tedge_config_macros::*;
+# #[derive(::thiserror::Error, Debug)]
+# pub enum ReadError { #[error(transparent)] NotSet(#[from] ConfigNotSet)}
+#
+define_tedge_config! {
+  mqtt: {
+    bind: {  
+      #[tedge_config(deprecated_key = "mqtt.port")]
+      port: u16,
+    }
+  }
+}
+
+assert_eq!(
+  "mqtt.port".parse::<WritableKey>().unwrap(),
+  "mqtt.bind.port".parse::<WritableKey>().unwrap(),
+);
+```
+More complex field name updates can be carried out with the `deprecated_key`
+attribute. This is required when a field is moved to a different group, e.g.
+renaming `mqtt.port` to `mqtt.bind.port`.
+
+#### Note
+The `deprecated_key` attribute only adds aliases that will be handled by `tedge
+config` and `TEDGE_` environment variables, you will also need to add a TOML
+migration step if making a change like this to ensure the field is moved in
+`tedge.toml` too.
+
+## Documentation
+### <a></a>Doc comments: `/// Comment` or `#[doc = "Comment"]`
+```rust
+# use tedge_config_macros::*;
+# #[derive(::thiserror::Error, Debug)]
+# pub enum ReadError { #[error(transparent)] NotSet(#[from] ConfigNotSet)}
+#
+define_tedge_config! {
+  /// You can document groups too, but this won't be shown to users
+  az: {
+    /// Endpoint URL of Azure IoT tenant
+    url: ConnectUrl,
+  }
+}
+```
+
+<a name="docs-fields"></a>Doc comments for **fields** are automatically
+converted to documentation in `tedge config list --doc`. All line breaks are
+removed before this. They are also preserved on the generated `Reader` and `Dto`
+structs.
+
+<a name="docs-groups"></a>Doc comments for **groups** are preserved on the
+`Reader` and `Dto` structs, and ignored by `tedge config list --doc`.
+
+### <a name="examples"></a>Examples: `#[tedge_config(example = "value")]`
+```rust
+# use tedge_config_macros::*;
+# #[derive(::thiserror::Error, Debug)]
+# pub enum ReadError { #[error(transparent)] NotSet(#[from] ConfigNotSet)}
+#
+define_tedge_config! {
+  az: {
+    #[tedge_config(example = "myazure.azure-devices.net")]
+    url: ConnectUrl,
+
+    mapper: {
+      // Examples are always specified using string literals
+      #[tedge_config(example = "true")]
+      timestamp: bool,
+    }
+  }
+}
+```
+
+Examples can be added using `#[tedge_config(example = "value")]`. The value
+provided must be a string literal.
+
+#### Example tests
+A test will automatically be generated for each example field to check that the
+provided value can be deserialised to the field's type (i.e. the example would
+work if used with `tedge config set`)
+
+For the code above, `define_tedge_config!` will create a test to verify
+`"myazure.azure-devices.net".parse::<ConnectUrl>().is_ok()` and another test
+will verify `"true".parse::<bool>().is_ok()`.
+
+### <a name="notes"></a>Notes 
+```rust
+# use tedge_config_macros::*;
+# #[derive(::thiserror::Error, Debug)]
+# pub enum ReadError { #[error(transparent)] NotSet(#[from] ConfigNotSet)}
+use std::path::PathBuf;
+use camino::Utf8PathBuf;
+
+define_tedge_config! {
+  c8y: {
+    #[tedge_config(note = "The value can be a directory path as well as the path of the direct certificate file.")]
+    #[doku(as = "PathBuf")]
+    root_cert_path: Utf8PathBuf,
+  }
+}
+```
+
+Additional notes about fields can be added using `#[tedge_config(note =
+"content")]`. This will be added to `tedge config list --doc` on a seperate
+line, with a coloured heading to make it more distinctive.
+
+## Reader: `#[tedge_config(reader(...))]`
+There are some options to customise the fields in the generated `Reader` struct.
+
+### <a name="reader-skip"></a> Skipping fields: `#[tedge_config(reader(skip))]`
+```rust
+# use tedge_config_macros::*;
+# #[derive(::thiserror::Error, Debug)]
+# pub enum ReadError { #[error(transparent)] NotSet(#[from] ConfigNotSet)}
+#
+define_tedge_config! {
+  #[tedge_config(reader(skip))]
+  config: {
+    version: u32,
+  }
+}
+
+assert!("config.version".parse::<WritableKey>().is_err());
+```
+
+Groups can be omitted from the `Reader` struct entirely. This was added to
+support the `config.version` field, which is used to manage `tedge.toml`
+migrations. Since this is just a detail about the `tedge.toml` version, it must
+not be exposed in `tedge config` or to other tedge crates. Omitting the group
+from the reader using this attribute
+
+### <a name="reader-priv"></a> Private fields: `#[tedge_config(reader(private))]`
+```rust compile_fail
+# use tedge_config_macros::*;
+# #[derive(::thiserror::Error, Debug)]
+# pub enum ReadError { #[error(transparent)] NotSet(#[from] ConfigNotSet)}
+#
+mod tedge_config {
+  define_tedge_config! {
+    #[tedge_config(reader(skip))]
+    config: {
+      version: u32,
+    }
+  }
+}
+
+use tedge_config::*;
+
+let reader = TEdgeConfigReader::from_dto(&TEdgeConfigDto::default(), &TEdgeConfigLocation::default());
+println!("{}", tedge_config::reader.config.version); // compile error! The field is not public
+```
+
+Occasionally, you may want to add custom logic, such as
+[`all_or_nothing`](`crate::all_or_nothing::all_or_nothing`) to field accesses.
+This attribute prevents the relevant `Reader` struct field from being marked
+with `pub`, which prevents other crates from accessing the value directly. The
+value is still exposed via `tedge config` to read from and write to.
+
+## Defaults
+There are a variety of ways to specify default values for fields. Supplying a
+default value for a field will result in the reader field being non-optional
+(with the exception of [`from_optional_key`](#from-opt-key)). 
+
+### <a name="default-lit"></a> Literals: `#[tedge_config(default(value = ...))]`
+```rust
+# use tedge_config_macros::*;
+# #[derive(::thiserror::Error, Debug)]
+# pub enum ReadError { #[error(transparent)] NotSet(#[from] ConfigNotSet)}
+#
+define_tedge_config! {
+  mqtt: {
+    bind: {
+      #[tedge_config(default(value = 1883_u16))]
+      port: u16,
+    }
+  }
+}
+
+let reader = TEdgeConfigReader::from_dto(&TEdgeConfigDto::default(), &TEdgeConfigLocation::default());
+assert_eq!(reader.mqtt.bind.port, 1883);
+```
+If the value can be specified as a literal (e.g. for `bool`, primitive numeric
+types (`u16`, `i32`, etc.), and strings), you can use the `value` specifier for
+a default value.
+
+The implematation calls `.into()` on the provided value, so any field with a
+type that implements `From<T>`, where `T` is the type of the literal, can be
+filled using this method. As shown above, numeric literals may have to specify
+their type [using a
+suffix](https://doc.rust-lang.org/rust-by-example/primitives/literals.html). As
+the value is a simply a literal, it does not have to be quoted.
+
+### <a name="default-var"></a> Variables/constants: `#[tedge_config(default(variable = "..."))]`
+```rust
+# use tedge_config_macros::*;
+# #[derive(::thiserror::Error, Debug)]
+# pub enum ReadError { #[error(transparent)] NotSet(#[from] ConfigNotSet)}
+use std::net::IpAddr;
+use std::net::Ipv4Addr;
+use camino::Utf8PathBuf;
+use std::path::PathBuf;
+
+const DEFAULT_CERT_PATH: &str = "/etc/ssl/certs";
+
+define_tedge_config! {
+  mqtt: {
+    bind: {
+      #[tedge_config(default(variable = "Ipv4Addr::LOCALHOST"))]
+      address: IpAddr,
+    }
+  },
+  c8y: {
+    // This will default to `DEFAULT_CERT_PATH.into()`, so we
+    // can use a `&str` const as the default for this field
+    #[tedge_config(default(variable = "DEFAULT_CERT_PATH"))]
+    #[doku(as = "PathBuf")]
+    root_cert_path: Utf8PathBuf,
+  }
+}
+
+let reader = TEdgeConfigReader::from_dto(&TEdgeConfigDto::default(), &TEdgeConfigLocation::default());
+assert_eq!(reader.mqtt.bind.address, Ipv4Addr::LOCALHOST);
+```
+
+Instead of providing a value as a literal, you can a reference a `const` value
+using the `variable` specifier. Like `value`, the generated implematation calls
+`.into()` on the constant.
+
+### <a name="from-key"></a> Fallback keys/derived keys `#[tedge_config(default(from_key = "..."))]`
+```rust
+# use tedge_config_macros::*;
+# #[derive(::thiserror::Error, Debug)]
+# pub enum ReadError { #[error(transparent)] NotSet(#[from] ConfigNotSet)}
+#
+define_tedge_config! {
+  mqtt: {
+    bind: {
+      #[tedge_config(default(value = 1883_u16))]
+      port: u16
+    },
+    client: {
+      #[tedge_config(default(from_key = "mqtt.bind.port"))]
+      port: u16,
+    }
+  }
+}
+
+let mut dto = TEdgeConfigDto::default();
+let reader = TEdgeConfigReader::from_dto(&dto, &TEdgeConfigLocation::default());
+assert_eq!(reader.mqtt.client.port, reader.mqtt.bind.port);
+
+dto.mqtt.bind.port = Some(2387);
+let reader = TEdgeConfigReader::from_dto(&dto, &TEdgeConfigLocation::default());
+assert_eq!(reader.mqtt.client.port, reader.mqtt.bind.port);
+```
+
+Using `from_key`, the default value can be derived from the value of another
+field. This allows the provided key to act as the default/fallback value for the
+field.
+
+| `dto.mqtt.bind.port` | `dto.mqtt.client.port` | `reader.mqtt.bind.port` | `reader.mqtt.client.port` |
+| -------------------- | ---------------------- | ----------------------- | ------------------------- |
+| `None`               | `None`                 | `1883`                  | `1883`                    |
+| `Some(5678)`         | `None`                 | `5678`                  | `5678`                    |
+| `None`               | `Some(1234)`           | `1883`                  | `1234`                    |
+| `Some(5678)`         | `Some(1234)`           | `1234`                  | `1234`                    |
+
+### <a name="from-opt-key"></a> Optional fallback keys `#[tedge_config(default(from_optional_key = "..."))]`
+```rust
+# use tedge_config_macros::*;
+# #[derive(::thiserror::Error, Debug)]
+# pub enum ReadError { #[error(transparent)] NotSet(#[from] ConfigNotSet)}
+#
+define_tedge_config! {
+  c8y: {
+    url: ConnectUrl,
+
+    /// The endpoint used for HTTP communication with Cumulocity
+    #[tedge_config(note = "This will fall back to the value of 'c8y.url' if it is not set")]
+    #[tedge_config(default(from_optional_key = "c8y.url"))]
+    http: ConnectUrl,
+  }
+}
+
+let mut dto = TEdgeConfigDto::default();
+let reader = TEdgeConfigReader::from_dto(&dto, &TEdgeConfigLocation::default());
+assert_eq!(reader.c8y.url, reader.c8y.http);
+
+let not_set_err = reader.c8y.http.or_config_not_set().unwrap_err();
+assert!(not_set_err.to_string().contains("'c8y.url'"));
+
+dto.c8y.url = Some("test.cumulocity.com".parse().unwrap());
+let reader = TEdgeConfigReader::from_dto(&dto, &TEdgeConfigLocation::default());
+assert_eq!(reader.c8y.url, reader.c8y.http);
+
+```
+
+Using `from_optional_key`, the default value can be derived from the value of an
+optional field (i.e. a field without a default value of its own).
+
+The type of the resulting reader field will be
+[`OptionalConfig`](crate::OptionalConfig), with `Empty("fallback.key.here")` if
+the value is not set.
+
+| `dto.c8y.url`         | `dto.c8y.http`             | `reader.c8y.url`         | `reader.c8y.http`             |
+| --------------------- | -------------------------- | ------------------------ | ----------------------------- |
+| `None`                | `None`                     | `Empty("c8y.url")`       | `Empty("c8y.url")`            |
+| `Some("example.com")` | `None`                     | `Present("example.com")` | `Present("example.com")`      |
+| `None`                | `Some("http.example.com")` | `Empty("c8y.url")`       | `Present("http.example.com")` |
+| `Some("example.com")` | `Some("http.example.com")` | `Present("example.com")` | `Present("http.example.com")` |
+
+### <a name="default-fn"></a> Default functions: `#[tedge_config(default(function = "..."))]`
+```rust
+# use tedge_config_macros::*;
+# #[derive(::thiserror::Error, Debug)]
+# pub enum ReadError { #[error(transparent)] NotSet(#[from] ConfigNotSet)}
+use std::num::NonZeroU16;
+use camino::Utf8PathBuf;
+
+define_tedge_config! {
+  device: {
+    #[tedge_config(default(function = "default_device_cert_path"))]
+    #[doku(as = "std::path::PathBuf")]
+    cert_path: Utf8PathBuf,
+  },
+  mqtt: {
+    bind: {
+      #[tedge_config(default(function = "default_mqtt_port"))]
+      #[doku(as = "u16")]
+      port: NonZeroU16,
+
+      #[tedge_config(default(function = "|| NonZeroU16::try_from(1883).unwrap()"))]
+      #[doku(as = "u16")]
+      another_port: NonZeroU16,
+    },
+  },
+}
+
+fn default_device_cert_path(location: &TEdgeConfigLocation) -> Utf8PathBuf {
+    location
+        .tedge_config_root_path()
+        .join("device-certs")
+        .join("tedge-certificate.pem")
+}
+
+fn default_mqtt_port() -> NonZeroU16 {
+    NonZeroU16::try_from(1883).unwrap()
+}
+
+```
+
+The `function` sub-attribute allows a function to be supplied as the source for
+a default value. The function can be supplied as either the name of a function,
+or as a closure (both are demonstrated above). Through the magic of
+[`TEdgeConfigDefault`], these functions can depend on `&TEdgeConfigDto`,
+`&TEdgeConfigLocation`, neither, or both. The relevant values will be
+automatically passed in.
+
+
+## <a name="readonly"></a> Read only settings: `#[tedge_config(readonly(...))]`
+The `readonly` attribute marks a field as read only, and has two required sub
+attributes:
+
+- `#[tedge_config(readonly(function = "..."))]` specifies the function that is
+  called to populate the field in the reader, this is called lazily using
+  [`once_cell::sync::Lazy`]
+- `#[tedge_config(readonly(write_error = "..."))]` specifies the error that will
+  be displayed if someone attempts to `tedge config set` the field
+
+```rust
+use tedge_config_macros::*;
+
+#[derive(::thiserror::Error, Debug)]
+pub enum ReadError {
+    #[error(transparent)]
+    NotSet(#[from] ConfigNotSet),
+
+    #[error("Couldn't read certificate")]
+    CertificateParseFailure(#[from] Box<dyn std::error::Error>),
+}
+
+define_tedge_config! {
+  device: {
+    #[tedge_config(readonly(
+      function = "try_read_device_id",
+      write_error = "This setting is derived from the device certificate and is therefore read only.",
+    ))]
+    #[doku(as = "String")]
+    id: Result<String, ReadError>,
+  }
+}
+
+fn try_read_device_id(_reader: &TEdgeConfigReader) -> Result<String, ReadError> {
+    unimplemented!()
+}
+```
+The `function` sub attribute is more restrictive than
+[`default(function)`](#default-fn). The function must be passed in by name, and
+must have a single argument of type `&TEdgeConfigReader`.
diff --git a/crates/common/tedge_config_macros/src/doku_aliases.rs b/crates/common/tedge_config_macros/src/doku_aliases.rs
new file mode 100644
index 00000000000..281344cd58b
--- /dev/null
+++ b/crates/common/tedge_config_macros/src/doku_aliases.rs
@@ -0,0 +1,116 @@
+use std::borrow::Cow;
+use std::collections::HashMap;
+
+fn dot_separate(prefix: Option<&str>, field: &str, sub_path: &str) -> Cow<'static, str> {
+    Cow::Owned(
+        prefix
+            .into_iter()
+            .chain([field, sub_path])
+            .collect::<Vec<_>>()
+            .join("."),
+    )
+}
+
+/// Creates a map from aliases to canonical keys
+pub fn struct_field_aliases(
+    prefix: Option<&str>,
+    fields: &[(&'static str, doku::Field)],
+) -> HashMap<Cow<'static, str>, Cow<'static, str>> {
+    fields
+        .iter()
+        .flat_map(|(field_name, field)| match named_fields(&field.ty.kind) {
+            Some(fields) => {
+                // e.g. normal_field.alias
+                struct_field_aliases(Some(&key_name(prefix, field_name)), fields)
+                    .into_iter()
+                    // e.g. alias.normal_field
+                    .chain(conventional_sub_paths(field, prefix, field_name, fields))
+                    // e.g. alias.other_alias
+                    .chain(aliased_sub_paths(field, prefix, field_name, fields))
+                    .collect::<HashMap<_, _>>()
+            }
+            None => field
+                .aliases
+                .iter()
+                .map(|alias| (key_name(prefix, alias), key_name(prefix, field_name)))
+                .collect(),
+        })
+        .collect()
+}
+
+fn aliased_sub_paths(
+    field: &doku::Field,
+    prefix: Option<&str>,
+    field_name: &str,
+    sub_fields: &[(&'static str, doku::Field)],
+) -> Vec<(Cow<'static, str>, Cow<'static, str>)> {
+    field
+        .aliases
+        .iter()
+        .flat_map(|alias| {
+            // e.g. alias.another_alias
+            struct_field_aliases(None, sub_fields).into_iter().map(
+                move |(nested_alias, resolved_subpath)| {
+                    (
+                        dot_separate(prefix, alias, &nested_alias),
+                        dot_separate(prefix, field_name, &resolved_subpath),
+                    )
+                },
+            )
+        })
+        .collect()
+}
+
+fn conventional_sub_paths(
+    field: &doku::Field,
+    prefix: Option<&str>,
+    name: &str,
+    sub_fields: &[(&'static str, doku::Field)],
+) -> Vec<(Cow<'static, str>, Cow<'static, str>)> {
+    field
+        .aliases
+        .iter()
+        .flat_map(|alias| {
+            // e.g. alias.normal_field
+            struct_field_paths(None, sub_fields)
+                .into_iter()
+                .map(move |(path, _ty)| {
+                    (
+                        dot_separate(prefix, alias, &path),
+                        dot_separate(prefix, name, &path),
+                    )
+                })
+        })
+        .collect()
+}
+
+/// Creates a "map" from keys to their doku type information
+pub fn struct_field_paths(
+    prefix: Option<&str>,
+    fields: &[(&'static str, doku::Field)],
+) -> Vec<(Cow<'static, str>, doku::Type)> {
+    fields
+        .iter()
+        .flat_map(|(name, field)| match named_fields(&field.ty.kind) {
+            Some(fields) => struct_field_paths(Some(&key_name(prefix, name)), fields),
+            None => vec![(key_name(prefix, name), field.ty.clone())],
+        })
+        .collect()
+}
+
+fn key_name(prefix: Option<&str>, name: &'static str) -> Cow<'static, str> {
+    match prefix {
+        Some(prefix) => Cow::Owned(format!("{}.{}", prefix, name)),
+        None => Cow::Borrowed(name),
+    }
+}
+
+fn named_fields(kind: &doku::TypeKind) -> Option<&[(&'static str, doku::Field)]> {
+    match kind {
+        doku::TypeKind::Struct {
+            fields: doku::Fields::Named { fields },
+            transparent: false,
+        } => Some(fields),
+        _ => None,
+    }
+}
diff --git a/crates/common/tedge_config_macros/src/example.rs b/crates/common/tedge_config_macros/src/example.rs
new file mode 100644
index 00000000000..44e24656b8e
--- /dev/null
+++ b/crates/common/tedge_config_macros/src/example.rs
@@ -0,0 +1,93 @@
+//! An example invocation of [define_tedge_config] to demonstrate what
+//! it expands to in `cargo doc` output.
+use super::*;
+use camino::Utf8PathBuf;
+use std::path::PathBuf;
+
+#[derive(thiserror::Error, Debug)]
+/// *Not macro generated!* An error that can be encountered when reading values
+/// from the configuration
+///
+/// As custom logic (e.g. for read-only values) needs to interact with this,
+/// this is left to the consuming module to define. It must include a case with
+/// `#[from] ConfigNotSet`, for instance:
+///
+/// ```
+/// use tedge_config_macros::ConfigNotSet;
+///
+/// #[derive(thiserror::Error, Debug)]
+/// pub enum ReadError {
+///     #[error(transparent)]
+///     ConfigNotSet(#[from] ConfigNotSet),
+///     
+///     // Add more cases, such as errors from inferring the device id, here
+/// }
+///
+/// ```
+pub enum ReadError {
+    #[error(transparent)]
+    ConfigNotSet(#[from] ConfigNotSet),
+}
+
+/// A trait defined to conveniently emit [ReadError]s from [OptionalConfig]
+/// values
+///
+/// Since this depends on [ReadError], this is not macro generated.
+///
+/// ```
+/// # #[derive(thiserror::Error, Debug)]
+/// # pub enum ReadError {
+/// #     #[error(transparent)]
+/// #     ConfigNotSet(#[from] ConfigNotSet),
+/// # }
+///
+/// # pub trait OptionalConfigError<T> {
+/// #     fn or_err(&self) -> Result<&T, ReadError>;
+/// # }
+///
+/// # impl<T> OptionalConfigError<T> for OptionalConfig<T> {
+/// #     fn or_err(&self) -> Result<&T, ReadError> {
+/// #         self.or_config_not_set().map_err(ReadError::from)
+/// #     }
+/// # }
+///
+/// use tedge_config_macros::*;
+///
+/// define_tedge_config! {
+///     c8y: {
+///         url: ConnectUrl,
+///     }
+/// }
+///
+/// fn connect_to_c8y(reader: &TEdgeConfigReader) -> Result<(), ReadError> {
+///     // If we fail here, the error message will tell the user that 'c8y.url' is unset
+///     let url = reader.c8y.url.or_err()?;
+///     println!("Connecting to Cumulocity: {url}");
+///     Ok(())
+/// }
+/// ```
+pub trait OptionalConfigError<T> {
+    fn or_err(&self) -> Result<&T, ReadError>;
+}
+
+impl<T> OptionalConfigError<T> for OptionalConfig<T> {
+    fn or_err(&self) -> Result<&T, ReadError> {
+        self.or_config_not_set().map_err(ReadError::from)
+    }
+}
+
+define_tedge_config! {
+    /// The device settings. Group doc comments are not used in tedge config, but they are copied to the Reader and DTO.
+    device: {
+        /// The root certificate path
+        #[doku(as = "PathBuf")]
+        root_cert_path: Utf8PathBuf,
+
+        #[doku(as = "PathBuf")]
+        #[tedge_config(default(from_optional_key = "device.root_cert_path"))]
+        root_cert_path2: Utf8PathBuf,
+
+        #[tedge_config(rename = "type")]
+        ty: String,
+    }
+}
diff --git a/crates/common/tedge_config_macros/src/lib.rs b/crates/common/tedge_config_macros/src/lib.rs
new file mode 100644
index 00000000000..f6e0515f31a
--- /dev/null
+++ b/crates/common/tedge_config_macros/src/lib.rs
@@ -0,0 +1,18 @@
+#[doc(inline)]
+#[doc = include_str!("define_tedge_config_docs.md")]
+pub use tedge_config_macros_macro::define_tedge_config;
+
+pub use all_or_nothing::*;
+#[doc(hidden)]
+pub use connect_url::*;
+pub use default::*;
+pub use doku_aliases::*;
+pub use option::*;
+
+mod all_or_nothing;
+mod connect_url;
+mod default;
+mod doku_aliases;
+#[cfg(doc)]
+pub mod example;
+mod option;
diff --git a/crates/common/tedge_config_macros/src/option.rs b/crates/common/tedge_config_macros/src/option.rs
new file mode 100644
index 00000000000..70004e78e3d
--- /dev/null
+++ b/crates/common/tedge_config_macros/src/option.rs
@@ -0,0 +1,122 @@
+//! Handling for optional configuration values
+//!
+//! This module provides types used to represent the presence or absence of
+//! values, but with the addition of metadata (such as the relevant
+//! configuration key) to aid in producing informative error messages.
+
+#[derive(serde::Serialize, Clone, Copy, PartialEq, Eq, Debug)]
+#[serde(into = "Option<T>", bound = "T: Clone + serde::Serialize")]
+/// The value for an optional configuration (i.e. one without a default value)
+///
+/// ```
+/// use tedge_config_macros::*;
+///
+/// assert_eq!(
+///     OptionalConfig::Present { value: "test", key: "device.type" }.or_none(),
+///     Some(&"test"),
+/// );
+/// ```
+pub enum OptionalConfig<T> {
+    /// Equivalent to `Some(T)`
+    Present { value: T, key: &'static str },
+    /// Equivalent to `None`, but stores the configuration key to create a
+    /// better error message
+    Empty(&'static str),
+}
+
+impl<T> From<OptionalConfig<T>> for Option<T> {
+    fn from(value: OptionalConfig<T>) -> Self {
+        match value {
+            OptionalConfig::Present { value, .. } => Some(value),
+            OptionalConfig::Empty(_key_name) => None,
+        }
+    }
+}
+
+#[derive(thiserror::Error, Debug)]
+#[error(
+    r#"A value for '{key}' is missing.\n\
+    A value can be set with `tedge config set {key} <value>`"#
+)]
+/// A descriptive error for missing configurations
+///
+/// When a configuration is missing, it can be converted to this via
+/// [OptionalConfig::or_config_not_set], and this will convert to a descriptive
+/// error message telling the user which key to set.
+pub struct ConfigNotSet {
+    key: &'static str,
+}
+
+impl<T> OptionalConfig<T> {
+    /// Converts the value to an [Option]
+    ///
+    /// ```
+    /// use tedge_config_macros::*;
+    ///
+    /// assert_eq!(
+    ///     OptionalConfig::Present { value: "test", key: "device.type" }.or_none(),
+    ///     Some(&"test"),
+    /// );
+    ///
+    /// assert_eq!(OptionalConfig::Empty::<&str>("device.type").or_none(), None);
+    /// ```
+    pub fn or_none(&self) -> Option<&T> {
+        match self {
+            Self::Present { value, .. } => Some(value),
+            Self::Empty(_) => None,
+        }
+    }
+
+    /// Converts the value to a [Result] with an error that contains the missing
+    /// key name
+    pub fn or_config_not_set(&self) -> Result<&T, ConfigNotSet> {
+        match self {
+            Self::Present { value, .. } => Ok(value),
+            Self::Empty(key) => Err(ConfigNotSet { key }),
+        }
+    }
+
+    pub fn key(&self) -> &'static str {
+        match self {
+            Self::Present { key, .. } => key,
+            Self::Empty(key) => key,
+        }
+    }
+
+    pub fn key_if_present(&self) -> Option<&'static str> {
+        match self {
+            Self::Present { key, .. } => Some(key),
+            Self::Empty(..) => None,
+        }
+    }
+
+    pub fn key_if_empty(&self) -> Option<&'static str> {
+        match self {
+            Self::Empty(key) => Some(key),
+            Self::Present { .. } => None,
+        }
+    }
+
+    pub fn as_ref(&self) -> OptionalConfig<&T> {
+        match self {
+            Self::Present { value, key } => OptionalConfig::Present { value, key },
+            Self::Empty(key) => OptionalConfig::Empty(key),
+        }
+    }
+
+    pub fn map<U>(self, f: impl FnOnce(T) -> U) -> OptionalConfig<U> {
+        match self {
+            Self::Present { value, key } => OptionalConfig::Present {
+                value: f(value),
+                key,
+            },
+            Self::Empty(key) => OptionalConfig::Empty(key),
+        }
+    }
+}
+
+impl<T: doku::Document> doku::Document for OptionalConfig<T> {
+    fn ty() -> doku::Type {
+        Option::<T>::ty()
+    }
+}
diff --git a/crates/common/tedge_config_macros/tests/aliases.rs b/crates/common/tedge_config_macros/tests/aliases.rs
new file mode 100644
index 00000000000..3249366073c
--- /dev/null
+++ b/crates/common/tedge_config_macros/tests/aliases.rs
@@ -0,0 +1,37 @@
+use tedge_config_macros::*;
+
+#[derive(thiserror::Error, Debug)]
+pub enum ReadError {
+    #[error(transparent)]
+    ConfigNotSet(#[from] ConfigNotSet),
+}
+
+define_tedge_config! {
+    #[tedge_config(deprecated_name = "azure")]
+    az: {
+        mapper: {
+            timestamp: bool,
+        }
+    },
+    device: {
+        #[tedge_config(rename = "type")]
+        ty: bool,
+    }
+}
+
+#[test]
+fn aliases_can_be_parsed_to_writable_keys() {
+    let _: WritableKey = "az.mapper.timestamp".parse().unwrap();
+    let _: WritableKey = "azure.mapper.timestamp".parse().unwrap();
+}
+
+#[test]
+fn aliases_can_be_parsed_to_readable_keys() {
+    let _: ReadableKey = "az.mapper.timestamp".parse().unwrap();
+    let _: ReadableKey = "azure.mapper.timestamp".parse().unwrap();
+}
+
+#[test]
+fn renamed_fields_can_be_parsed_to_writable_keys() {
+    let _: WritableKey = "device.type".parse().unwrap();
+}
diff --git a/crates/common/tedge_config_macros/tests/defaults.rs b/crates/common/tedge_config_macros/tests/defaults.rs
new file mode 100644
index 00000000000..f97c54b7f79
--- /dev/null
+++ b/crates/common/tedge_config_macros/tests/defaults.rs
@@ -0,0 +1,109 @@
+use camino::Utf8PathBuf;
+use tedge_config_macros::*;
+
+#[derive(thiserror::Error, Debug)]
+pub enum ReadError {
+    #[error(transparent)]
+    ConfigNotSet(#[from] ConfigNotSet),
+}
+
+#[test]
+fn root_cert_path_default() {
+    const DEFAULT_ROOT_CERT_PATH: &str = "/etc/ssl/certs";
+
+    define_tedge_config! {
+        az: {
+            #[tedge_config(default(variable = "DEFAULT_ROOT_CERT_PATH"))]
+            #[doku(as = "std::path::PathBuf")]
+            root_cert_path: Utf8PathBuf,
+        }
+    }
+
+    let dto = TEdgeConfigDto::default();
+    let reader = TEdgeConfigReader::from_dto(&dto, &TEdgeConfigLocation);
+    assert_eq!(reader.az.root_cert_path, "/etc/ssl/certs");
+}
+
+#[test]
+fn default_from_key_uses_the_correct_default() {
+    #![allow(unused_variables)]
+    define_tedge_config! {
+        test: {
+            #[tedge_config(default(value = "DEFAULT_VALUE_FOR_ONE"))]
+            one: String,
+            #[tedge_config(default(from_key = "test.one"))]
+            two: String,
+        }
+    }
+    let dto = TEdgeConfigDto::default();
+    assert_eq!(
+        TEdgeConfigReader::from_dto(&dto, &TEdgeConfigLocation)
+            .test
+            .two,
+        "DEFAULT_VALUE_FOR_ONE"
+    );
+}
+
+#[test]
+fn default_from_key_uses_the_value_of_other_field_if_set() {
+    #![allow(unused_variables)]
+    define_tedge_config! {
+        test: {
+            #[tedge_config(default(value = "DEFAULT_VALUE_FOR_ONE"))]
+            one: String,
+            #[tedge_config(default(from_key = "test.one"))]
+            two: String,
+        }
+    }
+    let mut dto = TEdgeConfigDto::default();
+    dto.test.one = Some("UPDATED_VALUE".into());
+    assert_eq!(
+        TEdgeConfigReader::from_dto(&dto, &TEdgeConfigLocation)
+            .test
+            .two,
+        "UPDATED_VALUE"
+    );
+}
+
+#[test]
+fn default_from_key_uses_its_own_value_if_both_are_set() {
+    #![allow(unused_variables)]
+    define_tedge_config! {
+        test: {
+            #[tedge_config(default(value = "DEFAULT_VALUE_FOR_ONE"))]
+            one: String,
+            #[tedge_config(default(from_key = "test.one"))]
+            two: String,
+        }
+    }
+    let mut dto = TEdgeConfigDto::default();
+    dto.test.one = Some("UPDATED_VALUE".into());
+    dto.test.two = Some("OWN_VALUE".into());
+    assert_eq!(
+        TEdgeConfigReader::from_dto(&dto, &TEdgeConfigLocation)
+            .test
+            .two,
+        "OWN_VALUE"
+    );
+}
+
+#[test]
+fn default_from_key_uses_its_own_value_if_only_it_is_set() {
+    #![allow(unused_variables)]
+    define_tedge_config! {
+        test: {
+            #[tedge_config(default(value = "DEFAULT_VALUE_FOR_ONE"))]
+            one: String,
+            #[tedge_config(default(from_key = "test.one"))]
+            two: String,
+        }
+    }
+    let mut dto = TEdgeConfigDto::default();
+    dto.test.two = Some("OWN_VALUE".into());
+    assert_eq!(
+        TEdgeConfigReader::from_dto(&dto, &TEdgeConfigLocation)
+            .test
+            .two,
+        "OWN_VALUE"
+    );
+}
diff --git a/crates/common/tedge_config_macros/tests/examples.rs b/crates/common/tedge_config_macros/tests/examples.rs
new file mode 100644
index 00000000000..f8b65eda9e4
--- /dev/null
+++ b/crates/common/tedge_config_macros/tests/examples.rs
@@ -0,0 +1,18 @@
+use camino::Utf8PathBuf;
+use tedge_config_macros::*;
+
+#[derive(thiserror::Error, Debug)]
+pub enum ReadError {
+    #[error(transparent)]
+    ConfigNotSet(#[from] ConfigNotSet),
+}
+
+// The macro invocation generates tests of its own for each example value
+define_tedge_config! {
+    device: {
+        #[tedge_config(example = "/test/cert/path")]
+        #[tedge_config(example = "/test/cert/path2")]
+        #[doku(as = "std::path::PathBuf")]
+        root_cert_path: Utf8PathBuf,
+    }
+}
diff --git a/crates/common/tedge_config_macros/tests/optional.rs b/crates/common/tedge_config_macros/tests/optional.rs
new file mode 100644
index 00000000000..e86fc3c9989
--- /dev/null
+++ b/crates/common/tedge_config_macros/tests/optional.rs
@@ -0,0 +1,28 @@
+use tedge_config_macros::*;
+
+#[derive(thiserror::Error, Debug)]
+pub enum ReadError {
+    #[error(transparent)]
+    ConfigNotSet(#[from] ConfigNotSet),
+}
+
+#[test]
+fn vacant_optional_configurations_contain_the_relevant_key() {
+    define_tedge_config! {
+        mqtt: {
+            external: {
+                bind: {
+                    port: u16,
+                }
+            }
+        }
+    }
+
+    let dto = TEdgeConfigDto::default();
+    let config = TEdgeConfigReader::from_dto(&dto, &TEdgeConfigLocation);
+
+    assert_eq!(
+        config.mqtt.external.bind.port,
+        OptionalConfig::Empty("mqtt.external.bind.port")
+    );
+}
diff --git a/crates/core/tedge/Cargo.toml b/crates/core/tedge/Cargo.toml
index ce6db3ea23a..4e5a795515e 100644
--- a/crates/core/tedge/Cargo.toml
+++ b/crates/core/tedge/Cargo.toml
@@ -16,11 +16,14 @@ maintainer-scripts = "../../../configuration/debian/tedge"
 
 [dependencies]
 anyhow = "1.0"
+atty = "0.2"
 base64 = "0.13"
 camino = "1.1.4"
 certificate = { path = "../../common/certificate" }
 clap = { version = "3", features = ["cargo", "derive"] }
+doku = "0.21"
 hyper = { version = "0.14", default-features = false }
+pad = "0.1"
 reqwest = { version = "0.11", default-features = false, features = [
     "blocking",
     "json",
@@ -39,6 +42,7 @@ toml = "0.5"
 tracing = { version = "0.1", features = ["attributes", "log"] }
 url = "2.2"
 which = "4.2"
+yansi = "0.5"
 
 [dev-dependencies]
 assert_cmd = "2.0"
diff --git a/crates/core/tedge/src/cli/certificate/cli.rs b/crates/core/tedge/src/cli/certificate/cli.rs
index 0a0066299da..883eb29984f 100644
--- a/crates/core/tedge/src/cli/certificate/cli.rs
+++ b/crates/core/tedge/src/cli/certificate/cli.rs
@@ -1,3 +1,5 @@
+use tedge_config::new::OptionalConfigError;
+
 use super::create::CreateCertCmd;
 use super::remove::RemoveCertCmd;
 use super::show::ShowCertCmd;
@@ -8,8 +10,6 @@ use crate::command::BuildContext;
 use crate::command::Command;
 use crate::ConfigError;
 
-use tedge_config::*;
-
 #[derive(clap::Subcommand, Debug)]
 pub enum TEdgeCertCli {
     /// Create a self-signed device certificate
@@ -32,29 +32,29 @@ pub enum TEdgeCertCli {
 
 impl BuildCommand for TEdgeCertCli {
     fn build_command(self, context: BuildContext) -> Result<Box<dyn Command>, ConfigError> {
-        let config = context.config_repository.load()?;
+        let config = context.config_repository.load_new()?;
 
         let cmd = match self {
             TEdgeCertCli::Create { id } => {
                 let cmd = CreateCertCmd {
                     id,
-                    cert_path: config.query(DeviceCertPathSetting)?,
-                    key_path: config.query(DeviceKeyPathSetting)?,
+                    cert_path: config.device.cert_path.clone(),
+                    key_path: config.device.key_path.clone(),
                 };
                 cmd.into_boxed()
             }
 
             TEdgeCertCli::Show => {
                 let cmd = ShowCertCmd {
-                    cert_path: config.query(DeviceCertPathSetting)?,
+                    cert_path: config.device.cert_path.clone(),
                 };
                 cmd.into_boxed()
             }
 
             TEdgeCertCli::Remove => {
                 let cmd = RemoveCertCmd {
-                    cert_path: config.query(DeviceCertPathSetting)?,
-                    key_path: config.query(DeviceKeyPathSetting)?,
+                    cert_path: config.device.cert_path.clone(),
+                    key_path: config.device.key_path.clone(),
                 };
                 cmd.into_boxed()
             }
@@ -62,9 +62,9 @@ impl BuildCommand for TEdgeCertCli {
             TEdgeCertCli::Upload(cmd) => {
                 let cmd = match cmd {
                     UploadCertCli::C8y { username } => UploadCertCmd {
-                        device_id: config.query(DeviceIdSetting)?,
-                        path: config.query(DeviceCertPathSetting)?,
-                        host: config.query(C8yHttpSetting)?,
+                        device_id: config.device.id.try_read(&config)?.clone(),
+                        path: config.device.cert_path.clone(),
+                        host: config.c8y.http.or_err()?.to_owned(),
                         username,
                     },
                 };
diff --git a/crates/core/tedge/src/cli/certificate/error.rs b/crates/core/tedge/src/cli/certificate/error.rs
index 1c83df69c1d..34c0ed23a6a 100644
--- a/crates/core/tedge/src/cli/certificate/error.rs
+++ b/crates/core/tedge/src/cli/certificate/error.rs
@@ -46,10 +46,10 @@ pub enum CertError {
     #[error("I/O error")]
     IoError(#[from] std::io::Error),
 
-    #[error("Invalid device.cert.path path: {0}")]
+    #[error("Invalid device.cert_path path: {0}")]
     CertPathError(PathsError),
 
-    #[error("Invalid device.key.path path: {0}")]
+    #[error("Invalid device.key_path path: {0}")]
     KeyPathError(PathsError),
 
     #[error(transparent)]
diff --git a/crates/core/tedge/src/cli/config/cli.rs b/crates/core/tedge/src/cli/config/cli.rs
index 067e7693f28..6b3da820e00 100644
--- a/crates/core/tedge/src/cli/config/cli.rs
+++ b/crates/core/tedge/src/cli/config/cli.rs
@@ -1,21 +1,21 @@
 use crate::cli::config::commands::*;
-use crate::cli::config::config_key::*;
 use crate::command::*;
 use crate::ConfigError;
-use tedge_config::ConfigRepository;
+use tedge_config::new::ReadableKey;
+use tedge_config::new::WritableKey;
 
 #[derive(clap::Subcommand, Debug)]
 pub enum ConfigCmd {
     /// Get the value of the provided configuration key
     Get {
         /// Configuration key. Run `tedge config list --doc` for available keys
-        key: ConfigKey,
+        key: ReadableKey,
     },
 
     /// Set or update the provided configuration key with the given value
     Set {
         /// Configuration key. Run `tedge config list --doc` for available keys
-        key: ConfigKey,
+        key: WritableKey,
 
         /// Configuration value.
         value: String,
@@ -24,7 +24,7 @@ pub enum ConfigCmd {
     /// Unset the provided configuration key
     Unset {
         /// Configuration key. Run `tedge config list --doc` for available keys
-        key: ConfigKey,
+        key: WritableKey,
     },
 
     /// Print the configuration keys and their values
@@ -41,31 +41,29 @@ pub enum ConfigCmd {
 
 impl BuildCommand for ConfigCmd {
     fn build_command(self, context: BuildContext) -> Result<Box<dyn Command>, ConfigError> {
-        let config = context.config_repository.load()?;
         let config_repository = context.config_repository;
 
         match self {
             ConfigCmd::Get { key } => Ok(GetConfigCommand {
-                config_key: key,
-                config,
+                key,
+                config: config_repository.load_new()?,
             }
             .into_boxed()),
             ConfigCmd::Set { key, value } => Ok(SetConfigCommand {
-                config_key: key,
+                key,
                 value,
                 config_repository,
             }
             .into_boxed()),
             ConfigCmd::Unset { key } => Ok(UnsetConfigCommand {
-                config_key: key,
+                key,
                 config_repository,
             }
             .into_boxed()),
             ConfigCmd::List { is_all, is_doc } => Ok(ListConfigCommand {
                 is_all,
                 is_doc,
-                config_keys: ConfigKey::list_all(),
-                config,
+                config: config_repository.load_new()?,
             }
             .into_boxed()),
         }
diff --git a/crates/core/tedge/src/cli/config/commands/get.rs b/crates/core/tedge/src/cli/config/commands/get.rs
index 82053e73767..ecffb89f56f 100644
--- a/crates/core/tedge/src/cli/config/commands/get.rs
+++ b/crates/core/tedge/src/cli/config/commands/get.rs
@@ -1,35 +1,27 @@
-use crate::cli::config::ConfigKey;
+use tedge_config::tedge_config_cli::new::ReadableKey;
+
 use crate::command::Command;
 
 pub struct GetConfigCommand {
-    pub config_key: ConfigKey,
-    pub config: tedge_config::TEdgeConfig,
+    pub key: ReadableKey,
+    pub config: tedge_config::new::TEdgeConfig,
 }
 
 impl Command for GetConfigCommand {
     fn description(&self) -> String {
-        format!(
-            "get the configuration value for key: {}",
-            self.config_key.key
-        )
+        format!("get the configuration value for key: '{}'", self.key)
     }
 
     fn execute(&self) -> anyhow::Result<()> {
-        match (self.config_key.get)(&self.config) {
+        match self.config.read_string(self.key) {
             Ok(value) => {
                 println!("{}", value);
             }
-            Err(tedge_config::ConfigSettingError::ConfigNotSet { .. }) => {
-                eprintln!(
-                    "The provided config key: '{}' is not set",
-                    self.config_key.key
-                );
+            Err(tedge_config::new::ReadError::ConfigNotSet { .. }) => {
+                eprintln!("The provided config key: '{}' is not set", self.key);
             }
-            Err(tedge_config::ConfigSettingError::SettingIsNotConfigurable { .. }) => {
-                eprintln!(
-                    "The provided config key: '{}' is not configurable",
-                    self.config_key.key
-                );
+            Err(tedge_config::new::ReadError::ReadOnlyNotFound { message, key }) => {
+                eprintln!("The provided config key: '{key}' is not configured: {message}",);
             }
             Err(err) => return Err(err.into()),
         }
diff --git a/crates/core/tedge/src/cli/config/commands/list.rs b/crates/core/tedge/src/cli/config/commands/list.rs
index 8645e7ecb3c..0ef8804cdd1 100644
--- a/crates/core/tedge/src/cli/config/commands/list.rs
+++ b/crates/core/tedge/src/cli/config/commands/list.rs
@@ -1,13 +1,14 @@
-use crate::cli::config::config_key::*;
 use crate::command::Command;
 use crate::ConfigError;
-use tedge_config::*;
+use pad::PadStr;
+use tedge_config::new::ReadableKey;
+use tedge_config::new::TEdgeConfig;
+use tedge_config::new::READABLE_KEYS;
 
 pub struct ListConfigCommand {
     pub is_all: bool,
     pub is_doc: bool,
     pub config: TEdgeConfig,
-    pub config_keys: Vec<ConfigKey>,
 }
 
 impl Command for ListConfigCommand {
@@ -17,31 +18,25 @@ impl Command for ListConfigCommand {
 
     fn execute(&self) -> anyhow::Result<()> {
         if self.is_doc {
-            print_config_doc(&self.config_keys);
+            print_config_doc();
         } else {
-            print_config_list(&self.config_keys, &self.config, self.is_all)?;
+            print_config_list(&self.config, self.is_all)?;
         }
 
         Ok(())
     }
 }
 
-fn print_config_list(
-    config_keys: &[ConfigKey],
-    config: &TEdgeConfig,
-    all: bool,
-) -> Result<(), ConfigError> {
-    let mut keys_without_values: Vec<String> = Vec::new();
-    for config_key in config_keys {
-        match (config_key.get)(config) {
-            Ok(value) => {
-                println!("{}={}", config_key.key, value);
+fn print_config_list(config: &TEdgeConfig, all: bool) -> Result<(), ConfigError> {
+    let mut keys_without_values = Vec::new();
+    for config_key in ReadableKey::iter() {
+        match config.read_string(config_key).ok() {
+            Some(value) => {
+                println!("{}={}", config_key, value);
             }
-            Err(tedge_config::ConfigSettingError::ConfigNotSet { .. })
-            | Err(tedge_config::ConfigSettingError::SettingIsNotConfigurable { .. }) => {
-                keys_without_values.push(config_key.key.into());
+            None => {
+                keys_without_values.push(config_key);
             }
-            Err(err) => return Err(err.into()),
         }
     }
     if all && !keys_without_values.is_empty() {
@@ -53,8 +48,73 @@ fn print_config_list(
     Ok(())
 }
 
-fn print_config_doc(config_keys: &[ConfigKey]) {
-    for config_key in config_keys {
-        println!("{:<30} {}", config_key.key, config_key.description);
+fn print_config_doc() {
+    if atty::isnt(atty::Stream::Stdout) {
+        yansi::Paint::disable();
+    }
+
+    let max_length = ReadableKey::iter()
+        .map(|c| c.as_str().len())
+        .max()
+        .unwrap_or_default();
+
+    for (key, ty) in READABLE_KEYS.iter() {
+        let docs = ty
+            .comment
+            .map(|c| {
+                let mut comment = c.replace('\n', " ");
+                if !comment.ends_with('.') {
+                    comment.push('.');
+                };
+                comment.push(' ');
+                comment
+            })
+            .unwrap_or_default();
+
+        println!(
+            "{}  {}",
+            yansi::Paint::yellow(
+                key.pad_to_width_with_alignment(max_length, pad::Alignment::Right)
+            ),
+            yansi::Paint::default(docs).italic()
+        );
+
+        // TODO add a test to make sure people don't accidentally set the wrong meta name
+        if let Some(note) = ty.metas.get("note") {
+            println!(
+                "{}  {} {note}",
+                "".pad_to_width(max_length),
+                yansi::Paint::blue("Note:")
+            );
+        }
+
+        match ty.example {
+            Some(doku::Example::Simple(val)) | Some(doku::Example::Literal(val)) => {
+                println!(
+                    "{}  {} {}",
+                    "".pad_to_width(max_length),
+                    yansi::Paint::green("Example:"),
+                    val
+                );
+            }
+            Some(doku::Example::Compound(val)) => {
+                let vals = val
+                    .iter()
+                    .map(|v| v.to_string())
+                    .collect::<Vec<_>>()
+                    .join(", ");
+                println!(
+                    "{}  {} {}",
+                    "".pad_to_width(max_length),
+                    yansi::Paint::green("Examples:"),
+                    vals
+                );
+            }
+            None => (),
+        };
+
+        if atty::isnt(atty::Stream::Stdout) {
+            println!();
+        }
     }
 }
diff --git a/crates/core/tedge/src/cli/config/commands/set.rs b/crates/core/tedge/src/cli/config/commands/set.rs
index bd4b277382c..5275a0ebcbe 100644
--- a/crates/core/tedge/src/cli/config/commands/set.rs
+++ b/crates/core/tedge/src/cli/config/commands/set.rs
@@ -1,9 +1,9 @@
-use crate::cli::config::ConfigKey;
 use crate::command::Command;
+use tedge_config::new::WritableKey;
 use tedge_config::*;
 
 pub struct SetConfigCommand {
-    pub config_key: ConfigKey,
+    pub key: WritableKey,
     pub value: String,
     pub config_repository: TEdgeConfigRepository,
 }
@@ -11,14 +11,17 @@ pub struct SetConfigCommand {
 impl Command for SetConfigCommand {
     fn description(&self) -> String {
         format!(
-            "set the configuration key: {} with value: {}.",
-            self.config_key.key, self.value
+            "set the configuration key: '{}' with value: {}.",
+            self.key.as_str(),
+            self.value
         )
     }
 
     fn execute(&self) -> anyhow::Result<()> {
-        self.config_repository
-            .update_toml(&|config| (self.config_key.set)(config, self.value.to_string()))?;
+        self.config_repository.update_toml_new(&|dto| {
+            dto.try_update_str(self.key, &self.value)
+                .map_err(|e| e.into())
+        })?;
         Ok(())
     }
 }
diff --git a/crates/core/tedge/src/cli/config/commands/unset.rs b/crates/core/tedge/src/cli/config/commands/unset.rs
index 14d61b0452d..5f2279e43bc 100644
--- a/crates/core/tedge/src/cli/config/commands/unset.rs
+++ b/crates/core/tedge/src/cli/config/commands/unset.rs
@@ -1,22 +1,22 @@
-use crate::cli::config::ConfigKey;
 use crate::command::Command;
+use tedge_config::new::WritableKey;
 use tedge_config::*;
 
 pub struct UnsetConfigCommand {
-    pub config_key: ConfigKey,
+    pub key: WritableKey,
     pub config_repository: TEdgeConfigRepository,
 }
 
 impl Command for UnsetConfigCommand {
     fn description(&self) -> String {
-        format!(
-            "unset the configuration value for key: {}",
-            self.config_key.key
-        )
+        format!("unset the configuration value for '{}'", self.key)
     }
 
     fn execute(&self) -> anyhow::Result<()> {
-        self.config_repository.update_toml(&self.config_key.unset)?;
+        self.config_repository.update_toml_new(&|dto| {
+            dto.unset_key(self.key);
+            Ok(())
+        })?;
         Ok(())
     }
 }
diff --git a/crates/core/tedge/src/cli/connect/bridge_config_aws.rs b/crates/core/tedge/src/cli/connect/bridge_config_aws.rs
index 806466fee39..2b462d35b05 100644
--- a/crates/core/tedge/src/cli/connect/bridge_config_aws.rs
+++ b/crates/core/tedge/src/cli/connect/bridge_config_aws.rs
@@ -25,7 +25,7 @@ impl From<BridgeConfigAwsParams> for BridgeConfig {
             bridge_keyfile,
         } = params;
 
-        let address = format!("{}:{}", connect_url.as_str(), mqtt_tls_port);
+        let address = format!("{}:{}", connect_url, mqtt_tls_port);
         let user_name = remote_clientid.to_string();
 
         // telemetry/command topics for use by the user
diff --git a/crates/core/tedge/src/cli/connect/bridge_config_azure.rs b/crates/core/tedge/src/cli/connect/bridge_config_azure.rs
index 6efb68f6e3f..d2b1e727174 100644
--- a/crates/core/tedge/src/cli/connect/bridge_config_azure.rs
+++ b/crates/core/tedge/src/cli/connect/bridge_config_azure.rs
@@ -25,11 +25,10 @@ impl From<BridgeConfigAzureParams> for BridgeConfig {
             bridge_keyfile,
         } = params;
 
-        let address = format!("{}:{}", connect_url.as_str(), mqtt_tls_port);
+        let address = format!("{}:{}", connect_url, mqtt_tls_port);
         let user_name = format!(
             "{}/{}/?api-version=2018-06-30",
-            connect_url.as_str(),
-            remote_clientid
+            connect_url, remote_clientid
         );
         let pub_msg_topic = format!("messages/events/# out 1 az/ devices/{}/", remote_clientid);
         let sub_msg_topic = format!(
diff --git a/crates/core/tedge/src/cli/connect/c8y_direct_connection.rs b/crates/core/tedge/src/cli/connect/c8y_direct_connection.rs
index 668a9e14279..c11408a0209 100644
--- a/crates/core/tedge/src/cli/connect/c8y_direct_connection.rs
+++ b/crates/core/tedge/src/cli/connect/c8y_direct_connection.rs
@@ -84,13 +84,13 @@ pub fn create_device_with_direct_connection(
                 {
                     if let AlertDescription::CertificateUnknown = alert_description {
                         // Either the device cert is not uploaded to c8y or
-                        // another cert is set in device.cert.path
+                        // another cert is set in device.cert_path
                         eprintln!("The device certificate is not trusted by Cumulocity.");
                         return Err(ConnectError::ConnectionCheckError);
                     } else if let AlertDescription::HandshakeFailure = alert_description {
-                        // Non-paired private key is set in device.key.path
+                        // Non-paired private key is set in device.key_path
                         eprintln!(
-                            "The private key is not paired with the certificate. Check your 'device.key.path'."
+                            "The private key is not paired with the certificate. Check your 'device.key_path'."
                         );
                         return Err(ConnectError::ConnectionCheckError);
                     }
@@ -108,7 +108,7 @@ pub fn create_device_with_direct_connection(
                     Some(Error::InvalidCertificateData(description))
                         if description == "invalid peer certificate: UnknownIssuer" =>
                     {
-                        eprintln!("Cumulocity certificate is not trusted by the device. Check your 'c8y.root.cert.path'.");
+                        eprintln!("Cumulocity certificate is not trusted by the device. Check your 'c8y.root_cert_path'.");
                     }
                     _ => {
                         eprintln!("ERROR: {:?}", err);
diff --git a/crates/core/tedge/src/error.rs b/crates/core/tedge/src/error.rs
index 14341cbe909..1f87474c285 100644
--- a/crates/core/tedge/src/error.rs
+++ b/crates/core/tedge/src/error.rs
@@ -24,4 +24,7 @@ pub enum TEdgeError {
 
     #[error(transparent)]
     FromSystemServiceError(#[from] tedge_config::system_services::SystemServiceError),
+
+    #[error(transparent)]
+    FromTEdgeConfigRead(#[from] tedge_config::new::ReadError),
 }
diff --git a/crates/core/tedge/src/main.rs b/crates/core/tedge/src/main.rs
index ef8f8554235..fc577eaa27a 100644
--- a/crates/core/tedge/src/main.rs
+++ b/crates/core/tedge/src/main.rs
@@ -20,10 +20,10 @@ const BROKER_USER: &str = "mosquitto";
 const BROKER_GROUP: &str = "mosquitto";
 
 fn main() -> anyhow::Result<()> {
-    let opt = cli::Opt::parse();
-
     set_log_level(tracing::Level::WARN);
 
+    let opt = cli::Opt::parse();
+
     if opt.init {
         initialize_tedge(&opt.config_dir)
             .with_context(|| "Failed to initialize tedge. You have to run tedge with sudo.")?;
diff --git a/crates/core/tedge/tests/main.rs b/crates/core/tedge/tests/main.rs
index 50952d92750..131561a26e4 100644
--- a/crates/core/tedge/tests/main.rs
+++ b/crates/core/tedge/tests/main.rs
@@ -52,7 +52,7 @@ mod tests {
             home_dir,
             "config",
             "set",
-            "device.cert.path",
+            "device.cert_path",
             &cert_path,
         ])?;
         let mut set_key_path_cmd = tedge_command_with_test_home([
@@ -60,7 +60,7 @@ mod tests {
             home_dir,
             "config",
             "set",
-            "device.key.path",
+            "device.key_path",
             &key_path,
         ])?;
 
@@ -88,7 +88,7 @@ mod tests {
         get_device_id_cmd
             .assert()
             .success()
-            .stderr(predicate::str::contains("'device.id' is not configurable"));
+            .stderr(predicate::str::contains("'device.id' is not configured"));
 
         // The create command created a certificate
         create_cmd.assert().success();
@@ -187,7 +187,7 @@ mod tests {
         "The provided config key: \'c8y.url\' is not set\n",
         false
     )]
-    #[test_case("mqtt.port", "8880", "1883", true)]
+    #[test_case("mqtt.bind.port", "8880", "1883", true)]
     fn run_config_set_get_unset_read_write_key(
         config_key: &str,
         config_value: &str,
@@ -294,7 +294,7 @@ mod tests {
             test_home_str,
             "config",
             "get",
-            "device.cert.path",
+            "device.cert_path",
         ])?;
 
         get_cert_path_cmd
@@ -307,7 +307,7 @@ mod tests {
             test_home_str,
             "config",
             "get",
-            "device.key.path",
+            "device.key_path",
         ])?;
 
         get_key_path_cmd
@@ -335,7 +335,7 @@ mod tests {
             test_home_str,
             "config",
             "get",
-            "c8y.root.cert.path",
+            "c8y.root_cert_path",
         ])?;
 
         get_c8y_root_cert_path_cmd
@@ -358,22 +358,21 @@ mod tests {
         let output = assert.get_output().clone();
         let output_str = String::from_utf8(output.stdout).unwrap();
 
-        let key_path = extract_config_value(&output_str, "device.key.path");
+        let key_path = extract_config_value(&output_str, "device.key_path");
         assert!(key_path.ends_with("tedge-private-key.pem"));
         assert!(key_path.contains(test_home_str));
 
-        let cert_path = extract_config_value(&output_str, "device.cert.path");
+        let cert_path = extract_config_value(&output_str, "device.cert_path");
         assert!(cert_path.ends_with("tedge-certificate.pem"));
         assert!(cert_path.contains(test_home_str));
     }
 
-    fn extract_config_value(output: &str, key: &str) -> String {
+    fn extract_config_value<'a>(output: &'a str, key: &str) -> &'a str {
         output
             .lines()
             .map(|line| line.splitn(2, '=').collect::<Vec<_>>())
             .find(|pair| pair[0] == key)
-            .unwrap()[1]
-            .into()
+            .unwrap_or_else(|| panic!("couldn't find config value for '{key}'"))[1]
     }
 
     #[test]
@@ -401,16 +400,19 @@ mod tests {
         let output = assert.get_output();
         let output_str = String::from_utf8(output.clone().stdout).unwrap();
 
-        let key_path = extract_config_value(&output_str, "device.key.path");
+        let key_path = extract_config_value(&output_str, "device.key_path");
         assert!(key_path.ends_with("tedge-private-key.pem"));
         assert!(key_path.contains(test_home_str));
 
-        let cert_path = extract_config_value(&output_str, "device.cert.path");
+        let cert_path = extract_config_value(&output_str, "device.cert_path");
         assert!(cert_path.ends_with("tedge-certificate.pem"));
         assert!(cert_path.contains(test_home_str));
 
         for key in get_tedge_config_keys() {
-            assert!(output_str.contains(key));
+            assert!(
+                output_str.contains(key),
+                "couldn't find '{key}' in output of tedge config list --all"
+            );
         }
     }
 
@@ -432,7 +434,10 @@ mod tests {
         let output_str = String::from_utf8(output.stdout).unwrap();
 
         for key in get_tedge_config_keys() {
-            assert!(output_str.contains(key));
+            assert!(
+                output_str.contains(key),
+                "couldn't find '{key}' in output of tedge config list --doc"
+            );
         }
         assert!(output_str.contains("Example"));
     }
@@ -455,10 +460,10 @@ mod tests {
     fn get_tedge_config_keys() -> Vec<&'static str> {
         let vec = vec![
             "device.id",
-            "device.key.path",
-            "device.cert.path",
+            "device.key_path",
+            "device.cert_path",
             "c8y.url",
-            "c8y.root.cert.path",
+            "c8y.root_cert_path",
         ];
         vec
     }
diff --git a/docs/src/howto-guides/006_config.md b/docs/src/howto-guides/006_config.md
index f20ca561535..6c52a8c96cf 100644
--- a/docs/src/howto-guides/006_config.md
+++ b/docs/src/howto-guides/006_config.md
@@ -56,9 +56,9 @@ The names for these environment variables are prefixed with `TEDGE_` to avoid co
 | Setting             | Environment variable      |
 | ------------------- | ------------------------- |
 | `c8y.url`           | `TEDGE_C8Y_URL`           |
-| `device.key.path`   | `TEDGE_DEVICE_KEY_PATH`   |
-| `device.cert.path`  | `TEDGE_DEVICE_CERT_PATH`  |
-| `mqtt.bind_address` | `TEDGE_MQTT_BIND_ADDRESS` |
+| `device.key_path`   | `TEDGE_DEVICE_KEY_PATH`   |
+| `device.cert_path`  | `TEDGE_DEVICE_CERT_PATH`  |
+| `mqtt.bind.address` | `TEDGE_MQTT_BIND_ADDRESS` |
 
 You can also use `tedge config` to inspect the value that is set, which may prove useful if you are using a mix of toml configuration and environment variables. If you had tedge.toml file set as shown [above](#tedgetoml), you could run:
 
diff --git a/docs/src/howto-guides/008_config_local_mqtt_bind_address_and_port.md b/docs/src/howto-guides/008_config_local_mqtt_bind_address_and_port.md
index 585fb5ed6e6..46bde422d09 100644
--- a/docs/src/howto-guides/008_config_local_mqtt_bind_address_and_port.md
+++ b/docs/src/howto-guides/008_config_local_mqtt_bind_address_and_port.md
@@ -3,7 +3,7 @@
 Configuring a mosquitto port and bind address in thin-edge.io is a three-step process.
 
 ```admonish note
-The mqtt.port and the mqtt.bind_address can be set/unset independently.
+The mqtt.bind.port and the mqtt.bind.address can be set/unset independently.
 ```
 
 ## Step 1: Disconnect thin-edge.io edge device
@@ -16,14 +16,14 @@ tedge disconnect c8y/az
 
 ## Step 2: Set and verify the new mqtt port and bind address
 
-Use the `tedge` command to set the mqtt.port and mqtt.bind_address with a desired port and bind address as below.
+Use the `tedge` command to set the mqtt.bind.port and mqtt.bind.address with a desired port and bind address as below.
 
 ```shell
-tedge config set mqtt.port 1024
+tedge config set mqtt.bind.port 1024
 ```
 
 ```shell
-tedge config set mqtt.bind_address 127.0.0.1
+tedge config set mqtt.bind.address 127.0.0.1
 ```
 
 ```admonish note
@@ -39,11 +39,11 @@ has been set once the device is connected to the cloud as in step 3.
 Use the `tedge` command to print the mqtt port and bind address that has been set as below.
 
 ```shell
-tedge config get mqtt.port
+tedge config get mqtt.bind.port
 ```
 
 ```shell
-tedge config get mqtt.bind_address
+tedge config get mqtt.bind.address
 ```
 
 ## Step 3: Connect the thin edge device to cloud
@@ -72,11 +72,11 @@ Note: The step 1 and 2 can be followed in any order.
 Use the `tedge` command to set the default port (1883) and default bind address (localhost) as below.
 
 ```shell
-tedge config unset mqtt.port
+tedge config unset mqtt.bind.port
 ```
 
 ```shell
-tedge config unset mqtt.bind_address
+tedge config unset mqtt.bind.address
 ```
 
 Once the port or the bind address is reverted to default, the [step 1](#Step-3:-Connect-the-thin-edge-device-to-cloud)
@@ -87,9 +87,9 @@ and 3 has to be followed to use the default port or the default bind address.
 The below example shows that we cannot set a string value for the port number.
 
 ```shell
-tedge config set mqtt.port '"1234"'
+tedge config set mqtt.bind.port '"1234"'
 
-Error: failed to set the configuration key: mqtt.port with value: "1234".
+Error: failed to set the configuration key: mqtt.bind.port with value: "1234".
 
 Caused by:
     Conversion from String failed
diff --git a/docs/src/howto-guides/013_connect_external_device.md b/docs/src/howto-guides/013_connect_external_device.md
index 5efd8ba1c5e..989fac5a7b6 100644
--- a/docs/src/howto-guides/013_connect_external_device.md
+++ b/docs/src/howto-guides/013_connect_external_device.md
@@ -12,13 +12,13 @@ External devices connection can be setup by using the `tedge` cli tool making so
 
 The following configurations option are available for you if you want to add an external listener to thin-edge.io:
 
-`mqtt.external.port`             Mqtt broker port, which is used by the external mqtt clients to publish or subscribe. Example: 8883
-`mqtt.external.bind_address`     IP address / hostname, which the mqtt broker limits incoming connections on. Example: 0.0.0.0
-`mqtt.external.bind_interface`   Name of network interface, which the mqtt broker limits incoming connections on. Example: wlan0
+`mqtt.external.bind.port`             Mqtt broker port, which is used by the external mqtt clients to publish or subscribe. Example: 8883
+`mqtt.external.bind.address`     IP address / hostname, which the mqtt broker limits incoming connections on. Example: 0.0.0.0
+`mqtt.external.bind.interface`   Name of network interface, which the mqtt broker limits incoming connections on. Example: wlan0
 
-`mqtt.external.capath`           Path to a file containing the PEM encoded CA certificates that are trusted when checking incoming client certificates. Example: /etc/ssl/certs
-`mqtt.external.certfile`         Path to the certificate file, which is used by external MQTT listener. Example: /etc/tedge/server-certs/tedge-certificate.pem
-`mqtt.external.keyfile`          Path to the private key file, which is used by external MQTT listener. Example: /etc/tedge/server-certs/tedge-private-key.pem
+`mqtt.external.ca_path`           Path to a file containing the PEM encoded CA certificates that are trusted when checking incoming client certificates. Example: /etc/ssl/certs
+`mqtt.external.cert_file`         Path to the certificate file, which is used by external MQTT listener. Example: /etc/tedge/server-certs/tedge-certificate.pem
+`mqtt.external.key_file`          Path to the private key file, which is used by external MQTT listener. Example: /etc/tedge/server-certs/tedge-private-key.pem
 
 ```admonish note
 If none of these options is set, then no external listener is set.
@@ -31,35 +31,35 @@ These settings can be considered in 2 groups, listener configuration and TLS con
 ### Configure basic listener
 
 To configure basic listener you should provide port and/or bind address which will use default interface.
-To change the default interface you can use mqtt.external.bind_interface configuration option.
+To change the default interface you can use mqtt.external.bind.interface configuration option.
 
 To set them you can use `tedge config` as so:
 
 ```shell
-tedge config set mqtt.external.port 8883
+tedge config set mqtt.external.bind.port 8883
 ```
 
 To allow connections from all IP addresses on the interface:
 
 ```shell
-tedge config set mqtt.external.bind_address 0.0.0.0
+tedge config set mqtt.external.bind.address 0.0.0.0
 ```
 
 ### Configure TLS on the listener
 
-To configure the external listener with TLS additional settings are available: `mqtt.external.capath` `mqtt.external.certfile` `mqtt.external.keyfile`
+To configure the external listener with TLS additional settings are available: `mqtt.external.ca_path` `mqtt.external.cert_file` `mqtt.external.key_file`
 
 To enable MQTT over TLS, a server side certificate must be configured using the 2 following settings:
 
 ```shell
-tedge config set mqtt.external.certfile /etc/tedge/server-certs/tedge-certificate.pem
-tedge config set mqtt.external.keyfile /etc/tedge/server-certs/tedge-private-key.pem
+tedge config set mqtt.external.cert_file /etc/tedge/server-certs/tedge-certificate.pem
+tedge config set mqtt.external.key_file /etc/tedge/server-certs/tedge-private-key.pem
 ```
 
 To fully enable TLS authentication clients, client side certificate validation can be enabled:
 
 ```shell
-tedge config set mqtt.external.capath /etc/ssl/certs
+tedge config set mqtt.external.ca_path /etc/ssl/certs
 ```
 
 ```admonish note
diff --git a/docs/src/howto-guides/029_mqtt_local_broker_authentication.md b/docs/src/howto-guides/029_mqtt_local_broker_authentication.md
index a2a5c0d6ae3..21258d3c0f3 100644
--- a/docs/src/howto-guides/029_mqtt_local_broker_authentication.md
+++ b/docs/src/howto-guides/029_mqtt_local_broker_authentication.md
@@ -90,8 +90,8 @@ keyfile  PATH_TO_CLIENT_CA_CERTIFICATE
 ### Step 2: Configure thin-edge.io to use a client certificate and private key
 
 ```sh
-tedge config set mqtt.client.auth.certfile PATH_TO_CLIENT_CERTIFICATE
-tedge config set mqtt.client.auth.keyfile PATH_TO_CLIENT_PRIVATE_KEY
+tedge config set mqtt.client.auth.cert_file PATH_TO_CLIENT_CERTIFICATE
+tedge config set mqtt.client.auth.key_file PATH_TO_CLIENT_PRIVATE_KEY
 ```
 
 Both `certfile` and `keyfile` are required to enable client authentication.
diff --git a/docs/src/howto-guides/child_device_config_management_agent.md b/docs/src/howto-guides/child_device_config_management_agent.md
index 3c72574119c..a038330f7ba 100644
--- a/docs/src/howto-guides/child_device_config_management_agent.md
+++ b/docs/src/howto-guides/child_device_config_management_agent.md
@@ -31,7 +31,7 @@ MQTT messages before and after the configuration file is uploaded/downloaded via
 
 Since child device agents typically run on an external device and not on the thin-edge device itself,
 the MQTT and HTTP APIs of thin-edge need to be accessed over the network using its IP address,
-which is configured using the tedge configuration settings `mqtt.external.bind_address` or `mqtt.bind_address`.
+which is configured using the tedge configuration settings `mqtt.external.bind.address` or `mqtt.bind.address`.
 The MQTT APIs are exposed via port 1883 and the HTTP APIs are exposed via port 8000.
 In rare cases, where the child device agent is installed alongside thin-edge on the same device,
 these APIs can be accessed via a local IP or even `127.0.0.1`.
@@ -64,7 +64,7 @@ The child device agent needs to upload this file to thin-edge with an HTTP PUT r
 to the URL: `http://{tedge-ip}:8000/tedge/file-transfer/{child-id}/c8y-configuration-plugin`
 
 * `{tedge-ip}` is the IP of the thin-edge device which is configured as 
-`mqtt.external.bind_address` or `mqtt.bind_address` or `127.0.0.1` if neither is configured.
+`mqtt.external.bind.address` or `mqtt.bind.address` or `127.0.0.1` if neither is configured.
 * `{child-id}` is the child-device-id
 
 Once the upload is complete, the agent should notify thin-edge about the upload by sending the following MQTT message:
diff --git a/docs/src/references/c8y-configuration-management.md b/docs/src/references/c8y-configuration-management.md
index 0d6e6c33708..cd3b72c0250 100644
--- a/docs/src/references/c8y-configuration-management.md
+++ b/docs/src/references/c8y-configuration-management.md
@@ -257,8 +257,8 @@ Note that:
 The behavior of the `c8y-configuration-plugin` is also controlled
 by the configuration of thin-edge:
 
-* `tedge config get mqtt.bind_address`: the address of the local MQTT bus.
-* `tedge config get mqtt.port`: the TCP port of the local MQTT bus.
+* `tedge config get mqtt.bind.address`: the address of the local MQTT bus.
+* `tedge config get mqtt.bind.port`: the TCP port of the local MQTT bus.
 * `tedge config get tmp.path`: the directory where the files are updated
   before being copied atomically to their targets.
 
@@ -339,11 +339,11 @@ From a TCP point of view, the child devices act as clients
 and all the connections to thin-edge are established by the child devices.
 * Thin-edge opens two ports for MQTT and HTTP over the local network.
   These ports are controlled on the main device `tedge config`:
-  * `mqtt.external.bind_address`
-  * `mqtt.external.port`
+  * `mqtt.external.bind.address`
+  * `mqtt.external.bind.port`
   * `http.external.port`
 * The child devices must know the main device IP address.
-  * This is the address set for `mqtt.external.bind_address` on the main device.
+  * This is the address set for `mqtt.external.bind.address` on the main device.
   * For the very specific case, where the child-device agent runs on the main device,
     this connection address can be the `localhost`.
 * On start, the child-device agent for configuration management,
diff --git a/docs/src/references/tedge-file-transfer-service.md b/docs/src/references/tedge-file-transfer-service.md
index 2bd459761cd..75e200aeaf9 100644
--- a/docs/src/references/tedge-file-transfer-service.md
+++ b/docs/src/references/tedge-file-transfer-service.md
@@ -12,11 +12,11 @@ Files can be uploaded, downloaded and deleted from this repository via the follo
 
 The `tedge-ip` is derived from the following tedge configurations:
 
-* mqtt.bind_address
-* mqtt.external.bind_address
+* mqtt.bind.address
+* mqtt.external.bind.address
 
-If the `mqtt.external.bind_address` is configured, then the `tedge-ip` is set to that value,
-else the `mqtt.bind_address` is used with the default value of `127.0.0.1`.
+If the `mqtt.external.bind.address` is configured, then the `tedge-ip` is set to that value,
+else the `mqtt.bind.address` is used with the default value of `127.0.0.1`.
 
 The files uploaded to this repository are stored at `/var/tedge/file-transfer` directory.
 The `{path}/{to}/{resource}` specified in the URL is replicated under this directory.
diff --git a/docs/src/tutorials/child-device-config-management.md b/docs/src/tutorials/child-device-config-management.md
index 2b81e386b0d..a0ff898e084 100644
--- a/docs/src/tutorials/child-device-config-management.md
+++ b/docs/src/tutorials/child-device-config-management.md
@@ -61,7 +61,7 @@ The rest of the MQTT and HTTP interactions would remain the same.
 Since child-device connectors typically run on thin-edge.io device itself, these APIs can be accessed via a local IP or even 127.0.0.1.
 In cases where the child-device connector is deployed on the external child-device itself,
 the MQTT and HTTP APIs of thin-edge.io need to be accessed over the network using its IP address, 
-which is configured using the thin-edge.io configuration settings `mqtt.external.bind_address` or `mqtt.bind_address`.
+which is configured using the thin-edge.io configuration settings `mqtt.external.bind.address` or `mqtt.bind.address`.
 The MQTT APIs are exposed via port 1883 and the HTTP APIs are exposed via port 8000.
 
 In this tutorial 127.0.0.1. is used.
@@ -145,7 +145,7 @@ Follow these steps to bootstrap the child device:
    The child-device connector needs to upload this file to thin-edge.io with an HTTP PUT request to the URL:
    `http://{tedge-ip}:8000/tedge/file-transfer/{child-id}/c8y-configuration-plugin`
    
-   * `{tedge-ip}` is the IP of the thin-edge.io device which is configured as `mqtt.external.bind_address` or `mqtt.bind_address` or
+   * `{tedge-ip}` is the IP of the thin-edge.io device which is configured as `mqtt.external.bind.address` or `mqtt.bind.address` or
      `127.0.0.1` if neither is configured.
    * `{child-id}` is the child-device-id.
    
diff --git a/docs/src/tutorials/connect-aws.md b/docs/src/tutorials/connect-aws.md
index 3f26feba7a2..2971e56f9ef 100644
--- a/docs/src/tutorials/connect-aws.md
+++ b/docs/src/tutorials/connect-aws.md
@@ -91,7 +91,7 @@ The URL is unique to the AWS account and region that is used, and can be found i
 Set the path to the root certificate if necessary. The default is `/etc/ssl/certs`.
 
 ```shell
-sudo tedge config set aws.root.cert.path /etc/ssl/certs/AmazonRootCA1.pem
+sudo tedge config set aws.root_cert_path /etc/ssl/certs/AmazonRootCA1.pem
 ```
 
 This will set the root certificate path of the AWS IoT Hub. In most of the Linux flavors, the certificate will be
diff --git a/docs/src/tutorials/connect-azure.md b/docs/src/tutorials/connect-azure.md
index c0729b6d528..6ebb72f6d7c 100644
--- a/docs/src/tutorials/connect-azure.md
+++ b/docs/src/tutorials/connect-azure.md
@@ -94,7 +94,7 @@ The URL/Hostname can be found in the Azure web portal, clicking on the overview
 Set the path to the root certificate if necessary. The default is `/etc/ssl/certs`.
 
 ```shell
-sudo tedge config set az.root.cert.path /etc/ssl/certs/Baltimore_CyberTrust_Root.pem
+sudo tedge config set az.root_cert_path /etc/ssl/certs/Baltimore_CyberTrust_Root.pem
 ```
 
 This will set the root certificate path of the Azure IoT Hub.
diff --git a/docs/src/tutorials/connect-c8y.md b/docs/src/tutorials/connect-c8y.md
index 19472a82d32..4c2b4ed843d 100644
--- a/docs/src/tutorials/connect-c8y.md
+++ b/docs/src/tutorials/connect-c8y.md
@@ -38,7 +38,7 @@ sudo tedge config set c8y.url your-tenant.cumulocity.com
 Set the path to the root certificate if necessary. The default is `/etc/ssl/certs`.
 
 ```
-sudo tedge config set c8y.root.cert.path /etc/ssl/certs
+sudo tedge config set c8y.root_cert_path /etc/ssl/certs
 ```
 
 This will set the root certificate path of the Cumulocity IoT.
@@ -49,10 +49,10 @@ If not found download it from [here](https://www.identrust.com/dst-root-ca-x3).
 ## Connecting to Cumulocity server signed with self-signed certificate
 
 If the Cumulocity IoT instance that you're connecting to, is signed with a self-signed certificate(eg: Cumulocity IoT Edge instance),
-then the path to that server certificate must be set as the c8y.root.cert.path as follows:
+then the path to that server certificate must be set as the c8y.root_cert_path as follows:
 
 ```
-sudo tedge config set c8y.root.cert.path /path/to/the/self-signed/certificate
+sudo tedge config set c8y.root_cert_path /path/to/the/self-signed/certificate
 ```
 
 ```admonish warning
diff --git a/plugins/c8y_firmware_plugin/src/main.rs b/plugins/c8y_firmware_plugin/src/main.rs
index a810699d58c..e231cbc0b3e 100644
--- a/plugins/c8y_firmware_plugin/src/main.rs
+++ b/plugins/c8y_firmware_plugin/src/main.rs
@@ -28,7 +28,7 @@ During a successful operation, `c8y-firmware-plugin` updates the installed firmw
 The thin-edge `CONFIG_DIR` is used to find where:
   * to store temporary files on download: `tedge config get tmp.path`,
   * to log operation errors and progress: `tedge config get log.path`,
-  * to connect the MQTT bus: `tedge config get mqtt.port`,
+  * to connect the MQTT bus: `tedge config get mqtt.bind.port`,
   * to timeout pending operations: `tedge config get firmware.child.update.timeout"#;
 
 #[derive(Debug, clap::Parser)]
diff --git a/tests/PySys/misc_features/invalid_device_id/run.py b/tests/PySys/misc_features/invalid_device_id/run.py
index a9e674689e1..a5c4120704f 100644
--- a/tests/PySys/misc_features/invalid_device_id/run.py
+++ b/tests/PySys/misc_features/invalid_device_id/run.py
@@ -39,7 +39,7 @@ def setup(self):
                 self.tedge,
                 "config",
                 "set",
-                "device.cert.path",
+                "device.cert_path",
                 "/tmp/test-device-certs/tedge-certificate.pem",
             ],
             stdouterr="set_cert_path",
@@ -52,7 +52,7 @@ def setup(self):
                 self.tedge,
                 "config",
                 "set",
-                "device.key.path",
+                "device.key_path",
                 "/tmp/test-device-certs/tedge-private-key.pem",
             ],
             stdouterr="set_key_path",
@@ -80,17 +80,17 @@ def validate(self):
         self.assertGrep("cert_create.err", "DeviceID Error", contains=True)
 
     def device_id_cleanup(self):
-        # unset the custom device.cert.path
+        # unset the custom device.cert_path
         unset_cert_path = self.startProcess(
             command=self.sudo,
-            arguments=[self.tedge, "config", "unset", "device.cert.path"],
+            arguments=[self.tedge, "config", "unset", "device.cert_path"],
             stdouterr="unset_cert_path",
         )
 
-        # unset the custom device.key.path
+        # unset the custom device.key_path
         unset_key_path = self.startProcess(
             command=self.sudo,
-            arguments=[self.tedge, "config", "unset", "device.key.path"],
+            arguments=[self.tedge, "config", "unset", "device.key_path"],
             stdouterr="unset_key_path",
         )
 
diff --git a/tests/PySys/misc_features/mqtt_port_change_connection_fails/run.py b/tests/PySys/misc_features/mqtt_port_change_connection_fails/run.py
index ae4bcd185d3..73a3ecc1d1d 100644
--- a/tests/PySys/misc_features/mqtt_port_change_connection_fails/run.py
+++ b/tests/PySys/misc_features/mqtt_port_change_connection_fails/run.py
@@ -8,7 +8,7 @@
 Validate changing the mqtt port using the tedge command that fails without restarting the mqtt server
 
 Given a configured system, that is configured with certificate created and registered in a cloud
-When `tedge mqtt.port set` with `sudo`
+When `tedge mqtt.bind.port set` with `sudo`
 When the `sudo tedge mqtt sub` tries to subscribe for a topic and fails to connect to mqtt server
 When the `sudo tedge mqtt pub` tries to publish a message and fails to connect to mqtt server
 
@@ -26,7 +26,7 @@ def execute(self):
         # set a new mqtt port for local communication
         mqtt_port = self.startProcess(
             command=self.sudo,
-            arguments=[self.tedge, "config", "set", "mqtt.port", "8880"],
+            arguments=[self.tedge, "config", "set", "mqtt.bind.port", "8880"],
             stdouterr="mqtt_port_set",
         )
 
@@ -57,6 +57,6 @@ def mqtt_cleanup(self):
         # unset a new mqtt port, falls back to default port (1883)
         mqtt_port = self.startProcess(
             command=self.sudo,
-            arguments=[self.tedge, "config", "unset", "mqtt.port"],
+            arguments=[self.tedge, "config", "unset", "mqtt.bind.port"],
             stdouterr="mqtt_port_unset",
         )
diff --git a/tests/PySys/misc_features/mqtt_port_change_connection_works/run.py b/tests/PySys/misc_features/mqtt_port_change_connection_works/run.py
index 4eab578e73c..c9a575f9639 100644
--- a/tests/PySys/misc_features/mqtt_port_change_connection_works/run.py
+++ b/tests/PySys/misc_features/mqtt_port_change_connection_works/run.py
@@ -10,7 +10,7 @@
 
 Given a configured system, that is configured with certificate created and registered in a cloud
 When the thin edge device is disconnected from cloud, `sudo tedge disconnect c8y`
-When `tedge mqtt.port set` with sudo
+When `tedge mqtt.bind.port set` with sudo
 When the thin edge device is connected to cloud, `sudo tedge connect c8y`
 Now validate the services that use the mqtt port
    Validate tedge mqtt pub/sub
@@ -32,7 +32,7 @@ def setup(self):
         # set a new mqtt port for local communication
         mqtt_port = self.startProcess(
             command=self.sudo,
-            arguments=[self.tedge, "config", "set", "mqtt.port", "8889"],
+            arguments=[self.tedge, "config", "set", "mqtt.bind.port", "8889"],
             stdouterr="mqtt_port",
         )
         self.addCleanupFunction(self.mqtt_cleanup)
@@ -177,7 +177,7 @@ def mqtt_cleanup(self):
         # unset a new mqtt port, falls back to default port (1883)
         mqtt_port = self.startProcess(
             command=self.sudo,
-            arguments=[self.tedge, "config", "unset", "mqtt.port"],
+            arguments=[self.tedge, "config", "unset", "mqtt.bind.port"],
             stdouterr="mqtt_port_unset",
         )
 
diff --git a/tests/PySys/misc_features/valid_device_id/run.py b/tests/PySys/misc_features/valid_device_id/run.py
index 264ad768f60..74203f6cd22 100644
--- a/tests/PySys/misc_features/valid_device_id/run.py
+++ b/tests/PySys/misc_features/valid_device_id/run.py
@@ -43,7 +43,7 @@ def setup(self):
                 self.tedge,
                 "config",
                 "set",
-                "device.cert.path",
+                "device.cert_path",
                 "/tmp/test-device-certs/tedge-certificate.pem",
             ],
             stdouterr="set_cert_path",
@@ -56,7 +56,7 @@ def setup(self):
                 self.tedge,
                 "config",
                 "set",
-                "device.key.path",
+                "device.key_path",
                 "/tmp/test-device-certs/tedge-private-key.pem",
             ],
             stdouterr="set_key_path",
@@ -115,14 +115,14 @@ def device_id_cleanup(self):
         # unset the device certificate path
         unset_cert_path = self.startProcess(
             command=self.sudo,
-            arguments=[self.tedge, "config", "unset", "device.cert.path"],
+            arguments=[self.tedge, "config", "unset", "device.cert_path"],
             stdouterr="unset_cert_path",
         )
 
         # unset the device key path
         unset_key_path = self.startProcess(
             command=self.sudo,
-            arguments=[self.tedge, "config", "unset", "device.key.path"],
+            arguments=[self.tedge, "config", "unset", "device.key_path"],
             stdouterr="unset_key_path",
         )
 
diff --git a/tests/PySys/tedge/call_tedge_config_list/run.py b/tests/PySys/tedge/call_tedge_config_list/run.py
index 945b8236bce..6ffec212571 100644
--- a/tests/PySys/tedge/call_tedge_config_list/run.py
+++ b/tests/PySys/tedge/call_tedge_config_list/run.py
@@ -33,12 +33,12 @@
 """
 
 configdict = {
-    "device.key.path": "",
-    "device.cert.path": "",
+    "device.key_path": "",
+    "device.cert_path": "",
     "c8y.url": "",
-    "c8y.root.cert.path": "",
+    "c8y.root_cert_path": "",
     "az.url": "",
-    "az.root.cert.path": "",
+    "az.root_cert_path": "",
 }
 
 DEFAULTKEYPATH = "/etc/tedge/device-certs/tedge-private-key.pem"
@@ -118,21 +118,21 @@ def execute(self):
             valueread = self.get_config_key(key)
             self.log.debug(f"Key: {key} Value: {valueread}")
             # Some values have defaults that are set instead of nothing:
-            if key == "device.key.path":
+            if key == "device.key_path":
                 self.assertThat(
                     "expect == valueread", expect=DEFAULTKEYPATH, valueread=valueread
                 )
-            elif key == "device.cert.path":
+            elif key == "device.cert_path":
                 self.assertThat(
                     "expect == valueread", expect=DEFAULTCERTPATH, valueread=valueread
                 )
-            elif key == "c8y.root.cert.path":
+            elif key == "c8y.root_cert_path":
                 self.assertThat(
                     "expect == valueread",
                     expect=DEFAULTROOTCERTPATH,
                     valueread=valueread,
                 )
-            elif key == "az.root.cert.path":
+            elif key == "az.root_cert_path":
                 self.assertThat(
                     "expect == valueread",
                     expect=DEFAULTROOTCERTPATH,
diff --git a/tests/RobotFramework/libraries/ThinEdgeIO/ThinEdgeIO.py b/tests/RobotFramework/libraries/ThinEdgeIO/ThinEdgeIO.py
index 6a3049ec0e4..7e72949c264 100644
--- a/tests/RobotFramework/libraries/ThinEdgeIO/ThinEdgeIO.py
+++ b/tests/RobotFramework/libraries/ThinEdgeIO/ThinEdgeIO.py
@@ -404,7 +404,7 @@ def assert_service_health_status_equal(
         return self._assert_health_status(service, status=status)
 
     def _assert_health_status(self, service: str, status: str) -> Dict[str, Any]:
-        # if mqtt.client.auth.cafile or mqtt.client.auth.cadir is set, we pass setting
+        # if mqtt.client.auth.ca_file or mqtt.client.auth.ca_dir is set, we pass setting
         # value to mosquitto_sub
         mqtt_config_options = self.execute_command(f"tedge config list",
             stdout=True,
@@ -413,11 +413,11 @@ def _assert_health_status(self, service: str, status: str) -> Dict[str, Any]:
         )
         
         server_auth = ""
-        if "mqtt.client.auth.ca" in mqtt_config_options:
+        if "mqtt.client.auth.ca_file" in mqtt_config_options:
             server_auth = "--cafile /etc/mosquitto/ca_certificates/ca.crt"
 
         client_auth = ""
-        if "mqtt.client.auth.certfile" in mqtt_config_options:
+        if "mqtt.client.auth.cert_file" in mqtt_config_options:
             client_auth = "--cert /setup/client.crt --key /setup/client.key"
 
         message = self.execute_command(
diff --git a/tests/RobotFramework/tests/config_management/child_conf_mgmt_plugin.robot b/tests/RobotFramework/tests/config_management/child_conf_mgmt_plugin.robot
index 9f325b1a6ef..2104e9c2997 100644
--- a/tests/RobotFramework/tests/config_management/child_conf_mgmt_plugin.robot
+++ b/tests/RobotFramework/tests/config_management/child_conf_mgmt_plugin.robot
@@ -1,81 +1,83 @@
+*** Comments ***
 #PRECONDITION:
 #Device CH_DEV_CONF_MGMT is existing on tenant, if not
 #use -v DeviceID:xxxxxxxxxxx in the command line to use your existing device
 
+
 *** Settings ***
-Resource    ../../resources/common.resource
-Library    ThinEdgeIO
-Library    Cumulocity
-Library    JSONLibrary
-Library    Collections
+Resource            ../../resources/common.resource
+Library             ThinEdgeIO
+Library             Cumulocity
+Library             JSONLibrary
+Library             Collections
 
-Test Tags    theme:configuration    theme:childdevices
-Suite Setup    Custom Setup
-Suite Teardown    Get Logs    name=${PARENT_SN}
+Suite Setup         Custom Setup
+Suite Teardown      Get Logs    name=${PARENT_SN}
 
-*** Variables ***
+Force Tags          theme:configuration    theme:childdevices
 
+
+*** Variables ***
 ${PARENT_IP}
-${HTTP_PORT}             8000
+${HTTP_PORT}        8000
 
-${config}                "files = [\n\t { path = '/home/pi/config1', type = 'config1' },\n ]\n"
+${config}           "files = [\n\t { path = '/home/pi/config1', type = 'config1' },\n ]\n"
 ${PARENT_SN}
 ${CHILD_SN}
 
-${topic_snap}            /commands/res/config_snapshot"
-${topic_upd}             /commands/res/config_update"
-${payl_notify}           '{"status": null,  "path": "", "type":"c8y-configuration-plugin", "reason": null}'
-${payl_exec}             '{"status": "executing", "path": "/home/pi/config1", "type": "config1", "reason": null}'
-${payl_succ}             '{"status": "successful", "path": "/home/pi/config1", "type": "config1", "reason": null}'
+${topic_snap}       /commands/res/config_snapshot"
+${topic_upd}        /commands/res/config_update"
+${payl_notify}      '{"status": null,    "path": "", "type":"c8y-configuration-plugin", "reason": null}'
+${payl_exec}        '{"status": "executing", "path": "/home/pi/config1", "type": "config1", "reason": null}'
+${payl_succ}        '{"status": "successful", "path": "/home/pi/config1", "type": "config1", "reason": null}'
 
 ${CHILD_CONFIG}=    SEPARATOR=\n
-    ...    files = [
-    ...        { path = '/home/pi/config1', type = 'config1' },
-    ...    ]
+...                 files = [
+...                 { path = '/home/pi/config1', type = 'config1' },
+...                 ]
 
-*** Test Cases ***
 
+*** Test Cases ***
 Prerequisite Parent
-    Set Device Context    ${PARENT_SN}                                   #Creates ssh connection to the parent device
+    Set Device Context    ${PARENT_SN}    #Creates ssh connection to the parent device
     Execute Command    sudo tedge disconnect c8y
 
-    Delete child related content                        #Delete any previous created child related configuration files/folders on the parent device
-    Check for child related content                     #Checks if folders that will trigger child device creation are existing
-    Set external MQTT bind address                      #Setting external MQTT bind address which child will use for communication
-    Set external MQTT port                              #Setting external MQTT port which child will use for communication Default:1883
+    Delete child related content    #Delete any previous created child related configuration files/folders on the parent device
+    Check for child related content    #Checks if folders that will trigger child device creation are existing
+    Set external MQTT bind address    #Setting external MQTT bind address which child will use for communication
+    Set external MQTT port    #Setting external MQTT port which child will use for communication Default:1883
 
     Sleep    3s
     Execute Command    sudo tedge connect c8y
-    Restart Configuration plugin                        #Stop and Start c8y-configuration-plugin
+    Restart Configuration plugin    #Stop and Start c8y-configuration-plugin
     Cumulocity.Log Device Info
 
 Prerequisite Child
-    Child device delete configuration files             #Delete any previous created child related configuration files/folders on the child device
+    Child device delete configuration files    #Delete any previous created child related configuration files/folders on the child device
 
 Child device bootstrapping
-    Startup child device                                #Setting up/Bootstrapping of a child device
-    Validate child Name                                 #This is to check the existence of the bug: https://github.com/thin-edge/thin-edge.io/issues/1569
+    Startup child device    #Setting up/Bootstrapping of a child device
+    Validate child Name    #This is to check the existence of the bug: https://github.com/thin-edge/thin-edge.io/issues/1569
 
 Snapshot from device
-    Request snapshot from child device                  #Using the cloud command: "Get snapshot from device"
-    Child device response on snapshot request           #Child device is sending 'executing' and 'successful' MQTT responses
-    No response from child device on snapshot request   #Tests the failing of request after timeout of 10s
+    Request snapshot from child device    #Using the cloud command: "Get snapshot from device"
+    Child device response on snapshot request    #Child device is sending 'executing' and 'successful' MQTT responses
+    No response from child device on snapshot request    #Tests the failing of request after timeout of 10s
 
 Child device config update
-    Send configuration to device                        #Using the cloud command: "Send configuration to device"
-    Child device response on update request             #Child device is sending 'executing' and 'successful' MQTT responses
-    No response from child device on config update      #Tests the failing of request after timeout of 10s
+    Send configuration to device    #Using the cloud command: "Send configuration to device"
+    Child device response on update request    #Child device is sending 'executing' and 'successful' MQTT responses
+    No response from child device on config update    #Tests the failing of request after timeout of 10s
 
 
 *** Keywords ***
-
 Set external MQTT bind address
     Set Device Context    ${PARENT_SN}
-    Execute Command    sudo tedge config set mqtt.external.bind_address ${PARENT_IP}
+    Execute Command    sudo tedge config set mqtt.external.bind.address ${PARENT_IP}
 
 Set external MQTT port
     Set Device Context    ${PARENT_SN}
-    Execute Command    sudo tedge config set mqtt.external.port 1883
+    Execute Command    sudo tedge config set mqtt.external.bind.port 1883
 
 Check for child related content
     Set Device Context    ${CHILD_SN}
@@ -84,9 +86,9 @@ Check for child related content
     Directory Should Not Have Sub Directories    /var/tedge
 
 Delete child related content
-    Execute Command    sudo rm -rf /etc/tedge/operations/c8y/TST*         #if folder exists, child device will be created
+    Execute Command    sudo rm -rf /etc/tedge/operations/c8y/TST*    #if folder exists, child device will be created
     Execute Command    sudo rm -f c8y-configuration-plugin.toml
-    Execute Command    sudo rm -rf /etc/tedge/c8y/TST*                    #if folder exists, child device will be created
+    Execute Command    sudo rm -rf /etc/tedge/c8y/TST*    #if folder exists, child device will be created
     Execute Command    sudo rm -rf /var/tedge/*
 
 Check parent child relationship
@@ -110,16 +112,17 @@ Child device delete configuration files
 Validate child Name
     Device Should Exist    ${CHILD_SN}
     ${child_mo}=    Cumulocity.Device Should Have Fragments    name
-    Should Be Equal    device_${PARENT_SN}     ${child_mo["owner"]}    # The parent is the owner of the child
+    Should Be Equal    device_${PARENT_SN}    ${child_mo["owner"]}    # The parent is the owner of the child
 
 Startup child device
-    Sleep    5s        reason=The registration of child devices is flakey
+    Sleep    5s    reason=The registration of child devices is flakey
     Set Device Context    ${CHILD_SN}
     Execute Command    printf ${config} > c8y-configuration-plugin
 
-    Execute Command    curl -X PUT http://${PARENT_IP}:${HTTP_PORT}/tedge/file-transfer/${CHILD_SN}/c8y-configuration-plugin --data-binary "${CHILD_CONFIG}"
+    Execute Command
+    ...    curl -X PUT http://${PARENT_IP}:${HTTP_PORT}/tedge/file-transfer/${CHILD_SN}/c8y-configuration-plugin --data-binary "${CHILD_CONFIG}"
 
-    Sleep    5s        reason=The registration of child devices is flakey
+    Sleep    5s    reason=The registration of child devices is flakey
 
     Execute Command    sudo apt-get install mosquitto-clients -y
     Execute Command    mosquitto_pub -h ${PARENT_IP} -t "tedge/${CHILD_SN}${topic_snap} -m ${payl_notify} -q 1
@@ -131,7 +134,9 @@ Request snapshot from child device
 
     Set Device Context    ${PARENT_SN}
     @{listen}=    Should Have MQTT Messages    topic=tedge/${CHILD_SN}/commands/req/config_snapshot    date_from=-5s
-    Should Be Equal    @{listen}    {"url":"http://${PARENT_IP}:${HTTP_PORT}/tedge/file-transfer/${CHILD_SN}/config_snapshot/config1","path":"/home/pi/config1","type":"config1"}
+    Should Be Equal
+    ...    @{listen}
+    ...    {"url":"http://${PARENT_IP}:${HTTP_PORT}/tedge/file-transfer/${CHILD_SN}/config_snapshot/config1","path":"/home/pi/config1","type":"config1"}
 
     #CHECK OPERATION
     Cumulocity.Operation Should Be PENDING    ${operation}
@@ -140,7 +145,8 @@ Child device response on snapshot request
     Set Device Context    ${CHILD_SN}
     Execute Command    mosquitto_pub -h ${PARENT_IP} -t "tedge/${CHILD_SN}${topic_snap} -m ${payl_exec}
 
-    Execute Command    curl -X PUT --data-binary @/home/pi/config1 http://${PARENT_IP}:${HTTP_PORT}/tedge/file-transfer/${CHILD_SN}/config_snapshot/config1
+    Execute Command
+    ...    curl -X PUT --data-binary @/home/pi/config1 http://${PARENT_IP}:${HTTP_PORT}/tedge/file-transfer/${CHILD_SN}/config_snapshot/config1
 
     Sleep    5s
     Execute Command    mosquitto_pub -h ${PARENT_IP} -t "tedge/${CHILD_SN}${topic_snap} -m ${payl_succ}
@@ -151,13 +157,18 @@ Child device response on snapshot request
     Cumulocity.Operation Should Be SUCCESSFUL    ${operation}
 
 Send configuration to device
-    ${file_url}=     Create Inventory Binary    test-config.toml    config1    contents=Dummy config
-    ${operation}=    Set Configuration    config1    ${file_url}    description=Send configuration snapshot config1 of configuration type config1 to device ${CHILD_SN}
+    ${file_url}=    Create Inventory Binary    test-config.toml    config1    contents=Dummy config
+    ${operation}=    Set Configuration
+    ...    config1
+    ...    ${file_url}
+    ...    description=Send configuration snapshot config1 of configuration type config1 to device ${CHILD_SN}
     Set Suite Variable    $operation
 
     Set Device Context    ${PARENT_SN}
-    @{listen}=    Should Have MQTT Messages    topic=tedge/${CHILD_SN}/commands/req/config_update        date_from=-5s
-    Should Be Equal    @{listen}    {"url":"http://${PARENT_IP}:${HTTP_PORT}/tedge/file-transfer/${CHILD_SN}/config_update/config1","path":"/home/pi/config1","type":"config1"}
+    @{listen}=    Should Have MQTT Messages    topic=tedge/${CHILD_SN}/commands/req/config_update    date_from=-5s
+    Should Be Equal
+    ...    @{listen}
+    ...    {"url":"http://${PARENT_IP}:${HTTP_PORT}/tedge/file-transfer/${CHILD_SN}/config_update/config1","path":"/home/pi/config1","type":"config1"}
 
     #CHECK OPERATION
     Operation Should Be DELIVERED    ${operation}
@@ -166,9 +177,10 @@ Child device response on update request
     Set Device Context    ${CHILD_SN}
     Execute Command    mosquitto_pub -h ${PARENT_IP} -t "tedge/${CHILD_SN}${topic_upd} -m ${payl_exec}
 
-    Execute Command    curl http://${PARENT_IP}:${HTTP_PORT}/tedge/file-transfer/${CHILD_SN}/config_update/config1 --output config1
+    Execute Command
+    ...    curl http://${PARENT_IP}:${HTTP_PORT}/tedge/file-transfer/${CHILD_SN}/config_update/config1 --output config1
 
-    # Sleep    5s             #Enable if tests starts to fail
+    # Sleep    5s    #Enable if tests starts to fail
     Execute Command    mosquitto_pub -h ${PARENT_IP} -t "tedge/${CHILD_SN}${topic_upd} -m ${payl_succ}
 
     Cumulocity.Operation Should Be SUCCESSFUL    ${operation}
@@ -178,21 +190,31 @@ No response from child device on snapshot request
 
     Set Device Context    ${PARENT_SN}
     @{listen}=    Should Have MQTT Messages    topic=tedge/${CHILD_SN}/commands/req/config_snapshot
-    Should Be Equal    @{listen}    {"url":"http://${PARENT_IP}:${HTTP_PORT}/tedge/file-transfer/${CHILD_SN}/config_snapshot/config1","path":"/home/pi/config1","type":"config1"}
+    Should Be Equal
+    ...    @{listen}
+    ...    {"url":"http://${PARENT_IP}:${HTTP_PORT}/tedge/file-transfer/${CHILD_SN}/config_snapshot/config1","path":"/home/pi/config1","type":"config1"}
 
     #CHECK TIMEOUT MESSAGE
-    Cumulocity.Operation Should Be FAILED    ${operation}    failure_reason=Timeout due to lack of response from child device: ${CHILD_SN} for config type: config1    timeout=60
+    Cumulocity.Operation Should Be FAILED
+    ...    ${operation}
+    ...    failure_reason=Timeout due to lack of response from child device: ${CHILD_SN} for config type: config1
+    ...    timeout=60
 
 No response from child device on config update
-    ${file_url}=     Create Inventory Binary    test-config.toml    config1    contents=Dummy config
+    ${file_url}=    Create Inventory Binary    test-config.toml    config1    contents=Dummy config
     ${operation}=    Set Configuration    config1    ${file_url}
 
     Set Device Context    ${PARENT_SN}
     @{listen}=    Should Have MQTT Messages    topic=tedge/${CHILD_SN}/commands/req/config_update
-    Should Be Equal    @{listen}    {"url":"http://${PARENT_IP}:${HTTP_PORT}/tedge/file-transfer/${CHILD_SN}/config_update/config1","path":"/home/pi/config1","type":"config1"}
+    Should Be Equal
+    ...    @{listen}
+    ...    {"url":"http://${PARENT_IP}:${HTTP_PORT}/tedge/file-transfer/${CHILD_SN}/config_update/config1","path":"/home/pi/config1","type":"config1"}
 
     #CHECK OPERATION
-    Cumulocity.Operation Should Be FAILED    ${operation}    failure_reason=Timeout due to lack of response from child device: ${CHILD_SN} for config type: config1    timeout=60
+    Cumulocity.Operation Should Be FAILED
+    ...    ${operation}
+    ...    failure_reason=Timeout due to lack of response from child device: ${CHILD_SN} for config type: config1
+    ...    timeout=60
 
 Custom Setup
     # Parent
@@ -200,8 +222,8 @@ Custom Setup
     Set Suite Variable    $PARENT_SN    ${parent_sn}
 
     ${parent_ip}=    Get IP Address
-    Set Suite Variable    $PARENT_IP         ${parent_ip}
+    Set Suite Variable    $PARENT_IP    ${parent_ip}
 
     # Child
     ${child_sn}=    Setup    skip_bootstrap=True
-    Set Suite Variable    $CHILD_SN         ${child_sn}
+    Set Suite Variable    $CHILD_SN    ${child_sn}
diff --git a/tests/RobotFramework/tests/cumulocity/firmware/firmware_operation_child_device.robot b/tests/RobotFramework/tests/cumulocity/firmware/firmware_operation_child_device.robot
index 6c0a514a55e..e22e86414d3 100644
--- a/tests/RobotFramework/tests/cumulocity/firmware/firmware_operation_child_device.robot
+++ b/tests/RobotFramework/tests/cumulocity/firmware/firmware_operation_child_device.robot
@@ -1,19 +1,19 @@
 *** Settings ***
-Resource    ../../../resources/common.resource
-Library    Cumulocity
-Library    ThinEdgeIO
-Library    JSONLibrary
-Library    String
+Resource            ../../../resources/common.resource
+Library             Cumulocity
+Library             ThinEdgeIO
+Library             JSONLibrary
+Library             String
 
-Suite Setup    Custom Setup
-Test Teardown    Get Logs    name=${PARENT_SN}
+Suite Setup         Custom Setup
+Test Teardown       Get Logs    name=${PARENT_SN}
 
-Force Tags    theme:firmware    theme:childdevices
+Force Tags          theme:firmware    theme:childdevices
 
-*** Variables ***
 
+*** Variables ***
 ${PARENT_IP}
-${HTTP_PORT}    8000
+${HTTP_PORT}                8000
 
 ${PARENT_SN}
 ${CHILD_SN}
@@ -24,6 +24,7 @@ ${file_url}
 ${file_transfer_url}
 ${file_creation_time}
 
+
 *** Test Cases ***
 Prerequisite Parent
     Set Device Context    ${PARENT_SN}    #Creates ssh connection to the parent device
@@ -59,10 +60,11 @@ Child device firmware update with cache
     Validate Cumulocity operation status and MO
     Validate if file is not newly downloaded
 
+
 *** Keywords ***
 Delete child related content
-    Execute Command    sudo rm -rf /etc/tedge/operations/c8y/TST*         #if folder exists, child device will be created
-    Execute Command    sudo rm -rf /etc/tedge/c8y/TST*                    #if folder exists, child device will be created
+    Execute Command    sudo rm -rf /etc/tedge/operations/c8y/TST*    #if folder exists, child device will be created
+    Execute Command    sudo rm -rf /etc/tedge/c8y/TST*    #if folder exists, child device will be created
     Execute Command    sudo rm -rf /var/tedge/file-transfer/*
     Execute Command    sudo rm -rf /var/tedge/cache/*
     Execute Command    sudo rm -rf /var/tedge/firmware/*
@@ -77,11 +79,11 @@ Check for child related content
 
 Set external MQTT bind address
     Set Device Context    ${PARENT_SN}
-    Execute Command    sudo tedge config set mqtt.external.bind_address ${PARENT_IP}
+    Execute Command    sudo tedge config set mqtt.external.bind.address ${PARENT_IP}
 
 Set external MQTT port
     Set Device Context    ${PARENT_SN}
-    Execute Command    sudo tedge config set mqtt.external.port 1883
+    Execute Command    sudo tedge config set mqtt.external.bind.port 1883
 
 Restart Firmware plugin
     ThinEdgeIO.Restart Service    c8y-firmware-plugin.service
@@ -103,7 +105,7 @@ Create child device
 
 Validate child Name
     ${child_mo}=    Cumulocity.Device Should Have Fragments    name
-    Should Be Equal    device_${PARENT_SN}     ${child_mo["owner"]}    # The parent is the owner of the child
+    Should Be Equal    device_${PARENT_SN}    ${child_mo["owner"]}    # The parent is the owner of the child
 
 Get timestamp of cache
     Set Device Context    ${PARENT_SN}
@@ -111,7 +113,7 @@ Get timestamp of cache
     Set Suite Variable    $file_creation_time
 
 Upload binary to Cumulocity
-    ${file_url}=     Cumulocity.Create Inventory Binary    firmware1.txt    firmware1    file=${CURDIR}/firmware1.txt
+    ${file_url}=    Cumulocity.Create Inventory Binary    firmware1.txt    firmware1    file=${CURDIR}/firmware1.txt
     Set Suite Variable    $file_url
 
 Create c8y_Firmware operation
@@ -126,7 +128,9 @@ Validate if file is not newly downloaded
 
 Validate firmware update request
     Set Device Context    ${PARENT_SN}
-    ${listen}=    ThinEdgeIO.Should Have MQTT Messages    topic=tedge/${CHILD_SN}/commands/req/firmware_update    date_from=-5s
+    ${listen}=    ThinEdgeIO.Should Have MQTT Messages
+    ...    topic=tedge/${CHILD_SN}/commands/req/firmware_update
+    ...    date_from=-5s
     ${message}=    JSONLibrary.Convert String To Json    ${listen[0]}
 
     Should Not Be Empty    ${message["id"]}
@@ -134,9 +138,14 @@ Validate firmware update request
     Should Be Equal    ${message["name"]}    firmware1
     Should Be Equal    ${message["version"]}    1.0
     Should Be Equal    ${message["sha256"]}    4b0126519dfc1a3023851bfcc5b312b20fc80452256f7f40a5d8722765500ba9
-    Should Match Regexp    ${message["url"]}    ^http://${PARENT_IP}:${HTTP_PORT}/tedge/file-transfer/${CHILD_SN}/firmware_update/[0-9A-Za-z]+$
+    Should Match Regexp
+    ...    ${message["url"]}
+    ...    ^http://${PARENT_IP}:${HTTP_PORT}/tedge/file-transfer/${CHILD_SN}/firmware_update/[0-9A-Za-z]+$
 
-    ${cache_key}=    Get Regexp Matches    ${message["url"]}    ^http://${PARENT_IP}:${HTTP_PORT}/tedge/file-transfer/${CHILD_SN}/firmware_update/([0-9A-Za-z]+)$    1
+    ${cache_key}=    Get Regexp Matches
+    ...    ${message["url"]}
+    ...    ^http://${PARENT_IP}:${HTTP_PORT}/tedge/file-transfer/${CHILD_SN}/firmware_update/([0-9A-Za-z]+)$
+    ...    1
 
     Set Suite Variable    $op_id    ${message["id"]}
     Set Suite Variable    $file_transfer_url    ${message["url"]}
@@ -164,4 +173,4 @@ Custom Setup
 
     # Child
     ${child_sn}=    Setup    skip_bootstrap=True
-    Set Suite Variable    $CHILD_SN         ${child_sn}
+    Set Suite Variable    $CHILD_SN    ${child_sn}
diff --git a/tests/RobotFramework/tests/mqtt/basic_pub_sub.robot b/tests/RobotFramework/tests/mqtt/basic_pub_sub.robot
index c87df3cfb07..240dab00dea 100644
--- a/tests/RobotFramework/tests/mqtt/basic_pub_sub.robot
+++ b/tests/RobotFramework/tests/mqtt/basic_pub_sub.robot
@@ -10,22 +10,23 @@ Force Tags      theme:mqtt
 
 *** Test Cases ***
 Publish on a local insecure broker
-    Start Service      mosquitto
+    Start Service    mosquitto
     Execute Command    tedge mqtt pub topic message
 
 Publish on a local secure broker
     Set up broker server authentication
-    Restart Service      mosquitto
+    Restart Service    mosquitto
     tedge configure MQTT server authentication
     Execute Command    tedge mqtt pub topic message
 
 Publish on a local secure broker with client authentication
     Set up broker with server and client authentication
-    Restart Service      mosquitto
+    Restart Service    mosquitto
     tedge configure MQTT server authentication
     tedge configure MQTT client authentication
     Execute Command    tedge mqtt pub topic message
 
+
 *** Keywords ***
 Custom Setup
     Setup    skip_bootstrap=True
@@ -33,22 +34,24 @@ Custom Setup
 
 Set up broker server authentication
     Transfer To Device    ${CURDIR}/mosquitto-server-auth.conf    /etc/tedge/mosquitto-conf/
-    Execute Command       mv /etc/tedge/mosquitto-conf/mosquitto-server-auth.conf /etc/tedge/mosquitto-conf/tedge-mosquitto.conf
+    Execute Command
+    ...    mv /etc/tedge/mosquitto-conf/mosquitto-server-auth.conf /etc/tedge/mosquitto-conf/tedge-mosquitto.conf
     Transfer To Device    ${CURDIR}/gen_certs.sh    /root/gen_certs.sh
-    Execute Command       chmod u+x /root/gen_certs.sh
-    Execute Command       /root/gen_certs.sh
+    Execute Command    chmod u+x /root/gen_certs.sh
+    Execute Command    /root/gen_certs.sh
 
 Set up broker with server and client authentication
     Transfer To Device    ${CURDIR}/mosquitto-client-auth.conf    /etc/tedge/mosquitto-conf/mosquitto-client-auth.conf
-    Execute Command       mv /etc/tedge/mosquitto-conf/mosquitto-client-auth.conf /etc/tedge/mosquitto-conf/tedge-mosquitto.conf
+    Execute Command
+    ...    mv /etc/tedge/mosquitto-conf/mosquitto-client-auth.conf /etc/tedge/mosquitto-conf/tedge-mosquitto.conf
     Transfer To Device    ${CURDIR}/gen_certs.sh    /root/gen_certs.sh
-    Execute Command       chmod u+x /root/gen_certs.sh
-    Execute Command       /root/gen_certs.sh
+    Execute Command    chmod u+x /root/gen_certs.sh
+    Execute Command    /root/gen_certs.sh
 
 tedge configure MQTT server authentication
     Execute Command    tedge config set mqtt.client.port 8883
-    Execute Command    tedge config set mqtt.client.auth.cafile /etc/mosquitto/ca_certificates/ca.crt
+    Execute Command    tedge config set mqtt.client.auth.ca_file /etc/mosquitto/ca_certificates/ca.crt
 
 tedge configure MQTT client authentication
-    Execute Command    tedge config set mqtt.client.auth.certfile client.crt
-    Execute Command    tedge config set mqtt.client.auth.keyfile client.key
+    Execute Command    tedge config set mqtt.client.auth.cert_file client.crt
+    Execute Command    tedge config set mqtt.client.auth.key_file client.key
diff --git a/tests/RobotFramework/tests/mqtt/remote_mqtt_broker.robot b/tests/RobotFramework/tests/mqtt/remote_mqtt_broker.robot
index f1baef0f623..a8172f5593c 100644
--- a/tests/RobotFramework/tests/mqtt/remote_mqtt_broker.robot
+++ b/tests/RobotFramework/tests/mqtt/remote_mqtt_broker.robot
@@ -1,16 +1,19 @@
 *** Settings ***
-Resource    ../../resources/common.resource
-Library    ThinEdgeIO
-Library    Cumulocity
+Resource            ../../resources/common.resource
+Library             ThinEdgeIO
+Library             Cumulocity
+
+Suite Setup         Custom Setup
+Suite Teardown      Get Logs    name=${CONTAINER_1}
+
+Force Tags          theme:mqtt    theme:c8y    adapter:docker
 
-Test Tags    theme:mqtt    theme:c8y    adapter:docker
-Suite Setup    Custom Setup
-Suite Teardown    Get Logs    name=${CONTAINER_1}
 
 *** Variables ***
 ${CONTAINER_1}
 ${CONTAINER_2}
 
+
 *** Test Cases ***
 Check remote mqtt broker #1773
     [Documentation]    The test relies on two containers functioning as one logical device. 1 container (CONTAINER_1)
@@ -50,8 +53,8 @@ Check remote mqtt broker #1773
     Cumulocity.Should Have Services    name=c8y-firmware-plugin    status=up
     Cumulocity.Should Have Services    name=c8y-log-plugin    status=up
 
-*** Keywords ***
 
+*** Keywords ***
 Custom Setup
     # Container 1 running mqtt host and mapper
     ${CONTAINER_1}=    Setup    skip_bootstrap=${True}
@@ -61,7 +64,7 @@ Custom Setup
     Disconnect Mapper    c8y
     Execute Command    sudo tedge config set mqtt.client.host ${CONTAINER_1_IP}
     Execute Command    sudo tedge config set mqtt.client.port 1883
-    Execute Command    sudo tedge config set mqtt.bind_address ${CONTAINER_1_IP}
+    Execute Command    sudo tedge config set mqtt.bind.address ${CONTAINER_1_IP}
     Connect Mapper    c8y
     Restart Service    mqtt-logger
 
@@ -72,21 +75,21 @@ Custom Setup
 
     # Copy files form one device to another (use base64 encoding to prevent quoting issues)
     ${tedge_toml_encoded}=    Execute Command    cat /etc/tedge/tedge.toml | base64
-    ${pem}=    Execute Command    cat "$(tedge config get device.cert.path)"
+    ${pem}=    Execute Command    cat "$(tedge config get device.cert_path)"
 
     # container 2 running all other services
     ${CONTAINER_2}=    Setup    skip_bootstrap=${True}
     Execute Command    ./bootstrap.sh --no-connect --no-bootstrap --no-secure
     Set Suite Variable    $CONTAINER_2
-    
+
     # Stop services that don't need to be running on the second device
-    Stop Service       mosquitto
-    Stop Service       tedge-mapper-c8y
+    Stop Service    mosquitto
+    Stop Service    tedge-mapper-c8y
 
     Execute Command    echo -n "${tedge_toml_encoded}" | base64 --decode | sudo tee /etc/tedge/tedge.toml
     Execute Command    sudo tedge config set mqtt.client.host ${CONTAINER_1_IP}
     Execute Command    sudo tedge config set mqtt.client.port 1883
-    Execute Command    echo "${pem}" | sudo tee "$(tedge config get device.cert.path)"
+    Execute Command    echo "${pem}" | sudo tee "$(tedge config get device.cert_path)"
     Restart Service    c8y-firmware-plugin
     Restart Service    c8y-log-plugin
     Restart Service    c8y-configuration-plugin
diff --git a/tests/RobotFramework/tests/tedge/call_tedge_config_list.robot b/tests/RobotFramework/tests/tedge/call_tedge_config_list.robot
index 6a313707006..37354721cb8 100644
--- a/tests/RobotFramework/tests/tedge/call_tedge_config_list.robot
+++ b/tests/RobotFramework/tests/tedge/call_tedge_config_list.robot
@@ -1,25 +1,26 @@
 *** Settings ***
-Documentation    Purpose of this test is to verify that the tedge config list and tedge config list --all
-...              will result with Return Code 0
-...              Set new device type and return to default value
-...              Set new kay path and return to default value
-...              Set new cert path and return to default value
-...              Set new c8y.root.cert.path and return to default value
-...              Set new c8y.smartrest.templates and return to default value
-...              Set new az.root.cert.path and return to default value
-...              Set new az.mapper.timestamp and return to default value
-...              Set new mqtt.bind_address and return to default value
-...              Set new mqtt.port and return to default value
-...              Set new tmp.path and return to default value
-...              Set new logs.path and return to default value
-...              Set new run.path and return to default value
-
-Resource    ../../resources/common.resource
-Library    ThinEdgeIO
-
-Test Tags    theme:cli    theme:configuration
-Suite Setup            Setup
-Suite Teardown         Get Logs
+Documentation       Purpose of this test is to verify that the tedge config list and tedge config list --all
+...                 will result with Return Code 0
+...                 Set new device type and return to default value
+...                 Set new kay path and return to default value
+...                 Set new cert path and return to default value
+...                 Set new c8y.root_cert_path and return to default value
+...                 Set new c8y.smartrest.templates and return to default value
+...                 Set new az.root_cert_path and return to default value
+...                 Set new az.mapper.timestamp and return to default value
+...                 Set new mqtt.bind.address and return to default value
+...                 Set new mqtt.bind.port and return to default value
+...                 Set new tmp.path and return to default value
+...                 Set new logs.path and return to default value
+...                 Set new run.path and return to default value
+
+Resource            ../../resources/common.resource
+Library             ThinEdgeIO
+
+Suite Setup         Setup
+Suite Teardown      Get Logs
+
+Force Tags          theme:cli    theme:configuration
 
 
 *** Test Cases ***
@@ -31,108 +32,136 @@ tedge config list --all
 
 set/unset device.type
     Execute Command    sudo tedge config set device.type changed-type    #Changing device.type to "changed-type"
-    ${set}     Execute Command    tedge config list
+    ${set}    Execute Command    tedge config list
     Should Contain    ${set}    device.type=changed-type
 
-    Execute Command    sudo tedge config unset device.type    #Undo the change by using the 'unset' command, value returns to default one
-    ${unset}     Execute Command    tedge config list
+    #Undo the change by using the 'unset' command, value returns to default one
+    Execute Command
+    ...    sudo tedge config unset device.type
+    ${unset}    Execute Command    tedge config list
     Should Contain    ${unset}    device.type=thin-edge.io
 
-set/unset device.key.path
-    Execute Command    sudo tedge config set device.key.path /etc/tedge/device-certs1/tedge-private-key.pem    #Changing device.key.path
-    ${set}     Execute Command    tedge config list
-    Should Contain    ${set}    device.key.path=/etc/tedge/device-certs1/tedge-private-key.pem
-
-    Execute Command    sudo tedge config unset device.key.path    #Undo the change by using the 'unset' command, value returns to default one
-    ${unset}     Execute Command    tedge config list
-    Should Contain    ${unset}    device.key.path=/etc/tedge/device-certs/tedge-private-key.pem
-
-set/unset device.cert.path
-    Execute Command    sudo tedge config set device.cert.path /etc/tedge/device-certs1/tedge-certificate.pem   #Changing device.cert.path
-    ${set}     Execute Command    tedge config list
-    Should Contain    ${set}    device.cert.path=/etc/tedge/device-certs1/tedge-certificate.pem
-
-    Execute Command    sudo tedge config unset device.cert.path    #Undo the change by using the 'unset' command, value returns to default one
-    ${unset}     Execute Command    tedge config list
-    Should Contain    ${unset}    device.cert.path=/etc/tedge/device-certs/tedge-certificate.pem
-
-set/unset c8y.root.cert.path
-    Execute Command    sudo tedge config set c8y.root.cert.path /etc/ssl/certs1   #Changing c8y.root.cert.path
-    ${set}     Execute Command    tedge config list
-    Should Contain    ${set}    c8y.root.cert.path=/etc/ssl/certs1
-
-    Execute Command    sudo tedge config unset c8y.root.cert.path    #Undo the change by using the 'unset' command, value returns to default one
-    ${unset}     Execute Command    tedge config list
-    Should Contain    ${unset}    c8y.root.cert.path=/etc/ssl/certs
+set/unset device.key_path
+    #Changing device.key_path
+    Execute Command
+    ...    sudo tedge config set device.key_path /etc/tedge/device-certs1/tedge-private-key.pem
+    ${set}    Execute Command    tedge config list
+    Should Contain    ${set}    device.key_path=/etc/tedge/device-certs1/tedge-private-key.pem
+
+    #Undo the change by using the 'unset' command, value returns to default one
+    Execute Command
+    ...    sudo tedge config unset device.key_path
+    ${unset}    Execute Command    tedge config list
+    Should Contain    ${unset}    device.key_path=/etc/tedge/device-certs/tedge-private-key.pem
+
+set/unset device.cert_path
+    #Changing device.cert_path
+    Execute Command
+    ...    sudo tedge config set device.cert_path /etc/tedge/device-certs1/tedge-certificate.pem
+    ${set}    Execute Command    tedge config list
+    Should Contain    ${set}    device.cert_path=/etc/tedge/device-certs1/tedge-certificate.pem
+
+    #Undo the change by using the 'unset' command, value returns to default one
+    Execute Command
+    ...    sudo tedge config unset device.cert_path
+    ${unset}    Execute Command    tedge config list
+    Should Contain    ${unset}    device.cert_path=/etc/tedge/device-certs/tedge-certificate.pem
+
+set/unset c8y.root_cert_path
+    Execute Command    sudo tedge config set c8y.root_cert_path /etc/ssl/certs1    #Changing c8y.root_cert_path
+    ${set}    Execute Command    tedge config list
+    Should Contain    ${set}    c8y.root_cert_path=/etc/ssl/certs1
+
+    #Undo the change by using the 'unset' command, value returns to default one
+    Execute Command
+    ...    sudo tedge config unset c8y.root_cert_path
+    ${unset}    Execute Command    tedge config list
+    Should Contain    ${unset}    c8y.root_cert_path=/etc/ssl/certs
 
 set/unset c8y.smartrest.templates
-    Execute Command    sudo tedge config set c8y.smartrest.templates 1   #Changing c8y.smartrest.templates
-    ${set}     Execute Command    tedge config list
+    Execute Command    sudo tedge config set c8y.smartrest.templates 1    #Changing c8y.smartrest.templates
+    ${set}    Execute Command    tedge config list
     Should Contain    ${set}    c8y.smartrest.templates=["1"]
 
-    Execute Command    sudo tedge config unset c8y.smartrest.templates    #Undo the change by using the 'unset' command, value returns to default one
-    ${unset}     Execute Command    tedge config list
+    #Undo the change by using the 'unset' command, value returns to default one
+    Execute Command
+    ...    sudo tedge config unset c8y.smartrest.templates
+    ${unset}    Execute Command    tedge config list
     Should Contain    ${unset}    c8y.smartrest.templates=[]
 
-set/unset az.root.cert.path
-    Execute Command    sudo tedge config set az.root.cert.path /etc/ssl/certs1   #Changing az.root.cert.path
-    ${set}     Execute Command    tedge config list
-    Should Contain    ${set}    az.root.cert.path=/etc/ssl/certs1
+set/unset az.root_cert_path
+    Execute Command    sudo tedge config set az.root_cert_path /etc/ssl/certs1    #Changing az.root_cert_path
+    ${set}    Execute Command    tedge config list
+    Should Contain    ${set}    az.root_cert_path=/etc/ssl/certs1
 
-    Execute Command    sudo tedge config unset az.root.cert.path    #Undo the change by using the 'unset' command, value returns to default one
-    ${unset}     Execute Command    tedge config list
-    Should Contain    ${unset}    az.root.cert.path=/etc/ssl/certs
+    #Undo the change by using the 'unset' command, value returns to default one
+    Execute Command
+    ...    sudo tedge config unset az.root_cert_path
+    ${unset}    Execute Command    tedge config list
+    Should Contain    ${unset}    az.root_cert_path=/etc/ssl/certs
 
 set/unset az.mapper.timestamp
-    Execute Command    sudo tedge config set az.mapper.timestamp false  #Changing az.mapper.timestamp
-    ${set}     Execute Command    tedge config list
+    Execute Command    sudo tedge config set az.mapper.timestamp false    #Changing az.mapper.timestamp
+    ${set}    Execute Command    tedge config list
     Should Contain    ${set}    az.mapper.timestamp=false
 
-    Execute Command    sudo tedge config unset az.mapper.timestamp    #Undo the change by using the 'unset' command, value returns to default one
-    ${unset}     Execute Command    tedge config list
+    #Undo the change by using the 'unset' command, value returns to default one
+    Execute Command
+    ...    sudo tedge config unset az.mapper.timestamp
+    ${unset}    Execute Command    tedge config list
     Should Contain    ${unset}    az.mapper.timestamp=true
 
-set/unset mqtt.bind_address
-    Execute Command    sudo tedge config set mqtt.bind_address 127.1.1.1  #Changing mqtt.bind_address
-    ${set}     Execute Command    tedge config list
-    Should Contain    ${set}    mqtt.bind_address=127.1.1.1
+set/unset mqtt.bind.address
+    Execute Command    sudo tedge config set mqtt.bind.address 127.1.1.1    #Changing mqtt.bind.address
+    ${set}    Execute Command    tedge config list
+    Should Contain    ${set}    mqtt.bind.address=127.1.1.1
 
-    Execute Command    sudo tedge config unset mqtt.bind_address    #Undo the change by using the 'unset' command, value returns to default one
-    ${unset}     Execute Command    tedge config list
-    Should Contain    ${unset}    mqtt.bind_address=127.0.0.1
+    #Undo the change by using the 'unset' command, value returns to default one
+    Execute Command
+    ...    sudo tedge config unset mqtt.bind.address
+    ${unset}    Execute Command    tedge config list
+    Should Contain    ${unset}    mqtt.bind.address=127.0.0.1
 
-set/unset mqtt.port
-    Execute Command    sudo tedge config set mqtt.port 8888  #Changing mqtt.port
-    ${set}     Execute Command    tedge config list
-    Should Contain    ${set}    mqtt.port=8888
+set/unset mqtt.bind.port
+    Execute Command    sudo tedge config set mqtt.bind.port 8888    #Changing mqtt.bind.port
+    ${set}    Execute Command    tedge config list
+    Should Contain    ${set}    mqtt.bind.port=8888
 
-    Execute Command    sudo tedge config unset mqtt.port    #Undo the change by using the 'unset' command, value returns to default one
-    ${unset}     Execute Command    tedge config list
-    Should Contain    ${unset}    mqtt.port=1883
+    #Undo the change by using the 'unset' command, value returns to default one
+    Execute Command
+    ...    sudo tedge config unset mqtt.bind.port
+    ${unset}    Execute Command    tedge config list
+    Should Contain    ${unset}    mqtt.bind.port=1883
 
 set/unset tmp.path
-    Execute Command    sudo tedge config set tmp.path /tmp1  #Changing tmp.path
-    ${set}     Execute Command    tedge config list
+    Execute Command    sudo tedge config set tmp.path /tmp1    #Changing tmp.path
+    ${set}    Execute Command    tedge config list
     Should Contain    ${set}    tmp.path=/tmp1
 
-    Execute Command    sudo tedge config unset tmp.path    #Undo the change by using the 'unset' command, value returns to default one
-    ${unset}     Execute Command    tedge config list
+    #Undo the change by using the 'unset' command, value returns to default one
+    Execute Command
+    ...    sudo tedge config unset tmp.path
+    ${unset}    Execute Command    tedge config list
     Should Contain    ${unset}    tmp.path=/tmp
 
 set/unset logs.path
-    Execute Command    sudo tedge config set logs.path /var/log1  #Changing logs.path
-    ${set}     Execute Command    tedge config list
+    Execute Command    sudo tedge config set logs.path /var/log1    #Changing logs.path
+    ${set}    Execute Command    tedge config list
     Should Contain    ${set}    logs.path=/var/log1
 
-    Execute Command    sudo tedge config unset logs.path    #Undo the change by using the 'unset' command, value returns to default one
-    ${unset}     Execute Command    tedge config list
+    #Undo the change by using the 'unset' command, value returns to default one
+    Execute Command
+    ...    sudo tedge config unset logs.path
+    ${unset}    Execute Command    tedge config list
     Should Contain    ${unset}    logs.path=/var/log
 
 set/unset run.path
-    Execute Command    sudo tedge config set run.path /run1  #Changing run.path
-    ${set}     Execute Command    tedge config list
+    Execute Command    sudo tedge config set run.path /run1    #Changing run.path
+    ${set}    Execute Command    tedge config list
     Should Contain    ${set}    run.path=/run1
 
-    Execute Command    sudo tedge config unset run.path    #Undo the change by using the 'unset' command, value returns to default one
-    ${unset}     Execute Command    tedge config list
+    #Undo the change by using the 'unset' command, value returns to default one
+    Execute Command
+    ...    sudo tedge config unset run.path
+    ${unset}    Execute Command    tedge config list
     Should Contain    ${unset}    run.path=/run
diff --git a/tests/RobotFramework/tests/tedge/http_file_transfer_api.robot b/tests/RobotFramework/tests/tedge/http_file_transfer_api.robot
index d0545111cf5..064fa165d71 100644
--- a/tests/RobotFramework/tests/tedge/http_file_transfer_api.robot
+++ b/tests/RobotFramework/tests/tedge/http_file_transfer_api.robot
@@ -1,23 +1,26 @@
+*** Comments ***
 #Command to execute:    robot -d \results --timestampoutputs --log http_file_transfer_api.html --report NONE -v BUILD:840 -v HOST:192.168.1.130 thin-edge.io/tests/RobotFramework/tedge/http_file_transfer_api.robot
+
+
 *** Settings ***
+Resource            ../../resources/common.resource
+Library             ThinEdgeIO
+
+Suite Setup         Custom Setup
+Suite Teardown      Custom Teardown
 
-Resource    ../../resources/common.resource
-Library    ThinEdgeIO
+Force Tags          theme:cli    theme:configuration    theme:childdevices
 
-Test Tags    theme:cli    theme:configuration    theme:childdevices
-Suite Setup            Custom Setup
-Suite Teardown         Custom Teardown
 
 *** Variables ***
+${DEVICE_SN}    # Parent device serial number
+${DEVICE_IP}    # Parent device host name which is reachable
+${PORT}=        8000
 
-${DEVICE_SN}        # Parent device serial number
-${DEVICE_IP}        # Parent device host name which is reachable
-${PORT}=    8000
 
 *** Test Cases ***
-
 Get Put Delete
-    Setup    skip_bootstrap=True        # Setup child device
+    Setup    skip_bootstrap=True    # Setup child device
 
     Execute Command    curl -X PUT -d "test of put" http://${DEVICE_IP}:${PORT}/tedge/file-transfer/file_a
     ${get}=    Execute Command    curl --silent http://${DEVICE_IP}:${PORT}/tedge/file-transfer/file_a
@@ -26,7 +29,6 @@ Get Put Delete
 
 
 *** Keywords ***
-
 Custom Setup
     ${DEVICE_SN}=    Setup    skip_bootstrap=False
     Set Suite Variable    $DEVICE_SN    ${DEVICE_SN}
@@ -34,8 +36,8 @@ Custom Setup
     ${DEVICE_IP}=    Get IP Address
     Set Suite Variable    ${DEVICE_IP}
 
-    Execute Command    sudo tedge config set mqtt.external.bind_address ${DEVICE_IP}
-    ${bind}=    Execute Command    tedge config get mqtt.external.bind_address    strip=True
+    Execute Command    sudo tedge config set mqtt.external.bind.address ${DEVICE_IP}
+    ${bind}=    Execute Command    tedge config get mqtt.external.bind.address    strip=True
     Should Be Equal    ${bind}    ${DEVICE_IP}
     Execute Command    sudo -u tedge mkdir -p /var/tedge
     Restart Service    tedge-agent
diff --git a/tests/RobotFramework/tests/tedge/tedge_config_get.robot b/tests/RobotFramework/tests/tedge/tedge_config_get.robot
index ea63b629fda..580cc13d88f 100644
--- a/tests/RobotFramework/tests/tedge/tedge_config_get.robot
+++ b/tests/RobotFramework/tests/tedge/tedge_config_get.robot
@@ -1,10 +1,11 @@
 *** Settings ***
-Resource    ../../resources/common.resource
-Library    ThinEdgeIO
+Resource            ../../resources/common.resource
+Library             ThinEdgeIO
 
-Test Tags    theme:cli    theme:configuration
-Suite Setup            Custom Setup
-Suite Teardown         Get Logs
+Suite Setup         Custom Setup
+Suite Teardown      Get Logs
+
+Force Tags          theme:cli    theme:configuration
 
 
 *** Test Cases ***
@@ -26,22 +27,25 @@ Invalid keys should not return anything on stdout and warnings on stderr
     ${stderr}=    Execute Command    tedge config get does.not.exist 2>&1 >/dev/null    exp_exit_code=!0
     Should Not Be Empty    ${stderr}
 
-
 Set configuration via environment variables
     [Template]    Check known tedge environment settings
-    TEDGE_AZ_URL                az.url                   az.example.com
-    TEDGE_C8Y_URL               c8y.url                  example.com
-    TEDGE_DEVICE_KEY_PATH       device.key.path          /etc/example.key
-    TEDGE_DEVICE_CERT_PATH      device.cert.path         /etc/example.pem
-    TEDGE_MQTT_BIND_ADDRESS     mqtt.bind_address        0.0.0.1
-    TEDGE_MQTT_CLIENT_HOST      mqtt.client.host         custom_host_name
-    TEDGE_MQTT_CLIENT_PORT      mqtt.client.port         8888
-
+    TEDGE_AZ_URL    az.url    az.example.com
+    TEDGE_C8Y_URL    c8y.url    example.com
+    TEDGE_DEVICE_KEY_PATH    device.key_path    /etc/example.key
+    TEDGE_DEVICE_CERT_PATH    device.cert_path    /etc/example.pem
+    TEDGE_MQTT_BIND_ADDRESS    mqtt.bind.address    0.0.0.1
+    TEDGE_MQTT_CLIENT_HOST    mqtt.client.host    custom_host_name
+    TEDGE_MQTT_CLIENT_PORT    mqtt.client.port    8888
 
 Set unknown configuration via environment variables
-    ${stdout}    ${stderr}=    Execute Command    env TEDGE_C8Y_UNKNOWN_CONFIGURATION\=dummy TEDGE_C8Y_URL\=example.com tedge config get c8y.url    stdout=${True}    stderr=${True}
+    ${stdout}    ${stderr}=    Execute Command
+    ...    env TEDGE_C8Y_UNKNOWN_CONFIGURATION\=dummy TEDGE_C8Y_URL\=example.com tedge config get c8y.url
+    ...    stdout=${True}
+    ...    stderr=${True}
     Should Be Equal    ${stdout}    example.com\n
-    Should Contain    ${stderr}    Unknown configuration field "c8y.unknown_configuration" from environment variable TEDGE_C8Y_UNKNOWN_CONFIGURATION
+    Should Contain
+    ...    ${stderr}
+    ...    Unknown configuration field "c8y_unknown_configuration" from environment variable TEDGE_C8Y_UNKNOWN_CONFIGURATION
 
 
 *** Keywords ***
diff --git a/tests/images/debian-systemd/files/bootstrap.sh b/tests/images/debian-systemd/files/bootstrap.sh
index 55ad43a591b..69db0961ed3 100755
--- a/tests/images/debian-systemd/files/bootstrap.sh
+++ b/tests/images/debian-systemd/files/bootstrap.sh
@@ -716,9 +716,9 @@ EOF
     chown tedge:tedge /setup/client.*
 
     tedge config set mqtt.client.port 8883
-    tedge config set mqtt.client.auth.cafile /etc/mosquitto/ca_certificates/ca.crt
-    tedge config set mqtt.client.auth.certfile /setup/client.crt
-    tedge config set mqtt.client.auth.keyfile /setup/client.key
+    tedge config set mqtt.client.auth.ca_file /etc/mosquitto/ca_certificates/ca.crt
+    tedge config set mqtt.client.auth.cert_file /setup/client.crt
+    tedge config set mqtt.client.auth.key_file /setup/client.key
 
     systemctl restart mosquitto
 }