diff --git a/book/src/developers/designs/application_passwords.md b/book/src/developers/designs/application_passwords.md
index 01a382a5e..483683b00 100644
--- a/book/src/developers/designs/application_passwords.md
+++ b/book/src/developers/designs/application_passwords.md
@@ -22,8 +22,8 @@ based authentication and remove the ability to bind with the UNIX password.
 # User experience
 
 The administrator configures two applications on their Kanidm instance. One is "mail" for a generic
-SMTP+IMAP service. The other is HTTP basic auth to a legacy web server. Then the administrator
-configures which users will be able to use application passwords for each application.
+SMTP+IMAP service. The other is HTTP basic auth to a legacy web server. Applications have a linked
+group to determine which users will be able to use application passwords for each application.
 
 The mail services and web services are configured to point to Kanidm's LDAP gateway with a
 customized search base DN.
@@ -79,10 +79,10 @@ A new class for applications will be added. Each application will have a single
 only members of this group will be able to bind with the application password for the associated
 application.
 
-Creating a new application will not create an associated group automatically. It will be the
-administrator who will configure the association after creating the application and optionally a new
-group. It will be possible to associate `idm_all_persons` to an application. Removing an application
-will not delete the associated group nor its members.
+Creating a new application will not create an associated group automatically, an existing group
+must be provided. It will be possible to associate `idm_all_persons` to an application. Removing
+an application will not delete the associated group nor its members. It will be possible to change
+the linked group after creation.
 
 When users are removed from a group associated to an application all of their application passwords
 for the application will be disabled.
@@ -150,6 +150,9 @@ We do not need temporary locks or holds - users can delete and recreate as neede
 Since application passwords are related to applications, on delete of an application all entries
 that have a bound application password should be removed from user accounts.
 
+Trying to delete a group linked to an application will raise an error showing the user that
+something still requires it.
+
 ### Access controls
 
 The "Application administrators" group will manage the applications, and applications will allow
diff --git a/libs/client/src/lib.rs b/libs/client/src/lib.rs
index 3e3e6cff2..56e36b8c5 100644
--- a/libs/client/src/lib.rs
+++ b/libs/client/src/lib.rs
@@ -691,12 +691,10 @@ impl KanidmClient {
         }
 
         #[cfg(any(test, debug_assertions))]
-        if !matching {
-            if !std::env::var("KANIDM_DEV_YOLO").is_ok() {
-                eprintln!("⚠️  You're in debug/dev mode, so we're going to quit here.");
-                eprintln!("If you really must do this, set KANIDM_DEV_YOLO=1");
-                std::process::exit(1);
-            }
+        if !matching && std::env::var("KANIDM_DEV_YOLO").is_err() {
+            eprintln!("⚠️  You're in debug/dev mode, so we're going to quit here.");
+            eprintln!("If you really must do this, set KANIDM_DEV_YOLO=1");
+            std::process::exit(1);
         }
 
         // Check is done once, mark as no longer needing to occur
diff --git a/libs/crypto/src/lib.rs b/libs/crypto/src/lib.rs
index bf5605480..103702a12 100644
--- a/libs/crypto/src/lib.rs
+++ b/libs/crypto/src/lib.rs
@@ -108,7 +108,7 @@ impl Into<OperationError> for CryptoError {
     }
 }
 
-#[derive(Serialize, Deserialize)]
+#[derive(Serialize, Deserialize, PartialEq, Eq, Clone)]
 #[allow(non_camel_case_types)]
 pub enum DbPasswordV1 {
     TPM_ARGON2ID {
diff --git a/proto/src/constants.rs b/proto/src/constants.rs
index a43fa2ab1..9794ca693 100644
--- a/proto/src/constants.rs
+++ b/proto/src/constants.rs
@@ -67,6 +67,7 @@ pub const ATTR_ACP_RECEIVER: &str = "acp_receiver";
 pub const ATTR_ACP_SEARCH_ATTR: &str = "acp_search_attr";
 pub const ATTR_ACP_TARGET_SCOPE: &str = "acp_targetscope";
 pub const ATTR_API_TOKEN_SESSION: &str = "api_token_session";
+pub const ATTR_APPLICATION_PASSWORD: &str = "application_password";
 pub const ATTR_ATTESTED_PASSKEYS: &str = "attested_passkeys";
 pub const ATTR_ATTR: &str = "attr";
 pub const ATTR_ATTRIBUTENAME: &str = "attributename";
@@ -129,6 +130,7 @@ pub const ATTR_KEY_PROVIDER: &str = "key_provider";
 pub const ATTR_LAST_MODIFIED_CID: &str = "last_modified_cid";
 pub const ATTR_LDAP_ALLOW_UNIX_PW_BIND: &str = "ldap_allow_unix_pw_bind";
 pub const ATTR_LEGALNAME: &str = "legalname";
+pub const ATTR_LINKEDGROUP: &str = "linked_group";
 pub const ATTR_LOGINSHELL: &str = "loginshell";
 pub const ATTR_MAIL: &str = "mail";
 pub const ATTR_MAY: &str = "may";
diff --git a/proto/src/internal/error.rs b/proto/src/internal/error.rs
index e28b06e7f..d951f9bc8 100644
--- a/proto/src/internal/error.rs
+++ b/proto/src/internal/error.rs
@@ -147,9 +147,13 @@ pub enum OperationError {
     // Value Errors
     VL0001ValueSshPublicKeyString,
 
+    // LDAP Errors
+    LD0001AnonymousNotAllowed,
+
     // DB low level errors.
     DB0001MismatchedRestoreVersion,
     DB0002MismatchedRestoreVersion,
+    DB0003FilterResolveCacheBuild,
 
     // SCIM
     SC0001IncomingSshPublicKey,
@@ -211,6 +215,10 @@ pub enum OperationError {
 
     // Plugins
     PL0001GidOverlapsSystemRange,
+
+    // Web UI
+    UI0001ChallengeSerialisation,
+    UI0002InvalidState,
 }
 
 impl PartialEq for OperationError {
@@ -307,12 +315,14 @@ impl OperationError {
             Self::VS0004CertificatePublicKeyDigest |
             Self::VS0005CertificatePublicKeyDigest => Some("The certificates public key is unabled to be digested."),
             Self::VL0001ValueSshPublicKeyString => None,
+            Self::LD0001AnonymousNotAllowed => Some("Anonymous is not allowed to access LDAP with this method."),
             Self::SC0001IncomingSshPublicKey => None,
             Self::MG0001InvalidReMigrationLevel => None,
             Self::MG0002RaiseDomainLevelExceedsMaximum => None,
             Self::MG0003ServerPhaseInvalidForMigration => None,
             Self::DB0001MismatchedRestoreVersion => None,
             Self::DB0002MismatchedRestoreVersion => None,
+            Self::DB0003FilterResolveCacheBuild => None,
             Self::MG0004DomainLevelInDevelopment => None,
             Self::MG0005GidConstraintsNotMet => None,
             Self::MG0006SKConstraintsNotMet => Some("Migration Constraints Not Met - Security Keys should not be present."),
@@ -364,6 +374,8 @@ impl OperationError {
             Self::KP0043KeyObjectJweA128GCMEncryption => None,
             Self::KP0044KeyObjectJwsPublicJwk => None,
             Self::PL0001GidOverlapsSystemRange => None,
+            Self::UI0001ChallengeSerialisation => Some("The WebAuthn challenge was unable to be serialised."),
+            Self::UI0002InvalidState => Some("The credential update process returned an invalid state transition."),
         }
     }
 }
diff --git a/server/core/src/https/views/login.rs b/server/core/src/https/views/login.rs
index 555e05207..0e060c8d8 100644
--- a/server/core/src/https/views/login.rs
+++ b/server/core/src/https/views/login.rs
@@ -207,7 +207,7 @@ pub async fn view_reauth_get(
         .into_response(),
     };
 
-    return Ok(res);
+    Ok(res)
 }
 
 pub async fn view_index_get(
diff --git a/server/core/src/https/views/oauth2.rs b/server/core/src/https/views/oauth2.rs
index f62bf50f1..86511a74b 100644
--- a/server/core/src/https/views/oauth2.rs
+++ b/server/core/src/https/views/oauth2.rs
@@ -141,13 +141,11 @@ async fn oauth2_auth_req(
 
             match maybe_jar {
                 Ok(jar) => (jar, Redirect::to("/ui/login")).into_response(),
-                Err(err_code) => {
-                    return HtmlTemplate(UnrecoverableErrorView {
-                        err_code,
-                        operation_id: kopid.eventid,
-                    })
-                    .into_response();
-                }
+                Err(err_code) => HtmlTemplate(UnrecoverableErrorView {
+                    err_code,
+                    operation_id: kopid.eventid,
+                })
+                .into_response(),
             }
         }
         Err(Oauth2Error::AccessDenied) => {
diff --git a/server/core/src/https/views/reset.rs b/server/core/src/https/views/reset.rs
index 2791b72c5..7f9573f1d 100644
--- a/server/core/src/https/views/reset.rs
+++ b/server/core/src/https/views/reset.rs
@@ -27,9 +27,10 @@ use kanidm_proto::internal::{
 use crate::https::extractors::VerifiedClientInformation;
 use crate::https::middleware::KOpId;
 use crate::https::views::errors::HtmxError;
-use crate::https::views::HtmlTemplate;
 use crate::https::ServerState;
 
+use super::{HtmlTemplate, UnrecoverableErrorView};
+
 #[derive(Template)]
 #[template(path = "credentials_reset_form.html")]
 struct ResetCredFormView {
@@ -66,7 +67,7 @@ pub(crate) struct ResetTokenParam {
 }
 
 #[derive(Template)]
-#[template(path = "cred_update/add_password_partial.html")]
+#[template(path = "credential_update_add_password_partial.html")]
 struct AddPasswordPartial {
     check_res: PwdCheckResult,
 }
@@ -97,7 +98,7 @@ pub(crate) struct NewTotp {
 }
 
 #[derive(Template)]
-#[template(path = "cred_update/add_passkey_partial.html")]
+#[template(path = "credential_update_add_passkey_partial.html")]
 struct AddPasskeyPartial {
     // Passkey challenge for adding a new passkey
     challenge: String,
@@ -169,7 +170,7 @@ impl Display for TotpFeedback {
 }
 
 #[derive(Template)]
-#[template(path = "cred_update/add_totp_partial.html")]
+#[template(path = "credential_update_add_totp_partial.html")]
 struct AddTotpPartial {
     check_res: TotpCheckResult,
 }
@@ -367,17 +368,25 @@ pub(crate) async fn view_new_passkey(
 
     let response = match cu_status.mfaregstate {
         CURegState::Passkey(chal) | CURegState::AttestedPasskey(chal) => {
-            HtmlTemplate(AddPasskeyPartial {
-                challenge: serde_json::to_string(&chal).unwrap(),
-                class: init_form.class,
-            })
-            .into_response()
+            if let Ok(challenge) = serde_json::to_string(&chal) {
+                HtmlTemplate(AddPasskeyPartial {
+                    challenge,
+                    class: init_form.class,
+                })
+                .into_response()
+            } else {
+                HtmlTemplate(UnrecoverableErrorView {
+                    err_code: OperationError::UI0001ChallengeSerialisation,
+                    operation_id: kopid.eventid,
+                })
+                .into_response()
+            }
         }
-        _ => (
-            StatusCode::INTERNAL_SERVER_ERROR,
-            HtmxError::new(&kopid, OperationError::Backend).into_response(),
-        )
-            .into_response(),
+        _ => HtmlTemplate(UnrecoverableErrorView {
+            err_code: OperationError::UI0002InvalidState,
+            operation_id: kopid.eventid,
+        })
+        .into_response(),
     };
 
     let passkey_init_trigger =
@@ -657,7 +666,7 @@ fn get_cu_partial(cu_status: CUStatus) -> CredResetPartialView {
         ..
     } = cu_status;
 
-    return CredResetPartialView {
+    CredResetPartialView {
         ext_cred_portal,
         warnings,
         attested_passkeys_state,
@@ -666,19 +675,19 @@ fn get_cu_partial(cu_status: CUStatus) -> CredResetPartialView {
         passkeys,
         primary_state,
         primary,
-    };
+    }
 }
 
 fn get_cu_partial_response(cu_status: CUStatus) -> Response {
     let credentials_update_partial = get_cu_partial(cu_status);
-    return (
+    (
         HxPushUrl(Uri::from_static("/ui/reset")),
         HxRetarget("#credentialUpdateDynamicSection".to_string()),
         HxReselect("#credentialUpdateDynamicSection".to_string()),
         HxReswap(SwapOption::OuterHtml),
         HtmlTemplate(credentials_update_partial),
     )
-        .into_response();
+        .into_response()
 }
 
 fn get_cu_response(domain: String, cu_status: CUStatus) -> Response {
@@ -710,34 +719,3 @@ async fn get_cu_session(jar: CookieJar) -> Result<CUSessionToken, Response> {
         Err((StatusCode::FORBIDDEN, Redirect::to("/ui/reset")).into_response())
     };
 }
-
-// Any filter defined in the module `filters` is accessible in your template.
-mod filters {
-    pub fn blank_if<T: std::fmt::Display>(
-        implicit_arg: T,
-        condition: bool,
-    ) -> ::askama::Result<String> {
-        blank_iff(implicit_arg, &condition)
-    }
-    pub fn ternary<T: std::fmt::Display, F: std::fmt::Display>(
-        implicit_arg: &bool,
-        true_case: T,
-        false_case: F,
-    ) -> ::askama::Result<String> {
-        if *implicit_arg {
-            Ok(format!("{true_case}"))
-        } else {
-            Ok(format!("{false_case}"))
-        }
-    }
-    pub fn blank_iff<T: std::fmt::Display>(
-        implicit_arg: T,
-        condition: &bool,
-    ) -> ::askama::Result<String> {
-        return if *condition {
-            Ok("".into())
-        } else {
-            Ok(format!("{implicit_arg}"))
-        };
-    }
-}
diff --git a/server/core/templates/cred_update/add_passkey_partial.html b/server/core/templates/credential_update_add_passkey_partial.html
similarity index 99%
rename from server/core/templates/cred_update/add_passkey_partial.html
rename to server/core/templates/credential_update_add_passkey_partial.html
index a6f46286b..90aac6309 100644
--- a/server/core/templates/cred_update/add_passkey_partial.html
+++ b/server/core/templates/credential_update_add_passkey_partial.html
@@ -24,4 +24,4 @@
             </div>
         </form>
     </div>
-</div>
\ No newline at end of file
+</div>
diff --git a/server/core/templates/cred_update/add_password_partial.html b/server/core/templates/credential_update_add_password_partial.html
similarity index 90%
rename from server/core/templates/cred_update/add_password_partial.html
rename to server/core/templates/credential_update_add_password_partial.html
index 8727ac389..8f900b9c2 100644
--- a/server/core/templates/cred_update/add_password_partial.html
+++ b/server/core/templates/credential_update_add_password_partial.html
@@ -7,8 +7,12 @@
 
         (% if let PwdCheckResult::Failure with { pwd_equal, warnings } = check_res %)
             (% let pwd_equal = pwd_equal.clone() %)
-            (% let potentially_invalid_input_class = "is-invalid"|blank_if(warnings.len() == 0) %)
-            (% let potentially_invalid_reinput_class = "is-invalid"|blank_iff(pwd_equal) %)
+            (% if !warnings.is_empty() %)
+                (% let potentially_invalid_input_class = "is-invalid" %)
+            (% endif %)
+            (% if pwd_equal %)
+                (% let potentially_invalid_reinput_class = "is-invalid" %)
+            (% endif %)
         (% endif %)
 
         <label for="new-password" class="form-label">Enter New Password</label>
diff --git a/server/core/templates/cred_update/add_totp_partial.html b/server/core/templates/credential_update_add_totp_partial.html
similarity index 93%
rename from server/core/templates/cred_update/add_totp_partial.html
rename to server/core/templates/credential_update_add_totp_partial.html
index 05dd0e286..4585c74a9 100644
--- a/server/core/templates/cred_update/add_totp_partial.html
+++ b/server/core/templates/credential_update_add_totp_partial.html
@@ -23,8 +23,12 @@
         (% if let TotpCheckResult::Failure with { wrong_code, broken_app, warnings } = check_res %)
             (% let wrong_code = wrong_code.clone() %)
             (% let broken_app = broken_app.clone() %)
-            (% let potentially_invalid_name_class = "is-invalid"|blank_if(warnings.len() == 0) %)
-            (% let potentially_invalid_check_class = "is-invalid"|blank_iff(wrong_code) %)
+            (% if !warnings.is_empty() %)
+                (% let potentially_invalid_name_class = "is-invalid" %)
+            (% endif %)
+            (% if wrong_code %)
+                (% let potentially_invalid_check_class = "is-invalid" %)
+            (% endif %)
         (% endif %)
 
         <label for="new-totp-name" class="form-label">Enter a name for your TOTP</label>
diff --git a/server/core/templates/credentials_reset_form.html b/server/core/templates/credentials_reset_form.html
index 9080ced3c..c47c2f3a7 100644
--- a/server/core/templates/credentials_reset_form.html
+++ b/server/core/templates/credentials_reset_form.html
@@ -23,7 +23,11 @@
                     name="token"
                     autofocus
                     aria-describedby="unknown-reset-token-validation-feedback"
-                    class='form-control (( "is-invalid"|blank_iff(!wrong_code) ))'
+                    (% if wrong_code %)
+                    class='form-control is-invalid'
+                    (% else %)
+                    class='form-control'
+                    (% endif %)
             >
         (% if wrong_code %)
             <div id="unknown-reset-token-validation-feedback" class="invalid-feedback">
@@ -45,4 +49,4 @@
         </button>
     </p>
 </main>
-(% endblock %)
\ No newline at end of file
+(% endblock %)
diff --git a/server/core/templates/credentials_update_partial.html b/server/core/templates/credentials_update_partial.html
index 4fdda6db0..1f13b6ec2 100644
--- a/server/core/templates/credentials_update_partial.html
+++ b/server/core/templates/credentials_update_partial.html
@@ -15,8 +15,12 @@
             <hr class="my-4" >
             (% for warning in warnings %)
                 (% let is_danger = [CURegWarning::WebauthnAttestationUnsatisfiable, CURegWarning::Unsatisfiable].contains(warning) %)
+                (% if is_danger %)
+                    <div class='alert alert-danger' role="alert">
+                (% else %)
+                    <div class='alert alert-warning' role="alert">
+                (% endif %)
 
-                <div class='alert alert-(( is_danger|ternary("danger", "warning") ))' role="alert">
                 (% match warning %)
                     (% when CURegWarning::MfaRequired %)
                         Multi-Factor Authentication is required for your account. Either add TOTP or remove your password in favour of passkeys to submit.
@@ -97,7 +101,9 @@
                         type="submit"
                         hx-post="/ui/api/cu_commit"
                         hx-boost="false"
-                        (( "disabled"|blank_if(warnings.len() == 0) ))
+                        (% if !warnings.is_empty() %)
+                        disabled
+                        (% endif %)
                 >Submit Changes</button>
             </span>
         </div>
diff --git a/server/daemon/run_insecure_dev_server.sh b/server/daemon/run_insecure_dev_server.sh
index 38b6c57ad..83ee6c74b 100755
--- a/server/daemon/run_insecure_dev_server.sh
+++ b/server/daemon/run_insecure_dev_server.sh
@@ -2,6 +2,8 @@
 
 set -e
 
+SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
+
 # This script based on the developer readme and allows you to run a test server.
 
 if [ -z "$KANI_CARGO_OPTS" ]; then
@@ -20,14 +22,14 @@ fi
 
 mkdir -p "${KANI_TMP}"/client_ca
 
-CONFIG_FILE=${CONFIG_FILE:="../../examples/insecure_server.toml"}
+CONFIG_FILE=${CONFIG_FILE:="${SCRIPT_DIR}/../../examples/insecure_server.toml"}
 
 if [ ! -f "${CONFIG_FILE}" ]; then
-    SCRIPT_DIR="$(dirname -a "$0")"
     echo "Couldn't find configuration file at ${CONFIG_FILE}, please ensure you're running this script from its base directory (${SCRIPT_DIR})."
     exit 1
 fi
 
+pushd "${SCRIPT_DIR}" > /dev/null 2>&1
 if [ -n "${1}" ]; then
     COMMAND=$*
     #shellcheck disable=SC2086
@@ -38,4 +40,4 @@ else
     #shellcheck disable=SC2086
     cargo run ${KANI_CARGO_OPTS} --bin kanidmd -- server -c "${CONFIG_FILE}"
 fi
-
+popd > /dev/null 2>&1
diff --git a/server/lib/src/be/dbvalue.rs b/server/lib/src/be/dbvalue.rs
index 7865ae0f3..0c136be20 100644
--- a/server/lib/src/be/dbvalue.rs
+++ b/server/lib/src/be/dbvalue.rs
@@ -337,16 +337,6 @@ pub struct DbValueCredV1 {
     pub data: DbCred,
 }
 
-#[derive(Serialize, Deserialize, Debug)]
-pub enum DbApiToken {
-    V1 {
-        #[serde(rename = "u")]
-        uuid: Uuid,
-        #[serde(rename = "s")]
-        secret: DbPasswordV1,
-    },
-}
-
 #[derive(Serialize, Deserialize, Debug)]
 pub enum DbValuePasskeyV1 {
     V4 { u: Uuid, t: String, k: PasskeyV4 },
@@ -663,6 +653,20 @@ pub enum DbValueCertificate {
     V1 { certificate_der: Vec<u8> },
 }
 
+#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
+pub enum DbValueApplicationPassword {
+    V1 {
+        #[serde(rename = "u")]
+        refer: Uuid,
+        #[serde(rename = "a")]
+        application_refer: Uuid,
+        #[serde(rename = "l")]
+        label: String,
+        #[serde(rename = "p")]
+        password: DbPasswordV1,
+    },
+}
+
 #[derive(Serialize, Deserialize, Debug)]
 pub enum DbValueV1 {
     #[serde(rename = "U8")]
@@ -822,6 +826,8 @@ pub enum DbValueSetV2 {
     HexString(Vec<String>),
     #[serde(rename = "X509")]
     Certificate(Vec<DbValueCertificate>),
+    #[serde(rename = "AP")]
+    ApplicationPassword(Vec<DbValueApplicationPassword>),
 }
 
 impl DbValueSetV2 {
@@ -874,6 +880,7 @@ impl DbValueSetV2 {
             DbValueSetV2::WebauthnAttestationCaList { ca_list } => ca_list.len(),
             DbValueSetV2::KeyInternal(set) => set.len(),
             DbValueSetV2::Certificate(set) => set.len(),
+            DbValueSetV2::ApplicationPassword(set) => set.len(),
         }
     }
 
diff --git a/server/lib/src/constants/acp.rs b/server/lib/src/constants/acp.rs
index 283ae5a0c..f0ee69515 100644
--- a/server/lib/src/constants/acp.rs
+++ b/server/lib/src/constants/acp.rs
@@ -1076,7 +1076,7 @@ lazy_static! {
 lazy_static! {
     pub static ref IDM_ACP_SELF_READ_V1: BuiltinAcp = BuiltinAcp {
         name: "idm_acp_self_read",
-        uuid: UUID_IDM_ACP_SELF_READ_V1,
+        uuid: UUID_IDM_ACP_SELF_READ,
         description:
             "Builtin IDM Control for self read - required for whoami and many other functions",
         classes: vec![
@@ -1111,6 +1111,45 @@ lazy_static! {
     };
 }
 
+lazy_static! {
+    pub static ref IDM_ACP_SELF_READ_DL8: BuiltinAcp = BuiltinAcp {
+        name: "idm_acp_self_read",
+        uuid: UUID_IDM_ACP_SELF_READ,
+        description:
+            "Builtin IDM Control for self read - required for whoami and many other functions",
+        classes: vec![
+            EntryClass::Object,
+            EntryClass::AccessControlProfile,
+            EntryClass::AccessControlSearch,
+        ],
+        receiver: BuiltinAcpReceiver::Group(vec![UUID_IDM_ALL_ACCOUNTS]),
+        target: BuiltinAcpTarget::Filter(ProtoFilter::SelfUuid),
+        search_attrs: vec![
+            Attribute::Class,
+            Attribute::Name,
+            Attribute::Spn,
+            Attribute::DisplayName,
+            Attribute::LegalName,
+            Attribute::Class,
+            Attribute::MemberOf,
+            Attribute::Mail,
+            Attribute::RadiusSecret,
+            Attribute::GidNumber,
+            Attribute::LoginShell,
+            Attribute::Uuid,
+            Attribute::SyncParentUuid,
+            Attribute::AccountExpire,
+            Attribute::AccountValidFrom,
+            Attribute::PrimaryCredential,
+            Attribute::UserAuthTokenSession,
+            Attribute::PassKeys,
+            Attribute::AttestedPasskeys,
+            Attribute::ApplicationPassword,
+        ],
+        ..Default::default()
+    };
+}
+
 lazy_static! {
     pub static ref IDM_ACP_SELF_WRITE_V1: BuiltinAcp = BuiltinAcp{
         name: "idm_acp_self_write",
@@ -1133,6 +1172,7 @@ lazy_static! {
             Attribute::PassKeys,
             Attribute::AttestedPasskeys,
             Attribute::UserAuthTokenSession,
+            Attribute::ApplicationPassword,
         ],
         modify_present_attrs: vec![
             Attribute::DisplayName,
@@ -1143,6 +1183,7 @@ lazy_static! {
             Attribute::UnixPassword,
             Attribute::PassKeys,
             Attribute::AttestedPasskeys,
+            Attribute::ApplicationPassword,
         ],
         ..Default::default()
     };
@@ -1181,6 +1222,42 @@ lazy_static! {
     };
 }
 
+lazy_static! {
+    pub static ref IDM_ACP_SELF_WRITE_DL8: BuiltinAcp = BuiltinAcp{
+        name: "idm_acp_self_write",
+        uuid: UUID_IDM_ACP_SELF_WRITE_V1,
+        classes: vec![
+            EntryClass::Object,
+            EntryClass::AccessControlProfile,
+            EntryClass::AccessControlModify,
+            ],
+        description: "Builtin IDM Control for self write - required for people to update their own credentials in line with best practices.",
+        receiver: BuiltinAcpReceiver::Group ( vec![UUID_IDM_ALL_PERSONS] ),
+        target: BuiltinAcpTarget::Filter(ProtoFilter::SelfUuid),
+        modify_removed_attrs: vec![
+            Attribute::RadiusSecret,
+            Attribute::PrimaryCredential,
+            Attribute::SshPublicKey,
+            Attribute::UnixPassword,
+            Attribute::PassKeys,
+            Attribute::AttestedPasskeys,
+            Attribute::UserAuthTokenSession,
+            Attribute::ApplicationPassword,
+        ],
+        modify_present_attrs: vec![
+            Attribute::RadiusSecret,
+            Attribute::PrimaryCredential,
+            Attribute::SshPublicKey,
+            Attribute::UnixPassword,
+            Attribute::PassKeys,
+            Attribute::AttestedPasskeys,
+            Attribute::ApplicationPassword,
+            Attribute::ApplicationPassword,
+        ],
+        ..Default::default()
+    };
+}
+
 lazy_static! {
     pub static ref IDM_ACP_SELF_NAME_WRITE_V1: BuiltinAcp = BuiltinAcp{
         name: "idm_acp_self_name_write",
@@ -2054,3 +2131,129 @@ lazy_static! {
         ..Default::default()
     };
 }
+
+lazy_static! {
+    pub static ref IDM_ACP_APPLICATION_MANAGE_DL8: BuiltinAcp = BuiltinAcp{
+        classes: vec![
+            EntryClass::Object,
+            EntryClass::AccessControlProfile,
+            EntryClass::AccessControlCreate,
+            EntryClass::AccessControlDelete,
+            EntryClass::AccessControlModify,
+            EntryClass::AccessControlSearch
+            ],
+        name: "idm_acp_application_manage",
+        uuid: UUID_IDM_ACP_APPLICATION_MANAGE,
+        description: "Builtin IDM Control for creating and deleting applications in the directory",
+        receiver: BuiltinAcpReceiver::Group ( vec![UUID_IDM_APPLICATION_ADMINS] ),
+        // Any application
+        target: BuiltinAcpTarget::Filter( ProtoFilter::And(vec![
+            match_class_filter!(EntryClass::Application),
+            FILTER_ANDNOT_TOMBSTONE_OR_RECYCLED.clone()
+        ])),
+        search_attrs: vec![
+            Attribute::Class,
+            Attribute::Uuid,
+            Attribute::Name,
+            Attribute::Description,
+            Attribute::DisplayName,
+            Attribute::Mail,
+            Attribute::UnixPassword,
+            Attribute::ApiTokenSession,
+            Attribute::UserAuthTokenSession,
+            Attribute::LinkedGroup,
+            Attribute::EntryManagedBy,
+        ],
+        create_attrs: vec![
+            Attribute::Class,
+            Attribute::Uuid,
+            Attribute::Name,
+            Attribute::Description,
+            Attribute::DisplayName,
+            Attribute::Mail,
+            Attribute::LinkedGroup,
+            Attribute::EntryManagedBy,
+        ],
+        create_classes: vec![
+            EntryClass::Object,
+            EntryClass::Account,
+            EntryClass::ServiceAccount,
+            EntryClass::Application,
+        ],
+        modify_present_attrs: vec![
+            Attribute::Name,
+            Attribute::Description,
+            Attribute::DisplayName,
+            Attribute::Mail,
+            Attribute::UnixPassword,
+            Attribute::ApiTokenSession,
+            Attribute::LinkedGroup,
+            Attribute::EntryManagedBy,
+        ],
+        modify_removed_attrs: vec![
+            Attribute::Name,
+            Attribute::Description,
+            Attribute::DisplayName,
+            Attribute::Mail,
+            Attribute::UnixPassword,
+            Attribute::ApiTokenSession,
+            Attribute::UserAuthTokenSession,
+            Attribute::LinkedGroup,
+            Attribute::EntryManagedBy,
+        ],
+        ..Default::default()
+    };
+}
+
+lazy_static! {
+    pub static ref IDM_ACP_APPLICATION_ENTRY_MANAGER_DL8: BuiltinAcp = BuiltinAcp {
+        classes: vec![
+            EntryClass::Object,
+            EntryClass::AccessControlProfile,
+            EntryClass::AccessControlModify,
+            EntryClass::AccessControlSearch
+        ],
+        name: "idm_acp_application_entry_manager",
+        uuid: UUID_IDM_ACP_APPLICATION_ENTRY_MANAGER,
+        description: "Builtin IDM Control for allowing EntryManager to read and modify applications",
+        receiver: BuiltinAcpReceiver::EntryManager,
+        // Applications that belong to the Entry Manager.
+        target: BuiltinAcpTarget::Filter( ProtoFilter::And(vec![
+            match_class_filter!(EntryClass::Application),
+            FILTER_ANDNOT_TOMBSTONE_OR_RECYCLED.clone()
+        ])),
+        search_attrs: vec![
+            Attribute::Class,
+            Attribute::Uuid,
+            Attribute::Name,
+            Attribute::DisplayName,
+            Attribute::Mail,
+            Attribute::UnixPassword,
+            Attribute::ApiTokenSession,
+            Attribute::UserAuthTokenSession,
+            Attribute::Description,
+            Attribute::LinkedGroup,
+            Attribute::EntryManagedBy,
+        ],
+        modify_present_attrs: vec![
+            Attribute::Name,
+            Attribute::Description,
+            Attribute::DisplayName,
+            Attribute::Mail,
+            Attribute::UnixPassword,
+            Attribute::ApiTokenSession,
+            Attribute::LinkedGroup,
+        ],
+        modify_removed_attrs: vec![
+            Attribute::Name,
+            Attribute::Description,
+            Attribute::DisplayName,
+            Attribute::Mail,
+            Attribute::UnixPassword,
+            Attribute::ApiTokenSession,
+            Attribute::UserAuthTokenSession,
+            Attribute::LinkedGroup,
+        ],
+        ..Default::default()
+    };
+}
diff --git a/server/lib/src/constants/entries.rs b/server/lib/src/constants/entries.rs
index 096ce4ad2..bfbe52359 100644
--- a/server/lib/src/constants/entries.rs
+++ b/server/lib/src/constants/entries.rs
@@ -52,6 +52,7 @@ pub enum Attribute {
     AcpSearchAttr,
     AcpTargetScope,
     ApiTokenSession,
+    ApplicationPassword,
     AttestedPasskeys,
     Attr,
     AttributeName,
@@ -116,6 +117,7 @@ pub enum Attribute {
     LegalName,
     LimitSearchMaxResults,
     LimitSearchMaxFilterTest,
+    LinkedGroup,
     LoginShell,
     Mail,
     May,
@@ -244,6 +246,7 @@ impl<'a> TryFrom<&'a str> for Attribute {
             ATTR_ACP_SEARCH_ATTR => Attribute::AcpSearchAttr,
             ATTR_ACP_TARGET_SCOPE => Attribute::AcpTargetScope,
             ATTR_API_TOKEN_SESSION => Attribute::ApiTokenSession,
+            ATTR_APPLICATION_PASSWORD => Attribute::ApplicationPassword,
             ATTR_ATTESTED_PASSKEYS => Attribute::AttestedPasskeys,
             ATTR_ATTR => Attribute::Attr,
             ATTR_ATTRIBUTENAME => Attribute::AttributeName,
@@ -305,6 +308,7 @@ impl<'a> TryFrom<&'a str> for Attribute {
             ATTR_LDAP_KEYS => Attribute::LdapKeys,
             ATTR_SSH_PUBLICKEY => Attribute::SshPublicKey,
             ATTR_LEGALNAME => Attribute::LegalName,
+            ATTR_LINKEDGROUP => Attribute::LinkedGroup,
             ATTR_LOGINSHELL => Attribute::LoginShell,
             ATTR_LIMIT_SEARCH_MAX_RESULTS => Attribute::LimitSearchMaxResults,
             ATTR_LIMIT_SEARCH_MAX_FILTER_TEST => Attribute::LimitSearchMaxFilterTest,
@@ -420,6 +424,7 @@ impl From<Attribute> for &'static str {
             Attribute::AcpSearchAttr => ATTR_ACP_SEARCH_ATTR,
             Attribute::AcpTargetScope => ATTR_ACP_TARGET_SCOPE,
             Attribute::ApiTokenSession => ATTR_API_TOKEN_SESSION,
+            Attribute::ApplicationPassword => ATTR_APPLICATION_PASSWORD,
             Attribute::AttestedPasskeys => ATTR_ATTESTED_PASSKEYS,
             Attribute::Attr => ATTR_ATTR,
             Attribute::AttributeName => ATTR_ATTRIBUTENAME,
@@ -483,6 +488,7 @@ impl From<Attribute> for &'static str {
             Attribute::LegalName => ATTR_LEGALNAME,
             Attribute::LimitSearchMaxResults => ATTR_LIMIT_SEARCH_MAX_RESULTS,
             Attribute::LimitSearchMaxFilterTest => ATTR_LIMIT_SEARCH_MAX_FILTER_TEST,
+            Attribute::LinkedGroup => ATTR_LINKEDGROUP,
             Attribute::LoginShell => ATTR_LOGINSHELL,
             Attribute::Mail => ATTR_MAIL,
             Attribute::May => ATTR_MAY,
@@ -621,6 +627,7 @@ pub enum EntryClass {
     AccessControlTargetScope,
     Account,
     AccountPolicy,
+    Application,
     AttributeType,
     Builtin,
     Class,
@@ -675,6 +682,7 @@ impl From<EntryClass> for &'static str {
             EntryClass::AccessControlTargetScope => "access_control_target_scope",
             EntryClass::Account => "account",
             EntryClass::AccountPolicy => "account_policy",
+            EntryClass::Application => "application",
             EntryClass::AttributeType => "attributetype",
             EntryClass::Builtin => ENTRYCLASS_BUILTIN,
             EntryClass::Class => ATTR_CLASS,
diff --git a/server/lib/src/constants/groups.rs b/server/lib/src/constants/groups.rs
index bfe0d6aed..8ad1b64f7 100644
--- a/server/lib/src/constants/groups.rs
+++ b/server/lib/src/constants/groups.rs
@@ -407,6 +407,15 @@ lazy_static! {
         ],
         ..Default::default()
     };
+
+    pub static ref BUILTIN_GROUP_APPLICATION_ADMINS: BuiltinGroup = BuiltinGroup {
+        name: "idm_application_admins",
+        uuid: UUID_IDM_APPLICATION_ADMINS,
+        description: "Builtin Application Administration Group.",
+        entry_managed_by: Some(UUID_IDM_ADMINS),
+        members: vec![UUID_IDM_ADMINS],
+        ..Default::default()
+    };
 }
 
 /// Make a list of all the non-admin BuiltinGroup's that are created by default, doing it in a standard-ish way so we can use it around the platform
@@ -426,6 +435,7 @@ pub fn idm_builtin_non_admin_groups() -> Vec<&'static BuiltinGroup> {
         &BUILTIN_GROUP_PEOPLE_PII_READ,
         &BUILTIN_GROUP_PEOPLE_ON_BOARDING,
         &BUILTIN_GROUP_SERVICE_ACCOUNT_ADMINS,
+        &BUILTIN_GROUP_APPLICATION_ADMINS,
         &IDM_GROUP_ADMINS_V1,
         &IDM_ALL_PERSONS,
         &IDM_ALL_ACCOUNTS,
diff --git a/server/lib/src/constants/schema.rs b/server/lib/src/constants/schema.rs
index 1c33aab11..5a0664ed0 100644
--- a/server/lib/src/constants/schema.rs
+++ b/server/lib/src/constants/schema.rs
@@ -769,6 +769,16 @@ pub static ref SCHEMA_ATTR_REFERS_DL7: SchemaAttribute = SchemaAttribute {
     ..Default::default()
 };
 
+pub static ref SCHEMA_ATTR_LINKED_GROUP_DL8: SchemaAttribute = SchemaAttribute {
+    uuid: UUID_SCHEMA_ATTR_LINKED_GROUP,
+    name: Attribute::LinkedGroup.into(),
+    description: "A reference linking a group to an entry".to_string(),
+
+    multivalue: false,
+    syntax: SyntaxType::ReferenceUuid,
+    ..Default::default()
+};
+
 pub static ref SCHEMA_ATTR_CERTIFICATE_DL7: SchemaAttribute = SchemaAttribute {
     uuid: UUID_SCHEMA_ATTR_CERTIFICATE,
     name: Attribute::Certificate.into(),
@@ -778,6 +788,16 @@ pub static ref SCHEMA_ATTR_CERTIFICATE_DL7: SchemaAttribute = SchemaAttribute {
     ..Default::default()
 };
 
+pub static ref SCHEMA_ATTR_APPLICATION_PASSWORD_DL8: SchemaAttribute = SchemaAttribute {
+    uuid: UUID_SCHEMA_ATTR_APPLICATION_PASSWORD,
+    name: Attribute::ApplicationPassword.into(),
+    description: "A set of application passwords".to_string(),
+
+    multivalue: true,
+    syntax: SyntaxType::ApplicationPassword,
+    ..Default::default()
+};
+
 // === classes ===
 
 pub static ref SCHEMA_CLASS_PERSON: SchemaClass = SchemaClass {
@@ -819,7 +839,34 @@ pub static ref SCHEMA_CLASS_PERSON_DL5: SchemaClass = SchemaClass {
     systemmust: vec![
         Attribute::IdVerificationEcKey.into()
     ],
-    systemexcludes: vec![EntryClass::ServiceAccount.into()],
+    systemexcludes: vec![EntryClass::ServiceAccount.into(), EntryClass::Application.into()],
+    ..Default::default()
+};
+
+pub static ref SCHEMA_CLASS_PERSON_DL8: SchemaClass = SchemaClass {
+    uuid: UUID_SCHEMA_CLASS_PERSON,
+    name: EntryClass::Person.into(),
+    description: "Object representation of a person".to_string(),
+
+    sync_allowed: true,
+    systemmay: vec![
+        Attribute::PrimaryCredential.into(),
+        Attribute::PassKeys.into(),
+        Attribute::AttestedPasskeys.into(),
+        Attribute::CredentialUpdateIntentToken.into(),
+        Attribute::SshPublicKey.into(),
+        Attribute::RadiusSecret.into(),
+        Attribute::OAuth2ConsentScopeMap.into(),
+        Attribute::UserAuthTokenSession.into(),
+        Attribute::OAuth2Session.into(),
+        Attribute::Mail.into(),
+        Attribute::LegalName.into(),
+        Attribute::ApplicationPassword.into(),
+    ],
+    systemmust: vec![
+        Attribute::IdVerificationEcKey.into()
+    ],
+    systemexcludes: vec![EntryClass::ServiceAccount.into(), EntryClass::Application.into()],
     ..Default::default()
 };
 
@@ -1291,4 +1338,15 @@ pub static ref SCHEMA_CLASS_CLIENT_CERTIFICATE_DL7: SchemaClass = SchemaClass {
     ..Default::default()
 };
 
+pub static ref SCHEMA_CLASS_APPLICATION_DL8: SchemaClass = SchemaClass {
+    uuid: UUID_SCHEMA_CLASS_APPLICATION,
+    name: EntryClass::Application.into(),
+
+    description: "The class representing an application".to_string(),
+    systemmust: vec![Attribute::Name.into(), Attribute::LinkedGroup.into()],
+    systemmay: vec![Attribute::Description.into()],
+    systemsupplements: vec![EntryClass::ServiceAccount.into()],
+    ..Default::default()
+};
+
 );
diff --git a/server/lib/src/constants/uuids.rs b/server/lib/src/constants/uuids.rs
index e804d7891..8d4739ab0 100644
--- a/server/lib/src/constants/uuids.rs
+++ b/server/lib/src/constants/uuids.rs
@@ -68,6 +68,7 @@ pub const UUID_IDM_SERVICE_ACCOUNT_ADMINS: Uuid = uuid!("00000000-0000-0000-0000
 pub const UUID_IDM_ACCOUNT_POLICY_ADMINS: Uuid = uuid!("00000000-0000-0000-0000-000000000047");
 pub const UUID_IDM_PEOPLE_SELF_NAME_WRITE: Uuid = uuid!("00000000-0000-0000-0000-000000000048");
 pub const UUID_IDM_CLIENT_CERTIFICATE_ADMINS: Uuid = uuid!("00000000-0000-0000-0000-000000000049");
+pub const UUID_IDM_APPLICATION_ADMINS: Uuid = uuid!("00000000-0000-0000-0000-000000000050");
 
 //
 pub const UUID_IDM_HIGH_PRIVILEGE: Uuid = uuid!("00000000-0000-0000-0000-000000001000");
@@ -311,6 +312,10 @@ pub const UUID_SCHEMA_CLASS_CLIENT_CERTIFICATE: Uuid =
     uuid!("00000000-0000-0000-0000-ffff00000179");
 pub const UUID_SCHEMA_ATTR_OAUTH2_STRICT_REDIRECT_URI: Uuid =
     uuid!("00000000-0000-0000-0000-ffff00000180");
+pub const UUID_SCHEMA_CLASS_APPLICATION: Uuid = uuid!("00000000-0000-0000-0000-ffff00000181");
+pub const UUID_SCHEMA_ATTR_LINKED_GROUP: Uuid = uuid!("00000000-0000-0000-0000-ffff00000182");
+pub const UUID_SCHEMA_ATTR_APPLICATION_PASSWORD: Uuid =
+    uuid!("00000000-0000-0000-0000-ffff00000183");
 
 // System and domain infos
 // I'd like to strongly criticise william of the past for making poor choices about these allocations.
@@ -324,7 +329,7 @@ pub const UUID_DOMAIN_INFO: Uuid = uuid!("00000000-0000-0000-0000-ffffff000025")
 // skip 00 / 01 - see system info
 pub const UUID_IDM_ACP_RECYCLE_BIN_SEARCH_V1: Uuid = uuid!("00000000-0000-0000-0000-ffffff000002");
 pub const UUID_IDM_ACP_RECYCLE_BIN_REVIVE_V1: Uuid = uuid!("00000000-0000-0000-0000-ffffff000003");
-pub const UUID_IDM_ACP_SELF_READ_V1: Uuid = uuid!("00000000-0000-0000-0000-ffffff000004");
+pub const UUID_IDM_ACP_SELF_READ: Uuid = uuid!("00000000-0000-0000-0000-ffffff000004");
 pub const UUID_IDM_ACP_ALL_ACCOUNTS_POSIX_READ_V1: Uuid =
     uuid!("00000000-0000-0000-0000-ffffff000006");
 pub const UUID_IDM_ACP_PEOPLE_PII_READ_V1: Uuid = uuid!("00000000-0000-0000-0000-ffffff000007");
@@ -427,6 +432,10 @@ pub const UUID_KEY_PROVIDER_INTERNAL: Uuid = uuid!("00000000-0000-0000-0000-ffff
 pub const UUID_IDM_ACP_HP_CLIENT_CERTIFICATE_MANAGER: Uuid =
     uuid!("00000000-0000-0000-0000-ffffff000071");
 
+pub const UUID_IDM_ACP_APPLICATION_ENTRY_MANAGER: Uuid =
+    uuid!("00000000-0000-0000-0000-ffffff000072");
+pub const UUID_IDM_ACP_APPLICATION_MANAGE: Uuid = uuid!("00000000-0000-0000-0000-ffffff000073");
+
 // End of system ranges
 pub const UUID_DOES_NOT_EXIST: Uuid = uuid!("00000000-0000-0000-0000-fffffffffffe");
 pub const UUID_ANONYMOUS: Uuid = uuid!("00000000-0000-0000-0000-ffffffffffff");
diff --git a/server/lib/src/credential/apppwd.rs b/server/lib/src/credential/apppwd.rs
new file mode 100644
index 000000000..7225208a7
--- /dev/null
+++ b/server/lib/src/credential/apppwd.rs
@@ -0,0 +1,65 @@
+use crate::credential::{CryptoPolicy, Password};
+use crate::prelude::*;
+use kanidm_proto::internal::OperationError;
+use std::cmp::Ordering;
+use std::fmt;
+
+#[derive(Clone)]
+pub struct ApplicationPassword {
+    pub uuid: Uuid,
+    pub(crate) application: Uuid,
+    pub(crate) label: String,
+    pub(crate) password: Password,
+}
+
+impl fmt::Debug for ApplicationPassword {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.debug_struct("ApplicationPassword")
+            .field("uuid", &self.uuid)
+            .field("application", &self.application)
+            .field("label", &self.label)
+            .finish()
+    }
+}
+
+impl ApplicationPassword {
+    pub fn new(
+        application: Uuid,
+        label: &str,
+        cleartext: &str,
+        policy: &CryptoPolicy,
+    ) -> Result<ApplicationPassword, OperationError> {
+        let pw = Password::new(policy, cleartext).map_err(|e| {
+            error!(crypto_err = ?e);
+            e.into()
+        })?;
+        let ap = ApplicationPassword {
+            uuid: Uuid::new_v4(),
+            application,
+            label: label.to_string(),
+            password: pw,
+        };
+        Ok(ap)
+    }
+}
+
+impl PartialEq for ApplicationPassword {
+    fn eq(&self, other: &Self) -> bool {
+        self.uuid == other.uuid
+            || (self.application == other.application && self.label == other.label)
+    }
+}
+
+impl Eq for ApplicationPassword {}
+
+impl PartialOrd for ApplicationPassword {
+    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+impl Ord for ApplicationPassword {
+    fn cmp(&self, other: &Self) -> Ordering {
+        self.uuid.cmp(&other.uuid)
+    }
+}
diff --git a/server/lib/src/credential/mod.rs b/server/lib/src/credential/mod.rs
index 7c30597e2..f201afdd5 100644
--- a/server/lib/src/credential/mod.rs
+++ b/server/lib/src/credential/mod.rs
@@ -11,6 +11,7 @@ use webauthn_rs_core::proto::{Credential as WebauthnCredential, CredentialV3};
 use crate::be::dbvalue::{DbBackupCodeV1, DbCred};
 use crate::repl::proto::{ReplBackupCodeV1, ReplCredV1, ReplPasskeyV4V1, ReplSecurityKeyV4V1};
 
+pub mod apppwd;
 pub mod softlock;
 pub mod totp;
 
diff --git a/server/lib/src/entry.rs b/server/lib/src/entry.rs
index e0ce66317..fa22a71e2 100644
--- a/server/lib/src/entry.rs
+++ b/server/lib/src/entry.rs
@@ -50,6 +50,7 @@ use webauthn_rs::prelude::{
 use crate::be::dbentry::{DbEntry, DbEntryVers};
 use crate::be::dbvalue::DbValueSetV2;
 use crate::be::{IdxKey, IdxSlope};
+use crate::credential::apppwd::ApplicationPassword;
 use crate::credential::Credential;
 use crate::filter::{Filter, FilterInvalid, FilterResolved, FilterValidResolved};
 use crate::idm::ldap::ldap_vattr_map;
@@ -2933,6 +2934,15 @@ impl<VALID, STATE> Entry<VALID, STATE> {
             .and_then(|vs| vs.as_webauthn_attestation_ca_list())
     }
 
+    pub fn get_ava_application_password(
+        &self,
+        attr: Attribute,
+    ) -> Option<&BTreeMap<Uuid, Vec<ApplicationPassword>>> {
+        self.attrs
+            .get(attr.as_ref())
+            .and_then(|vs| vs.as_application_password_map())
+    }
+
     #[inline(always)]
     /// Return a single security principle name, if valid to transform this value.
     pub(crate) fn generate_spn(&self, domain_name: &str) -> Option<Value> {
diff --git a/server/lib/src/idm/account.rs b/server/lib/src/idm/account.rs
index fd62bfc9d..f38266235 100644
--- a/server/lib/src/idm/account.rs
+++ b/server/lib/src/idm/account.rs
@@ -14,10 +14,12 @@ use webauthn_rs::prelude::{
 use super::accountpolicy::ResolvedAccountPolicy;
 use crate::constants::UUID_ANONYMOUS;
 use crate::credential::softlock::CredSoftLockPolicy;
-use crate::credential::Credential;
+use crate::credential::{apppwd::ApplicationPassword, Credential};
 use crate::entry::{Entry, EntryCommitted, EntryReduced, EntrySealed};
 use crate::event::SearchEvent;
+use crate::idm::application::Application;
 use crate::idm::group::Group;
+use crate::idm::ldap::{LdapBoundToken, LdapSession};
 use crate::idm::server::{IdmServerProxyReadTransaction, IdmServerProxyWriteTransaction};
 use crate::modify::{ModifyInvalid, ModifyList};
 use crate::prelude::*;
@@ -65,6 +67,7 @@ pub struct Account {
     pub mail: Vec<String>,
     pub credential_update_intent_tokens: BTreeMap<String, IntentTokenState>,
     pub(crate) unix_extn: Option<UnixExtensions>,
+    pub apps_pwds: BTreeMap<Uuid, Vec<ApplicationPassword>>,
 }
 
 macro_rules! try_from_entry {
@@ -197,6 +200,11 @@ macro_rules! try_from_entry {
             None
         };
 
+        let apps_pwds = $value
+            .get_ava_application_password(Attribute::ApplicationPassword)
+            .cloned()
+            .unwrap_or_default();
+
         Ok(Account {
             uuid,
             name,
@@ -215,6 +223,7 @@ macro_rules! try_from_entry {
             mail,
             credential_update_intent_tokens,
             unix_extn,
+            apps_pwds,
         })
     }};
 }
@@ -738,6 +747,53 @@ impl Account {
             }
         }
     }
+
+    pub(crate) fn verify_application_password(
+        &self,
+        application: &Application,
+        cleartext: &str,
+    ) -> Result<Option<LdapBoundToken>, OperationError> {
+        if let Some(v) = self.apps_pwds.get(&application.uuid) {
+            for ap in v.iter() {
+                let password_verified = ap.password.verify(cleartext).map_err(|e| {
+                    error!(crypto_err = ?e);
+                    e.into()
+                })?;
+
+                if password_verified {
+                    let session_id = uuid::Uuid::new_v4();
+                    security_info!(
+                        "Starting session {} for {} {}",
+                        session_id,
+                        self.spn,
+                        self.uuid
+                    );
+
+                    return Ok(Some(LdapBoundToken {
+                        spn: self.spn.clone(),
+                        session_id,
+                        effective_session: LdapSession::ApplicationPasswordBind(
+                            application.uuid,
+                            self.uuid,
+                        ),
+                    }));
+                }
+            }
+        }
+        Ok(None)
+    }
+
+    pub(crate) fn generate_application_password_mod(
+        &self,
+        application: Uuid,
+        label: &str,
+        cleartext: &str,
+        policy: &CryptoPolicy,
+    ) -> Result<ModifyList<ModifyInvalid>, OperationError> {
+        let ap = ApplicationPassword::new(application, label, cleartext, policy)?;
+        let vap = Value::ApplicationPassword(ap);
+        Ok(ModifyList::new_append(Attribute::ApplicationPassword, vap))
+    }
 }
 
 // Need to also add a "to UserAuthToken" ...
diff --git a/server/lib/src/idm/application.rs b/server/lib/src/idm/application.rs
new file mode 100644
index 000000000..313118009
--- /dev/null
+++ b/server/lib/src/idm/application.rs
@@ -0,0 +1,728 @@
+use super::ldap::{LdapBoundToken, LdapSession};
+use crate::idm::account::Account;
+use crate::idm::event::LdapApplicationAuthEvent;
+use crate::idm::server::{IdmServerAuthTransaction, IdmServerTransaction};
+use crate::prelude::*;
+use concread::cowcell::*;
+use hashbrown::HashMap;
+use kanidm_proto::internal::OperationError;
+use std::sync::Arc;
+use uuid::Uuid;
+
+#[derive(Clone)]
+pub(crate) struct Application {
+    pub uuid: Uuid,
+    pub name: String,
+    pub linked_group: Uuid,
+}
+
+impl Application {
+    #[cfg(test)]
+    pub(crate) fn try_from_entry_ro(
+        value: &Entry<EntrySealed, EntryCommitted>,
+        _qs: &mut QueryServerReadTransaction,
+    ) -> Result<Self, OperationError> {
+        if !value.attribute_equality(Attribute::Class, &EntryClass::Application.to_partialvalue()) {
+            return Err(OperationError::InvalidAccountState(
+                "Missing class: application".to_string(),
+            ));
+        }
+
+        let uuid = value.get_uuid();
+
+        let name = value
+            .get_ava_single_iname(Attribute::Name)
+            .map(|s| s.to_string())
+            .ok_or_else(|| {
+                OperationError::InvalidAccountState(format!(
+                    "Missing attribute: {}",
+                    Attribute::Name
+                ))
+            })?;
+
+        let linked_group = value
+            .get_ava_single_refer(Attribute::LinkedGroup)
+            .ok_or_else(|| {
+                OperationError::InvalidAccountState(format!(
+                    "Missing attribute: {}",
+                    Attribute::LinkedGroup
+                ))
+            })?;
+
+        Ok(Application {
+            name,
+            uuid,
+            linked_group,
+        })
+    }
+}
+
+#[derive(Clone)]
+struct LdapApplicationsInner {
+    set: HashMap<String, Application>,
+}
+
+pub struct LdapApplications {
+    inner: CowCell<LdapApplicationsInner>,
+}
+
+pub struct LdapApplicationsReadTransaction {
+    inner: CowCellReadTxn<LdapApplicationsInner>,
+}
+
+pub struct LdapApplicationsWriteTransaction<'a> {
+    inner: CowCellWriteTxn<'a, LdapApplicationsInner>,
+}
+
+impl<'a> LdapApplicationsWriteTransaction<'a> {
+    pub fn reload(&mut self, value: Vec<Arc<EntrySealedCommitted>>) -> Result<(), OperationError> {
+        let app_set: Result<HashMap<_, _>, _> = value
+            .into_iter()
+            .map(|ent| {
+                if !ent.attribute_equality(Attribute::Class, &EntryClass::Application.into()) {
+                    error!("Missing class application");
+                    return Err(OperationError::InvalidEntryState);
+                }
+
+                let uuid = ent.get_uuid();
+                let name = ent
+                    .get_ava_single_iname(Attribute::Name)
+                    .map(str::to_string)
+                    .ok_or(OperationError::InvalidValueState)?;
+
+                let linked_group = ent
+                    .get_ava_single_refer(Attribute::LinkedGroup)
+                    .ok_or(OperationError::InvalidValueState)?;
+
+                let app = Application {
+                    uuid,
+                    name: name.clone(),
+                    linked_group,
+                };
+
+                Ok((name, app))
+            })
+            .collect();
+
+        let new_inner = LdapApplicationsInner { set: app_set? };
+        self.inner.replace(new_inner);
+
+        Ok(())
+    }
+
+    pub fn commit(self) {
+        self.inner.commit();
+    }
+}
+
+impl LdapApplications {
+    pub fn read(&self) -> LdapApplicationsReadTransaction {
+        LdapApplicationsReadTransaction {
+            inner: self.inner.read(),
+        }
+    }
+
+    pub fn write(&self) -> LdapApplicationsWriteTransaction {
+        LdapApplicationsWriteTransaction {
+            inner: self.inner.write(),
+        }
+    }
+}
+
+impl TryFrom<Vec<Arc<EntrySealedCommitted>>> for LdapApplications {
+    type Error = OperationError;
+
+    fn try_from(value: Vec<Arc<EntrySealedCommitted>>) -> Result<Self, Self::Error> {
+        let apps = LdapApplications {
+            inner: CowCell::new(LdapApplicationsInner {
+                set: HashMap::new(),
+            }),
+        };
+
+        let mut apps_wr = apps.write();
+        apps_wr.reload(value)?;
+        apps_wr.commit();
+        Ok(apps)
+    }
+}
+
+impl<'a> IdmServerAuthTransaction<'a> {
+    pub async fn application_auth_ldap(
+        &mut self,
+        lae: &LdapApplicationAuthEvent,
+        ct: Duration,
+    ) -> Result<Option<LdapBoundToken>, OperationError> {
+        let usr_entry = self.get_qs_txn().internal_search_uuid(lae.target)?;
+
+        let account: Account =
+            Account::try_from_entry_ro(&usr_entry, &mut self.qs_read).map_err(|e| {
+                error!("Failed to search account {:?}", e);
+                e
+            })?;
+
+        if account.is_anonymous() {
+            return Err(OperationError::InvalidUuid);
+        }
+
+        if !account.is_within_valid_time(ct) {
+            security_info!("Account has expired or is not yet valid, not allowing to proceed");
+            return Err(OperationError::SessionExpired);
+        }
+
+        let application = self
+            .applications
+            .inner
+            .set
+            .get(&lae.application)
+            .ok_or_else(|| {
+                info!("Application {:?} not found", lae.application);
+                OperationError::NoMatchingEntries
+            })?;
+
+        // Check linked group membership
+        let is_memberof = usr_entry
+            .get_ava_refer(Attribute::MemberOf)
+            .map(|member_of_set| member_of_set.contains(&application.linked_group))
+            .unwrap_or_default();
+
+        if !is_memberof {
+            debug!(
+                "User {:?} not member of application {}:{:?} linked group {:?}",
+                account.uuid, application.name, application.uuid, application.linked_group,
+            );
+            return Ok(None);
+        }
+
+        match account.verify_application_password(application, lae.cleartext.as_str())? {
+            Some(_) => {
+                let session_id = Uuid::new_v4();
+                security_info!(
+                    "Starting session {} for {} {} with application {}:{:?}",
+                    session_id,
+                    account.spn,
+                    account.uuid,
+                    application.name,
+                    application.uuid,
+                );
+
+                Ok(Some(LdapBoundToken {
+                    spn: account.spn,
+                    session_id,
+                    effective_session: LdapSession::UnixBind(account.uuid),
+                }))
+            }
+            None => {
+                security_info!("Account does not have a configured application password.");
+                Ok(None)
+            }
+        }
+    }
+}
+
+#[derive(Debug)]
+pub struct GenerateApplicationPasswordEvent {
+    pub ident: Identity,
+    pub target: Uuid,
+    pub application: Uuid,
+    pub label: String,
+}
+
+impl GenerateApplicationPasswordEvent {
+    pub fn from_parts(
+        ident: Identity,
+        target: Uuid,
+        application: Uuid,
+        label: String,
+    ) -> Result<Self, OperationError> {
+        Ok(GenerateApplicationPasswordEvent {
+            ident,
+            target,
+            application,
+            label,
+        })
+    }
+
+    pub fn new_internal(target: Uuid, application: Uuid, label: String) -> Self {
+        GenerateApplicationPasswordEvent {
+            ident: Identity::from_internal(),
+            target,
+            application,
+            label,
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::event::CreateEvent;
+    use crate::idm::account::Account;
+    use crate::idm::application::Application;
+    use crate::idm::application::GenerateApplicationPasswordEvent;
+    use crate::idm::server::IdmServerTransaction;
+    use crate::idm::serviceaccount::{DestroyApiTokenEvent, GenerateApiTokenEvent};
+    use crate::prelude::*;
+    use compact_jwt::{dangernoverify::JwsDangerReleaseWithoutVerify, JwsVerifier};
+    use kanidm_proto::internal::ApiToken as ProtoApiToken;
+    use std::time::Duration;
+
+    const TEST_CURRENT_TIME: u64 = 6000;
+
+    // Tests that only the correct combinations of [Account, Person, Application and
+    // ServiceAccount] classes are allowed.
+    #[idm_test]
+    async fn test_idm_application_excludes(idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed) {
+        let ct = Duration::from_secs(TEST_CURRENT_TIME);
+        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
+
+        // ServiceAccount, Application and Person not allowed together
+        let test_grp_name = "testgroup1";
+        let test_grp_uuid = Uuid::new_v4();
+        let e1 = entry_init!(
+            (Attribute::Class, EntryClass::Object.to_value()),
+            (Attribute::Class, EntryClass::Group.to_value()),
+            (Attribute::Name, Value::new_iname(test_grp_name)),
+            (Attribute::Uuid, Value::Uuid(test_grp_uuid))
+        );
+        let test_entry_uuid = Uuid::new_v4();
+        let e2 = entry_init!(
+            (Attribute::Class, EntryClass::Object.to_value()),
+            (Attribute::Class, EntryClass::Account.to_value()),
+            (Attribute::Class, EntryClass::ServiceAccount.to_value()),
+            (Attribute::Class, EntryClass::Application.to_value()),
+            (Attribute::Class, EntryClass::Person.to_value()),
+            (Attribute::Name, Value::new_iname("test_app_name")),
+            (Attribute::Uuid, Value::Uuid(test_entry_uuid)),
+            (Attribute::Description, Value::new_utf8s("test_app_desc")),
+            (
+                Attribute::DisplayName,
+                Value::new_utf8s("test_app_dispname")
+            ),
+            (Attribute::LinkedGroup, Value::Refer(test_grp_uuid))
+        );
+        let ce = CreateEvent::new_internal(vec![e1, e2]);
+        let cr = idms_prox_write.qs_write.create(&ce);
+        assert!(!cr.is_ok());
+
+        // Application and Person not allowed together
+        let test_grp_name = "testgroup1";
+        let test_grp_uuid = Uuid::new_v4();
+        let e1 = entry_init!(
+            (Attribute::Class, EntryClass::Object.to_value()),
+            (Attribute::Class, EntryClass::Group.to_value()),
+            (Attribute::Name, Value::new_iname(test_grp_name)),
+            (Attribute::Uuid, Value::Uuid(test_grp_uuid))
+        );
+        let test_entry_uuid = Uuid::new_v4();
+        let e2 = entry_init!(
+            (Attribute::Class, EntryClass::Object.to_value()),
+            (Attribute::Class, EntryClass::Account.to_value()),
+            (Attribute::Class, EntryClass::Application.to_value()),
+            (Attribute::Class, EntryClass::Person.to_value()),
+            (Attribute::Name, Value::new_iname("test_app_name")),
+            (Attribute::Uuid, Value::Uuid(test_entry_uuid)),
+            (Attribute::Description, Value::new_utf8s("test_app_desc")),
+            (
+                Attribute::DisplayName,
+                Value::new_utf8s("test_app_dispname")
+            ),
+            (Attribute::LinkedGroup, Value::Refer(test_grp_uuid))
+        );
+        let ce = CreateEvent::new_internal(vec![e1, e2]);
+        let cr = idms_prox_write.qs_write.create(&ce);
+        assert!(!cr.is_ok());
+
+        // Supplements not satisfied, Application supplements ServiceAccount
+        let test_grp_name = "testgroup1";
+        let test_grp_uuid = Uuid::new_v4();
+        let e1 = entry_init!(
+            (Attribute::Class, EntryClass::Object.to_value()),
+            (Attribute::Class, EntryClass::Group.to_value()),
+            (Attribute::Name, Value::new_iname(test_grp_name)),
+            (Attribute::Uuid, Value::Uuid(test_grp_uuid))
+        );
+        let test_entry_uuid = Uuid::new_v4();
+        let e2 = entry_init!(
+            (Attribute::Class, EntryClass::Object.to_value()),
+            (Attribute::Class, EntryClass::Account.to_value()),
+            (Attribute::Class, EntryClass::Application.to_value()),
+            (Attribute::Name, Value::new_iname("test_app_name")),
+            (Attribute::Uuid, Value::Uuid(test_entry_uuid)),
+            (Attribute::Description, Value::new_utf8s("test_app_desc")),
+            (Attribute::LinkedGroup, Value::Refer(test_grp_uuid))
+        );
+        let ce = CreateEvent::new_internal(vec![e1, e2]);
+        let cr = idms_prox_write.qs_write.create(&ce);
+        assert!(!cr.is_ok());
+
+        // Supplements not satisfied, Application supplements ServiceAccount
+        let test_grp_name = "testgroup1";
+        let test_grp_uuid = Uuid::new_v4();
+        let e1 = entry_init!(
+            (Attribute::Class, EntryClass::Object.to_value()),
+            (Attribute::Class, EntryClass::Group.to_value()),
+            (Attribute::Name, Value::new_iname(test_grp_name)),
+            (Attribute::Uuid, Value::Uuid(test_grp_uuid))
+        );
+        let test_entry_uuid = Uuid::new_v4();
+        let e2 = entry_init!(
+            (Attribute::Class, EntryClass::Object.to_value()),
+            (Attribute::Class, EntryClass::Application.to_value()),
+            (Attribute::Name, Value::new_iname("test_app_name")),
+            (Attribute::Uuid, Value::Uuid(test_entry_uuid)),
+            (Attribute::Description, Value::new_utf8s("test_app_desc")),
+            (Attribute::LinkedGroup, Value::Refer(test_grp_uuid))
+        );
+        let ce = CreateEvent::new_internal(vec![e1, e2]);
+        let cr = idms_prox_write.qs_write.create(&ce);
+        assert!(!cr.is_ok());
+
+        // Supplements satisfied, Application supplements ServiceAccount
+        let test_grp_name = "testgroup1";
+        let test_grp_uuid = Uuid::new_v4();
+        let e1 = entry_init!(
+            (Attribute::Class, EntryClass::Object.to_value()),
+            (Attribute::Class, EntryClass::Group.to_value()),
+            (Attribute::Name, Value::new_iname(test_grp_name)),
+            (Attribute::Uuid, Value::Uuid(test_grp_uuid))
+        );
+        let test_entry_uuid = Uuid::new_v4();
+        let e2 = entry_init!(
+            (Attribute::Class, EntryClass::Object.to_value()),
+            (Attribute::Class, EntryClass::Application.to_value()),
+            (Attribute::Class, EntryClass::ServiceAccount.to_value()),
+            (Attribute::Name, Value::new_iname("test_app_name")),
+            (Attribute::Uuid, Value::Uuid(test_entry_uuid)),
+            (Attribute::Description, Value::new_utf8s("test_app_desc")),
+            (Attribute::LinkedGroup, Value::Refer(test_grp_uuid))
+        );
+        let ce = CreateEvent::new_internal(vec![e1, e2]);
+        let cr = idms_prox_write.qs_write.create(&ce);
+        assert!(cr.is_ok());
+    }
+
+    // Tests it is not possible to create an applicatin without the linked group attribute
+    #[idm_test]
+    async fn test_idm_application_no_linked_group(
+        idms: &IdmServer,
+        _idms_delayed: &mut IdmServerDelayed,
+    ) {
+        let ct = Duration::from_secs(TEST_CURRENT_TIME);
+        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
+
+        let test_entry_uuid = Uuid::new_v4();
+
+        let e1 = entry_init!(
+            (Attribute::Class, EntryClass::Object.to_value()),
+            (Attribute::Class, EntryClass::Account.to_value()),
+            (Attribute::Class, EntryClass::ServiceAccount.to_value()),
+            (Attribute::Class, EntryClass::Application.to_value()),
+            (Attribute::Name, Value::new_iname("test_app_name")),
+            (Attribute::Uuid, Value::Uuid(test_entry_uuid)),
+            (Attribute::Description, Value::new_utf8s("test_app_desc")),
+            (
+                Attribute::DisplayName,
+                Value::new_utf8s("test_app_dispname")
+            )
+        );
+
+        let ce = CreateEvent::new_internal(vec![e1]);
+        let cr = idms_prox_write.qs_write.create(&ce);
+        assert!(!cr.is_ok());
+    }
+
+    // Tests creating an applicatin with a real linked group attribute
+    #[idm_test]
+    async fn test_idm_application_linked_group(
+        idms: &IdmServer,
+        _idms_delayed: &mut IdmServerDelayed,
+    ) {
+        let test_entry_name = "test_app_name";
+        let test_entry_uuid = Uuid::new_v4();
+        let test_grp_name = "testgroup1";
+        let test_grp_uuid = Uuid::new_v4();
+
+        {
+            let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
+
+            let e1 = entry_init!(
+                (Attribute::Class, EntryClass::Object.to_value()),
+                (Attribute::Class, EntryClass::Group.to_value()),
+                (Attribute::Name, Value::new_iname(test_grp_name)),
+                (Attribute::Uuid, Value::Uuid(test_grp_uuid))
+            );
+
+            let e2 = entry_init!(
+                (Attribute::Class, EntryClass::Object.to_value()),
+                (Attribute::Class, EntryClass::Account.to_value()),
+                (Attribute::Class, EntryClass::ServiceAccount.to_value()),
+                (Attribute::Class, EntryClass::Application.to_value()),
+                (Attribute::Name, Value::new_iname(test_entry_name)),
+                (Attribute::Uuid, Value::Uuid(test_entry_uuid)),
+                (Attribute::Description, Value::new_utf8s("test_app_desc")),
+                (
+                    Attribute::DisplayName,
+                    Value::new_utf8s("test_app_dispname")
+                ),
+                (Attribute::LinkedGroup, Value::Refer(test_grp_uuid))
+            );
+
+            let ce = CreateEvent::new_internal(vec![e1, e2]);
+            let cr = idms_prox_write.qs_write.create(&ce);
+            assert!(cr.is_ok());
+
+            let cr = idms_prox_write.qs_write.commit();
+            assert!(cr.is_ok());
+        }
+
+        {
+            let mut idms_prox_read = idms.proxy_read().await.unwrap();
+            let app = idms_prox_read
+                .qs_read
+                .internal_search_uuid(test_entry_uuid)
+                .and_then(|entry| {
+                    Application::try_from_entry_ro(&entry, &mut idms_prox_read.qs_read)
+                })
+                .map_err(|e| {
+                    trace!("Error: {:?}", e);
+                    e
+                });
+            assert!(app.is_ok());
+
+            let app = app.unwrap();
+            assert_eq!(app.name, "test_app_name");
+            assert_eq!(app.uuid, test_entry_uuid);
+            assert_eq!(app.linked_group, test_grp_uuid);
+        }
+
+        // Test reference integrity. An attempt to remove a linked group blocks
+        // the group being deleted
+        {
+            let de = DeleteEvent::new_internal_invalid(filter!(f_eq(
+                Attribute::Uuid,
+                PartialValue::Uuid(test_grp_uuid)
+            )));
+            let mut idms_proxy_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
+            assert!(idms_proxy_write.qs_write.delete(&de).is_err());
+        }
+
+        {
+            let de = DeleteEvent::new_internal_invalid(filter!(f_eq(
+                Attribute::Uuid,
+                PartialValue::Uuid(test_entry_uuid)
+            )));
+            let mut idms_proxy_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
+            assert!(idms_proxy_write.qs_write.delete(&de).is_ok());
+            assert!(idms_proxy_write.qs_write.commit().is_ok());
+        }
+
+        {
+            let de = DeleteEvent::new_internal_invalid(filter!(f_eq(
+                Attribute::Uuid,
+                PartialValue::Uuid(test_grp_uuid)
+            )));
+            let mut idms_proxy_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
+            assert!(idms_proxy_write.qs_write.delete(&de).is_ok());
+            assert!(idms_proxy_write.qs_write.commit().is_ok());
+        }
+    }
+
+    #[idm_test]
+    async fn test_idm_application_delete(idms: &IdmServer, _idms_delayed: &mut IdmServerDelayed) {
+        let test_usr_name = "testuser1";
+        let test_usr_uuid = Uuid::new_v4();
+        let test_app_name = "testapp1";
+        let test_app_uuid = Uuid::new_v4();
+        let test_grp_name = "testgroup1";
+        let test_grp_uuid = Uuid::new_v4();
+
+        {
+            let ct = duration_from_epoch_now();
+            let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
+
+            let e1 = entry_init!(
+                (Attribute::Class, EntryClass::Object.to_value()),
+                (Attribute::Class, EntryClass::Account.to_value()),
+                (Attribute::Class, EntryClass::Person.to_value()),
+                (Attribute::Name, Value::new_iname(test_usr_name)),
+                (Attribute::Uuid, Value::Uuid(test_usr_uuid)),
+                (Attribute::Description, Value::new_utf8s(test_usr_name)),
+                (Attribute::DisplayName, Value::new_utf8s(test_usr_name))
+            );
+
+            let e2 = entry_init!(
+                (Attribute::Class, EntryClass::Object.to_value()),
+                (Attribute::Class, EntryClass::Group.to_value()),
+                (Attribute::Name, Value::new_iname(test_grp_name)),
+                (Attribute::Uuid, Value::Uuid(test_grp_uuid)),
+                (Attribute::Member, Value::Refer(test_usr_uuid))
+            );
+
+            let e3 = entry_init!(
+                (Attribute::Class, EntryClass::Object.to_value()),
+                (Attribute::Class, EntryClass::ServiceAccount.to_value()),
+                (Attribute::Class, EntryClass::Application.to_value()),
+                (Attribute::Name, Value::new_iname(test_app_name)),
+                (Attribute::Uuid, Value::Uuid(test_app_uuid)),
+                (Attribute::LinkedGroup, Value::Refer(test_grp_uuid))
+            );
+
+            let ce = CreateEvent::new_internal(vec![e1, e2, e3]);
+            let cr = idms_prox_write.qs_write.create(&ce);
+            assert!(cr.is_ok());
+
+            let ev = GenerateApplicationPasswordEvent {
+                ident: Identity::from_internal(),
+                target: test_usr_uuid,
+                application: test_app_uuid,
+                label: "label".to_string(),
+            };
+            idms_prox_write
+                .generate_application_password(&ev)
+                .expect("Failed to create application password");
+
+            let cr = idms_prox_write.qs_write.commit();
+            assert!(cr.is_ok());
+        }
+
+        {
+            let mut idms_prox_read = idms.proxy_read().await.unwrap();
+            let account = idms_prox_read
+                .qs_read
+                .internal_search_uuid(test_usr_uuid)
+                .and_then(|entry| Account::try_from_entry_ro(&entry, &mut idms_prox_read.qs_read))
+                .map_err(|e| {
+                    trace!("Error: {:?}", e);
+                    e
+                })
+                .expect("Failed to search for account");
+
+            assert!(account.apps_pwds.values().count() > 0);
+        }
+
+        // Test reference integrity. If app is removed linked application passwords must go
+        {
+            let de = DeleteEvent::new_internal_invalid(filter!(f_eq(
+                Attribute::Uuid,
+                PartialValue::Uuid(test_app_uuid)
+            )));
+            let mut idms_proxy_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
+            assert!(idms_proxy_write.qs_write.delete(&de).is_ok());
+            assert!(idms_proxy_write.qs_write.commit().is_ok());
+        }
+
+        {
+            let mut idms_prox_read = idms.proxy_read().await.unwrap();
+            assert!(idms_prox_read
+                .qs_read
+                .internal_search_uuid(test_app_uuid)
+                .is_err());
+        }
+
+        {
+            let mut idms_prox_read = idms.proxy_read().await.unwrap();
+            let account = idms_prox_read
+                .qs_read
+                .internal_search_uuid(test_usr_uuid)
+                .and_then(|entry| Account::try_from_entry_ro(&entry, &mut idms_prox_read.qs_read))
+                .map_err(|e| {
+                    trace!("Error: {:?}", e);
+                    e
+                })
+                .expect("Failed to search for account");
+
+            assert!(account.apps_pwds.values().count() == 0);
+        }
+    }
+
+    // Test apitoken for application entries
+    #[idm_test]
+    async fn test_idm_application_api_token(
+        idms: &IdmServer,
+        _idms_delayed: &mut IdmServerDelayed,
+    ) {
+        let ct = Duration::from_secs(TEST_CURRENT_TIME);
+        let past_grc = Duration::from_secs(TEST_CURRENT_TIME + 1) + AUTH_TOKEN_GRACE_WINDOW;
+        let exp = Duration::from_secs(TEST_CURRENT_TIME + 6000);
+        let post_exp = Duration::from_secs(TEST_CURRENT_TIME + 6010);
+        let mut idms_prox_write = idms.proxy_write(ct).await.unwrap();
+
+        let test_entry_uuid = Uuid::new_v4();
+        let test_group_uuid = Uuid::new_v4();
+
+        let e1 = entry_init!(
+            (Attribute::Class, EntryClass::Object.to_value()),
+            (Attribute::Class, EntryClass::Group.to_value()),
+            (Attribute::Name, Value::new_iname("test_group")),
+            (Attribute::Uuid, Value::Uuid(test_group_uuid))
+        );
+
+        let e2 = entry_init!(
+            (Attribute::Class, EntryClass::Object.to_value()),
+            (Attribute::Class, EntryClass::ServiceAccount.to_value()),
+            (Attribute::Class, EntryClass::Application.to_value()),
+            (Attribute::Name, Value::new_iname("test_app_name")),
+            (Attribute::Uuid, Value::Uuid(test_entry_uuid)),
+            (Attribute::Description, Value::new_utf8s("test_app_desc")),
+            (Attribute::LinkedGroup, Value::Refer(test_group_uuid))
+        );
+
+        let ce = CreateEvent::new_internal(vec![e1, e2]);
+        let cr = idms_prox_write.qs_write.create(&ce);
+        assert!(cr.is_ok());
+
+        let gte = GenerateApiTokenEvent::new_internal(test_entry_uuid, "TestToken", Some(exp));
+
+        let api_token = idms_prox_write
+            .service_account_generate_api_token(&gte, ct)
+            .expect("failed to generate new api token");
+
+        trace!(?api_token);
+
+        // Deserialise it.
+        let jws_verifier = JwsDangerReleaseWithoutVerify::default();
+
+        let apitoken_inner = jws_verifier
+            .verify(&api_token)
+            .unwrap()
+            .from_json::<ProtoApiToken>()
+            .unwrap();
+
+        let ident = idms_prox_write
+            .validate_client_auth_info_to_ident(api_token.clone().into(), ct)
+            .expect("Unable to verify api token.");
+
+        assert!(ident.get_uuid() == Some(test_entry_uuid));
+
+        // Check the expiry
+        assert!(
+            idms_prox_write
+                .validate_client_auth_info_to_ident(api_token.clone().into(), post_exp)
+                .expect_err("Should not succeed")
+                == OperationError::SessionExpired
+        );
+
+        // Delete session
+        let dte =
+            DestroyApiTokenEvent::new_internal(apitoken_inner.account_id, apitoken_inner.token_id);
+        assert!(idms_prox_write
+            .service_account_destroy_api_token(&dte)
+            .is_ok());
+
+        // Within gracewindow?
+        // This is okay, because we are within the gracewindow.
+        let ident = idms_prox_write
+            .validate_client_auth_info_to_ident(api_token.clone().into(), ct)
+            .expect("Unable to verify api token.");
+        assert!(ident.get_uuid() == Some(test_entry_uuid));
+
+        // Past gracewindow?
+        assert!(
+            idms_prox_write
+                .validate_client_auth_info_to_ident(api_token.clone().into(), past_grc)
+                .expect_err("Should not succeed")
+                == OperationError::SessionExpired
+        );
+
+        assert!(idms_prox_write.commit().is_ok());
+    }
+}
diff --git a/server/lib/src/idm/event.rs b/server/lib/src/idm/event.rs
index 09ab3a3b8..a95ed358f 100644
--- a/server/lib/src/idm/event.rs
+++ b/server/lib/src/idm/event.rs
@@ -276,6 +276,22 @@ impl LdapTokenAuthEvent {
     }
 }
 
+pub struct LdapApplicationAuthEvent {
+    pub application: String,
+    pub target: Uuid,
+    pub cleartext: String,
+}
+
+impl LdapApplicationAuthEvent {
+    pub fn new(app_name: &str, usr_uuid: Uuid, cleartext: String) -> Result<Self, OperationError> {
+        Ok(LdapApplicationAuthEvent {
+            application: app_name.to_string(),
+            target: usr_uuid,
+            cleartext,
+        })
+    }
+}
+
 #[derive(Debug)]
 pub struct AuthEventStepInit {
     pub username: String,
diff --git a/server/lib/src/idm/ldap.rs b/server/lib/src/idm/ldap.rs
index 7efdb50a4..e9e373cbf 100644
--- a/server/lib/src/idm/ldap.rs
+++ b/server/lib/src/idm/ldap.rs
@@ -9,14 +9,14 @@ use compact_jwt::JwsCompact;
 use kanidm_proto::constants::*;
 use kanidm_proto::internal::{ApiToken, UserAuthToken};
 use ldap3_proto::simple::*;
-use regex::Regex;
+use regex::{Regex, RegexBuilder};
 use std::net::IpAddr;
 use tracing::trace;
 use uuid::Uuid;
 
 use crate::event::SearchEvent;
-use crate::idm::event::{LdapAuthEvent, LdapTokenAuthEvent};
-use crate::idm::server::{IdmServer, IdmServerTransaction};
+use crate::idm::event::{LdapApplicationAuthEvent, LdapAuthEvent, LdapTokenAuthEvent};
+use crate::idm::server::{IdmServer, IdmServerAuthTransaction, IdmServerTransaction};
 use crate::prelude::*;
 
 // Clippy doesn't like Bind here. But proto needs unboxed ldapmsg,
@@ -38,6 +38,7 @@ pub enum LdapSession {
     UnixBind(Uuid),
     UserAuthToken(UserAuthToken),
     ApiToken(ApiToken),
+    ApplicationPasswordBind(Uuid, Uuid),
 }
 
 #[derive(Debug, Clone)]
@@ -65,12 +66,13 @@ pub struct LdapServer {
 enum LdapBindTarget {
     Account(Uuid),
     ApiToken,
+    Application(String, Uuid),
 }
 
 impl LdapServer {
     pub async fn new(idms: &IdmServer) -> Result<Self, OperationError> {
         // let ct = duration_from_epoch_now();
-        let mut idms_prox_read = idms.proxy_read().await.unwrap();
+        let mut idms_prox_read = idms.proxy_read().await?;
         // This is the rootdse path.
         // get the domain_info item
         let domain_entry = idms_prox_read
@@ -87,11 +89,31 @@ impl LdapServer {
             })
             .ok_or(OperationError::InvalidEntryState)?;
 
-        let dnre = Regex::new(format!("^((?P<attr>[^=]+)=(?P<val>[^=]+),)?{basedn}$").as_str())
-            .map_err(|_| OperationError::InvalidEntryState)?;
+        // It is necessary to swap greed to avoid the first group "<attr>=<val>" matching the
+        // next group "app=<app>", son one can use "app=app1,dc=test,dc=net" as search base:
+        // Greedy (app=app1,dc=test,dc=net):
+        //     Match 1      - app=app1,dc=test,dc=net
+        //     Group 1      - app=app1,
+        //     Group <attr> - app
+        //     Group <val>  - app1
+        //     Group 6      - dc=test,dc=net
+        // Ungreedy (app=app1,dc=test,dc=net):
+        //     Match 1      - app=app1,dc=test,dc=net
+        //     Group 4      - app=app1,
+        //     Group <app>  - app1
+        //     Group 6      - dc=test,dc=net
+        let dnre = RegexBuilder::new(
+            format!("^((?P<attr>[^=,]+)=(?P<val>[^=,]+),)?(app=(?P<app>[^=,]+),)?({basedn})$")
+                .as_str(),
+        )
+        .swap_greed(true)
+        .build()
+        .map_err(|_| OperationError::InvalidEntryState)?;
 
-        let binddnre = Regex::new(format!("^(([^=,]+)=)?(?P<val>[^=,]+)(,{basedn})?$").as_str())
-            .map_err(|_| OperationError::InvalidEntryState)?;
+        let binddnre = Regex::new(
+            format!("^((([^=,]+)=)?(?P<val>[^=,]+))(,app=(?P<app>[^=,]+))?(,{basedn})?$").as_str(),
+        )
+        .map_err(|_| OperationError::InvalidEntryState)?;
 
         let rootdse = LdapSearchResultEntry {
             dn: "".to_string(),
@@ -299,7 +321,7 @@ impl LdapServer {
             admin_info!(attr = ?k_attrs, "LDAP Search Request Mapped Attrs");
 
             let ct = duration_from_epoch_now();
-            let mut idm_read = idms.proxy_read().await.unwrap();
+            let mut idm_read = idms.proxy_read().await?;
             // Now start the txn - we need it for resolving filter components.
 
             // join the filter, with ext_filter
@@ -406,41 +428,8 @@ impl LdapServer {
         );
         let ct = duration_from_epoch_now();
 
-        let mut idm_auth = idms.auth().await.unwrap();
-
-        let target: LdapBindTarget = if dn.is_empty() {
-            if pw.is_empty() {
-                LdapBindTarget::Account(UUID_ANONYMOUS)
-            } else {
-                // This is the path to access api-token logins.
-                LdapBindTarget::ApiToken
-            }
-        } else if dn == "dn=token" {
-            // Is the passed dn requesting token auth?
-            // We use dn= here since these are attr=value, and dn is a phantom so it will
-            // never be present or match a real value. We also make it an ava so that clients
-            // that over-zealously validate dn syntax are happy.
-            LdapBindTarget::ApiToken
-        } else {
-            let rdn = self
-                .binddnre
-                .captures(dn)
-                .and_then(|caps| caps.name("val"))
-                .map(|v| v.as_str().to_string())
-                .ok_or(OperationError::NoMatchingEntries)?;
-
-            if rdn.is_empty() {
-                // That's weird ...
-                return Err(OperationError::NoMatchingEntries);
-            }
-
-            let uuid = idm_auth.qs_read.name_to_uuid(rdn.as_str()).map_err(|e| {
-                request_error!(err = ?e, ?rdn, "Error resolving rdn to target");
-                e
-            })?;
-
-            LdapBindTarget::Account(uuid)
-        };
+        let mut idm_auth = idms.auth().await?;
+        let target = self.bind_target_from_bind_dn(&mut idm_auth, dn, pw).await?;
 
         let result = match target {
             LdapBindTarget::Account(uuid) => {
@@ -456,6 +445,11 @@ impl LdapServer {
                 let lae = LdapTokenAuthEvent::from_parts(jwsc)?;
                 idm_auth.token_auth_ldap(&lae, ct).await?
             }
+            LdapBindTarget::Application(ref app_name, usr_uuid) => {
+                let lae =
+                    LdapApplicationAuthEvent::new(app_name.as_str(), usr_uuid, pw.to_string())?;
+                idm_auth.application_auth_ldap(&lae, ct).await?
+            }
         };
 
         idm_auth.commit()?;
@@ -507,7 +501,7 @@ impl LdapServer {
         };
 
         let ct = duration_from_epoch_now();
-        let mut idm_read = idms.proxy_read().await.unwrap();
+        let mut idm_read = idms.proxy_read().await?;
         // Now start the txn - we need it for resolving filter components.
 
         // join the filter, with ext_filter
@@ -698,6 +692,63 @@ impl LdapServer {
             },
         } // end match server op
     }
+
+    async fn bind_target_from_bind_dn<'a>(
+        &self,
+        idm_auth: &mut IdmServerAuthTransaction<'a>,
+        dn: &str,
+        pw: &str,
+    ) -> Result<LdapBindTarget, OperationError> {
+        if dn.is_empty() {
+            if pw.is_empty() {
+                return Ok(LdapBindTarget::Account(UUID_ANONYMOUS));
+            } else {
+                // This is the path to access api-token logins.
+                return Ok(LdapBindTarget::ApiToken);
+            }
+        } else if dn == "dn=token" {
+            // Is the passed dn requesting token auth?
+            // We use dn= here since these are attr=value, and dn is a phantom so it will
+            // never be present or match a real value. We also make it an ava so that clients
+            // that over-zealously validate dn syntax are happy.
+            return Ok(LdapBindTarget::ApiToken);
+        }
+
+        if let Some(captures) = self.binddnre.captures(dn) {
+            if let Some(usr) = captures.name("val") {
+                let usr = usr.as_str();
+
+                if usr.is_empty() {
+                    error!("Failed to parse user name from bind DN, it is empty (capture group is {:#?})", captures.name("val"));
+                    return Err(OperationError::NoMatchingEntries);
+                }
+
+                let usr_uuid = idm_auth.qs_read.name_to_uuid(usr).map_err(|e| {
+                    error!(err = ?e, ?usr, "Error resolving rdn to target");
+                    e
+                })?;
+
+                if let Some(app) = captures.name("app") {
+                    let app = app.as_str();
+
+                    if app.is_empty() {
+                        error!("Failed to parse application name from bind DN, it is empty (capture group is {:#?})", captures.name("app"));
+                        return Err(OperationError::NoMatchingEntries);
+                    }
+
+                    return Ok(LdapBindTarget::Application(app.to_string(), usr_uuid));
+                }
+
+                return Ok(LdapBindTarget::Account(usr_uuid));
+            }
+        }
+
+        error!(
+            "Failed to parse bind DN, no captures. Bind DN was {:?})",
+            dn
+        );
+        Err(OperationError::NoMatchingEntries)
+    }
 }
 
 fn ldap_domain_to_dc(input: &str) -> String {
@@ -800,7 +851,8 @@ mod tests {
     use ldap3_proto::simple::*;
 
     use super::{LdapServer, LdapSession};
-    use crate::idm::event::UnixPasswordChangeEvent;
+    use crate::idm::application::GenerateApplicationPasswordEvent;
+    use crate::idm::event::{LdapApplicationAuthEvent, UnixPasswordChangeEvent};
     use crate::idm::serviceaccount::GenerateApiTokenEvent;
 
     const TEST_PASSWORD: &str = "ntaoeuntnaoeuhraohuercahu😍";
@@ -997,6 +1049,578 @@ mod tests {
         assert!(ldaps.do_bind(idms, "claire", "test").await.is_err());
     }
 
+    #[idm_test]
+    async fn test_ldap_application_dnre(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
+        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
+
+        let testdn = format!("app=app1,{0}", ldaps.basedn);
+        let captures = ldaps.dnre.captures(testdn.as_str()).unwrap();
+        assert!(captures.name("app").is_some());
+        assert!(captures.name("attr").is_none());
+        assert!(captures.name("val").is_none());
+
+        let testdn = format!("uid=foo,app=app1,{0}", ldaps.basedn);
+        let captures = ldaps.dnre.captures(testdn.as_str()).unwrap();
+        assert!(captures.name("app").is_some());
+        assert!(captures.name("attr").is_some());
+        assert!(captures.name("val").is_some());
+
+        let testdn = format!("uid=foo,{0}", ldaps.basedn);
+        let captures = ldaps.dnre.captures(testdn.as_str()).unwrap();
+        assert!(captures.name("app").is_none());
+        assert!(captures.name("attr").is_some());
+        assert!(captures.name("val").is_some());
+    }
+
+    #[idm_test]
+    async fn test_ldap_application_search(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
+        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
+
+        let usr_uuid = Uuid::new_v4();
+        let grp_uuid = Uuid::new_v4();
+        let app_uuid = Uuid::new_v4();
+        let app_name = "testapp1";
+
+        // Setup person, group and application
+        {
+            let e1 = entry_init!(
+                (Attribute::Class, EntryClass::Object.to_value()),
+                (Attribute::Class, EntryClass::Account.to_value()),
+                (Attribute::Class, EntryClass::Person.to_value()),
+                (Attribute::Name, Value::new_iname("testperson1")),
+                (Attribute::Uuid, Value::Uuid(usr_uuid)),
+                (Attribute::Description, Value::new_utf8s("testperson1")),
+                (Attribute::DisplayName, Value::new_utf8s("testperson1"))
+            );
+
+            let e2 = entry_init!(
+                (Attribute::Class, EntryClass::Object.to_value()),
+                (Attribute::Class, EntryClass::Group.to_value()),
+                (Attribute::Name, Value::new_iname("testgroup1")),
+                (Attribute::Uuid, Value::Uuid(grp_uuid))
+            );
+
+            let e3 = entry_init!(
+                (Attribute::Class, EntryClass::Object.to_value()),
+                (Attribute::Class, EntryClass::ServiceAccount.to_value()),
+                (Attribute::Class, EntryClass::Application.to_value()),
+                (Attribute::Name, Value::new_iname(app_name)),
+                (Attribute::Uuid, Value::Uuid(app_uuid)),
+                (Attribute::LinkedGroup, Value::Refer(grp_uuid))
+            );
+
+            let ct = duration_from_epoch_now();
+            let mut server_txn = idms.proxy_write(ct).await.unwrap();
+            assert!(server_txn
+                .qs_write
+                .internal_create(vec![e1, e2, e3])
+                .and_then(|_| server_txn.commit())
+                .is_ok());
+        }
+
+        // Setup the anonymous login
+        let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
+        assert!(anon_t.effective_session == LdapSession::UnixBind(UUID_ANONYMOUS));
+
+        // Searches under application base DN must show same content
+        let sr = SearchRequest {
+            msgid: 1,
+            base: format!("app={app_name},dc=example,dc=com"),
+            scope: LdapSearchScope::Subtree,
+            filter: LdapFilter::Present(Attribute::ObjectClass.to_string()),
+            attrs: vec!["*".to_string()],
+        };
+
+        let r1 = ldaps
+            .do_search(idms, &sr, &anon_t, Source::Internal)
+            .await
+            .unwrap();
+
+        let sr = SearchRequest {
+            msgid: 1,
+            base: format!("dc=example,dc=com"),
+            scope: LdapSearchScope::Subtree,
+            filter: LdapFilter::Present(Attribute::ObjectClass.to_string()),
+            attrs: vec!["*".to_string()],
+        };
+
+        let r2 = ldaps
+            .do_search(idms, &sr, &anon_t, Source::Internal)
+            .await
+            .unwrap();
+        assert!(r1.len() > 0);
+        assert!(r1.len() == r2.len());
+    }
+
+    #[idm_test]
+    async fn test_ldap_application_bind(idms: &IdmServer, _idms_delayed: &IdmServerDelayed) {
+        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
+
+        let usr_uuid = Uuid::new_v4();
+        let grp_uuid = Uuid::new_v4();
+        let app_uuid = Uuid::new_v4();
+
+        // Setup person, group and application
+        {
+            let e1 = entry_init!(
+                (Attribute::Class, EntryClass::Object.to_value()),
+                (Attribute::Class, EntryClass::Account.to_value()),
+                (Attribute::Class, EntryClass::Person.to_value()),
+                (Attribute::Name, Value::new_iname("testperson1")),
+                (Attribute::Uuid, Value::Uuid(usr_uuid)),
+                (Attribute::Description, Value::new_utf8s("testperson1")),
+                (Attribute::DisplayName, Value::new_utf8s("testperson1"))
+            );
+
+            let e2 = entry_init!(
+                (Attribute::Class, EntryClass::Object.to_value()),
+                (Attribute::Class, EntryClass::Group.to_value()),
+                (Attribute::Name, Value::new_iname("testgroup1")),
+                (Attribute::Uuid, Value::Uuid(grp_uuid))
+            );
+
+            let e3 = entry_init!(
+                (Attribute::Class, EntryClass::Object.to_value()),
+                (Attribute::Class, EntryClass::ServiceAccount.to_value()),
+                (Attribute::Class, EntryClass::Application.to_value()),
+                (Attribute::Name, Value::new_iname("testapp1")),
+                (Attribute::Uuid, Value::Uuid(app_uuid)),
+                (Attribute::LinkedGroup, Value::Refer(grp_uuid))
+            );
+
+            let ct = duration_from_epoch_now();
+            let mut server_txn = idms.proxy_write(ct).await.unwrap();
+            assert!(server_txn
+                .qs_write
+                .internal_create(vec![e1, e2, e3])
+                .and_then(|_| server_txn.commit())
+                .is_ok());
+        }
+
+        // No session, user not member of linked group
+        let res = ldaps
+            .do_bind(idms, "spn=testperson1,app=testapp1,dc=example,dc=com", "")
+            .await;
+        assert!(res.is_ok());
+        assert!(res.unwrap().is_none());
+
+        {
+            let ml = ModifyList::new_append(Attribute::Member, Value::Refer(usr_uuid));
+            let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
+            assert!(idms_prox_write
+                .qs_write
+                .internal_modify_uuid(grp_uuid, &ml)
+                .is_ok());
+            assert!(idms_prox_write.commit().is_ok());
+        }
+
+        // No session, user does not have app password for testapp1
+        let res = ldaps
+            .do_bind(idms, "spn=testperson1,app=testapp1,dc=example,dc=com", "")
+            .await;
+        assert!(res.is_ok());
+        assert!(res.unwrap().is_none());
+
+        let pass1: String;
+        let pass2: String;
+        let pass3: String;
+        {
+            let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
+
+            let ev = GenerateApplicationPasswordEvent::new_internal(
+                usr_uuid,
+                app_uuid,
+                "apppwd1".to_string(),
+            );
+            pass1 = idms_prox_write
+                .generate_application_password(&ev)
+                .expect("Failed to generate application password");
+
+            let ev = GenerateApplicationPasswordEvent::new_internal(
+                usr_uuid,
+                app_uuid,
+                "apppwd2".to_string(),
+            );
+            pass2 = idms_prox_write
+                .generate_application_password(&ev)
+                .expect("Failed to generate application password");
+
+            assert!(idms_prox_write.commit().is_ok());
+
+            // Application password overwritten on duplicated label
+            let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
+            let ev = GenerateApplicationPasswordEvent::new_internal(
+                usr_uuid,
+                app_uuid,
+                "apppwd2".to_string(),
+            );
+            pass3 = idms_prox_write
+                .generate_application_password(&ev)
+                .expect("Failed to generate application password");
+            assert!(idms_prox_write.commit().is_ok());
+        }
+
+        // Got session, app password valid
+        let res = ldaps
+            .do_bind(
+                idms,
+                "spn=testperson1,app=testapp1,dc=example,dc=com",
+                pass1.as_str(),
+            )
+            .await;
+        assert!(res.is_ok());
+        assert!(res.unwrap().is_some());
+
+        // No session, app password overwritten
+        let res = ldaps
+            .do_bind(
+                idms,
+                "spn=testperson1,app=testapp1,dc=example,dc=com",
+                pass2.as_str(),
+            )
+            .await;
+        assert!(res.is_ok());
+        assert!(res.unwrap().is_none());
+
+        // Got session, app password overwritten
+        let res = ldaps
+            .do_bind(
+                idms,
+                "spn=testperson1,app=testapp1,dc=example,dc=com",
+                pass3.as_str(),
+            )
+            .await;
+        assert!(res.is_ok());
+        assert!(res.unwrap().is_some());
+
+        // No session, invalid app password
+        let res = ldaps
+            .do_bind(
+                idms,
+                "spn=testperson1,app=testapp1,dc=example,dc=com",
+                "FOO",
+            )
+            .await;
+        assert!(res.is_ok());
+        assert!(res.unwrap().is_none());
+    }
+
+    #[idm_test]
+    async fn test_ldap_application_linked_group(
+        idms: &IdmServer,
+        _idms_delayed: &IdmServerDelayed,
+    ) {
+        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
+
+        let usr_uuid = Uuid::new_v4();
+        let usr_name = "testuser1";
+
+        let grp1_uuid = Uuid::new_v4();
+        let grp1_name = "testgroup1";
+        let grp2_uuid = Uuid::new_v4();
+        let grp2_name = "testgroup2";
+
+        let app1_uuid = Uuid::new_v4();
+        let app1_name = "testapp1";
+        let app2_uuid = Uuid::new_v4();
+        let app2_name = "testapp2";
+
+        // Setup person, groups and applications
+        {
+            let e1 = entry_init!(
+                (Attribute::Class, EntryClass::Object.to_value()),
+                (Attribute::Class, EntryClass::Account.to_value()),
+                (Attribute::Class, EntryClass::Person.to_value()),
+                (Attribute::Name, Value::new_iname(usr_name)),
+                (Attribute::Uuid, Value::Uuid(usr_uuid)),
+                (Attribute::Description, Value::new_utf8s(usr_name)),
+                (Attribute::DisplayName, Value::new_utf8s(usr_name))
+            );
+
+            let e2 = entry_init!(
+                (Attribute::Class, EntryClass::Object.to_value()),
+                (Attribute::Class, EntryClass::Group.to_value()),
+                (Attribute::Name, Value::new_iname(grp1_name)),
+                (Attribute::Uuid, Value::Uuid(grp1_uuid)),
+                (Attribute::Member, Value::Refer(usr_uuid))
+            );
+
+            let e3 = entry_init!(
+                (Attribute::Class, EntryClass::Object.to_value()),
+                (Attribute::Class, EntryClass::Group.to_value()),
+                (Attribute::Name, Value::new_iname(grp2_name)),
+                (Attribute::Uuid, Value::Uuid(grp2_uuid))
+            );
+
+            let e4 = entry_init!(
+                (Attribute::Class, EntryClass::Object.to_value()),
+                (Attribute::Class, EntryClass::ServiceAccount.to_value()),
+                (Attribute::Class, EntryClass::Application.to_value()),
+                (Attribute::Name, Value::new_iname(app1_name)),
+                (Attribute::Uuid, Value::Uuid(app1_uuid)),
+                (Attribute::LinkedGroup, Value::Refer(grp1_uuid))
+            );
+
+            let e5 = entry_init!(
+                (Attribute::Class, EntryClass::Object.to_value()),
+                (Attribute::Class, EntryClass::ServiceAccount.to_value()),
+                (Attribute::Class, EntryClass::Application.to_value()),
+                (Attribute::Name, Value::new_iname(app2_name)),
+                (Attribute::Uuid, Value::Uuid(app2_uuid)),
+                (Attribute::LinkedGroup, Value::Refer(grp2_uuid))
+            );
+
+            let ct = duration_from_epoch_now();
+            let mut server_txn = idms.proxy_write(ct).await.unwrap();
+            assert!(server_txn
+                .qs_write
+                .internal_create(vec![e1, e2, e3, e4, e5])
+                .and_then(|_| server_txn.commit())
+                .is_ok());
+        }
+
+        let pass_app1: String;
+        let pass_app2: String;
+        {
+            let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
+
+            let ev = GenerateApplicationPasswordEvent::new_internal(
+                usr_uuid,
+                app1_uuid,
+                "label".to_string(),
+            );
+            pass_app1 = idms_prox_write
+                .generate_application_password(&ev)
+                .expect("Failed to generate application password");
+
+            // It is possible to generate an application password even if the
+            // user is not member of the linked group
+            let ev = GenerateApplicationPasswordEvent::new_internal(
+                usr_uuid,
+                app2_uuid,
+                "label".to_string(),
+            );
+            pass_app2 = idms_prox_write
+                .generate_application_password(&ev)
+                .expect("Failed to generate application password");
+
+            assert!(idms_prox_write.commit().is_ok());
+        }
+
+        // Got session, app password valid
+        let res = ldaps
+            .do_bind(
+                idms,
+                format!("spn={usr_name},app={app1_name},dc=example,dc=com").as_str(),
+                pass_app1.as_str(),
+            )
+            .await;
+        assert!(res.is_ok());
+        assert!(res.unwrap().is_some());
+
+        // No session, not member
+        let res = ldaps
+            .do_bind(
+                idms,
+                format!("spn={usr_name},app={app2_name},dc=example,dc=com").as_str(),
+                pass_app2.as_str(),
+            )
+            .await;
+        assert!(res.is_ok());
+        assert!(res.unwrap().is_none());
+
+        // Add user to grp2
+        {
+            let ml = ModifyList::new_append(Attribute::Member, Value::Refer(usr_uuid));
+            let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
+            assert!(idms_prox_write
+                .qs_write
+                .internal_modify_uuid(grp2_uuid, &ml)
+                .is_ok());
+            assert!(idms_prox_write.commit().is_ok());
+        }
+
+        // Got session, app password valid
+        let res = ldaps
+            .do_bind(
+                idms,
+                format!("spn={usr_name},app={app2_name},dc=example,dc=com").as_str(),
+                pass_app2.as_str(),
+            )
+            .await;
+        assert!(res.is_ok());
+        assert!(res.unwrap().is_some());
+
+        // No session, wrong app
+        let res = ldaps
+            .do_bind(
+                idms,
+                format!("spn={usr_name},app={app1_name},dc=example,dc=com").as_str(),
+                pass_app2.as_str(),
+            )
+            .await;
+        assert!(res.is_ok());
+        assert!(res.unwrap().is_none());
+
+        // Bind error, app not exists
+        {
+            let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
+            let de = DeleteEvent::new_internal_invalid(filter!(f_eq(
+                Attribute::Uuid,
+                PartialValue::Uuid(app2_uuid)
+            )));
+            assert!(idms_prox_write.qs_write.delete(&de).is_ok());
+            assert!(idms_prox_write.commit().is_ok());
+        }
+
+        let res = ldaps
+            .do_bind(
+                idms,
+                format!("spn={usr_name},app={app2_name},dc=example,dc=com").as_str(),
+                pass_app2.as_str(),
+            )
+            .await;
+        assert!(res.is_err());
+    }
+
+    // For testing the timeouts
+    // We need times on this scale
+    //    not yet valid <-> valid from time <-> current_time <-> expire time <-> expired
+    const TEST_CURRENT_TIME: u64 = 6000;
+    const TEST_NOT_YET_VALID_TIME: u64 = TEST_CURRENT_TIME - 240;
+    const TEST_VALID_FROM_TIME: u64 = TEST_CURRENT_TIME - 120;
+    const TEST_EXPIRE_TIME: u64 = TEST_CURRENT_TIME + 120;
+    const TEST_AFTER_EXPIRY: u64 = TEST_CURRENT_TIME + 240;
+
+    async fn set_account_valid_time(idms: &IdmServer, acct: Uuid) {
+        let mut idms_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
+
+        let v_valid_from = Value::new_datetime_epoch(Duration::from_secs(TEST_VALID_FROM_TIME));
+        let v_expire = Value::new_datetime_epoch(Duration::from_secs(TEST_EXPIRE_TIME));
+
+        let me = ModifyEvent::new_internal_invalid(
+            filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(acct))),
+            ModifyList::new_list(vec![
+                Modify::Present(Attribute::AccountExpire.into(), v_expire),
+                Modify::Present(Attribute::AccountValidFrom.into(), v_valid_from),
+            ]),
+        );
+        assert!(idms_write.qs_write.modify(&me).is_ok());
+        idms_write.commit().expect("Must not fail");
+    }
+
+    #[idm_test]
+    async fn test_ldap_application_valid_from_expire(
+        idms: &IdmServer,
+        _idms_delayed: &IdmServerDelayed,
+    ) {
+        let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
+
+        let usr_uuid = Uuid::new_v4();
+        let usr_name = "testuser1";
+
+        let grp1_uuid = Uuid::new_v4();
+        let grp1_name = "testgroup1";
+
+        let app1_uuid = Uuid::new_v4();
+        let app1_name = "testapp1";
+
+        let pass_app1: String;
+
+        // Setup person, group, application and app password
+        {
+            let e1 = entry_init!(
+                (Attribute::Class, EntryClass::Object.to_value()),
+                (Attribute::Class, EntryClass::Account.to_value()),
+                (Attribute::Class, EntryClass::Person.to_value()),
+                (Attribute::Name, Value::new_iname(usr_name)),
+                (Attribute::Uuid, Value::Uuid(usr_uuid)),
+                (Attribute::Description, Value::new_utf8s(usr_name)),
+                (Attribute::DisplayName, Value::new_utf8s(usr_name))
+            );
+
+            let e2 = entry_init!(
+                (Attribute::Class, EntryClass::Object.to_value()),
+                (Attribute::Class, EntryClass::Group.to_value()),
+                (Attribute::Name, Value::new_iname(grp1_name)),
+                (Attribute::Uuid, Value::Uuid(grp1_uuid)),
+                (Attribute::Member, Value::Refer(usr_uuid))
+            );
+
+            let e3 = entry_init!(
+                (Attribute::Class, EntryClass::Object.to_value()),
+                (Attribute::Class, EntryClass::ServiceAccount.to_value()),
+                (Attribute::Class, EntryClass::Application.to_value()),
+                (Attribute::Name, Value::new_iname(app1_name)),
+                (Attribute::Uuid, Value::Uuid(app1_uuid)),
+                (Attribute::LinkedGroup, Value::Refer(grp1_uuid))
+            );
+
+            let ct = duration_from_epoch_now();
+            let mut server_txn = idms.proxy_write(ct).await.unwrap();
+            assert!(server_txn
+                .qs_write
+                .internal_create(vec![e1, e2, e3])
+                .and_then(|_| server_txn.commit())
+                .is_ok());
+
+            let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
+
+            let ev = GenerateApplicationPasswordEvent::new_internal(
+                usr_uuid,
+                app1_uuid,
+                "label".to_string(),
+            );
+            pass_app1 = idms_prox_write
+                .generate_application_password(&ev)
+                .expect("Failed to generate application password");
+
+            assert!(idms_prox_write.commit().is_ok());
+        }
+
+        // Got session, app password valid
+        let res = ldaps
+            .do_bind(
+                idms,
+                format!("spn={usr_name},app={app1_name},dc=example,dc=com").as_str(),
+                pass_app1.as_str(),
+            )
+            .await;
+        assert!(res.is_ok());
+        assert!(res.unwrap().is_some());
+
+        // Any account that is not yet valid / expired can't auth.
+        // Set the valid bounds high/low
+        // TEST_VALID_FROM_TIME/TEST_EXPIRE_TIME
+        set_account_valid_time(idms, usr_uuid).await;
+
+        let time_low = Duration::from_secs(TEST_NOT_YET_VALID_TIME);
+        let time = Duration::from_secs(TEST_CURRENT_TIME);
+        let time_high = Duration::from_secs(TEST_AFTER_EXPIRY);
+
+        let mut idms_auth = idms.auth().await.unwrap();
+        let lae = LdapApplicationAuthEvent::new(app1_name, usr_uuid, pass_app1)
+            .expect("Failed to build auth event");
+
+        let r1 = idms_auth
+            .application_auth_ldap(&lae, time_low)
+            .await
+            .expect_err("Authentication succeeded");
+        assert!(r1 == OperationError::SessionExpired);
+
+        let r1 = idms_auth
+            .application_auth_ldap(&lae, time)
+            .await
+            .expect("Failed auth");
+        assert!(r1.is_some());
+
+        let r1 = idms_auth
+            .application_auth_ldap(&lae, time_high)
+            .await
+            .expect_err("Authentication succeeded");
+        assert!(r1 == OperationError::SessionExpired);
+    }
+
     macro_rules! assert_entry_contains {
         (
             $entry:expr,
diff --git a/server/lib/src/idm/mod.rs b/server/lib/src/idm/mod.rs
index ac67d6570..21c9c89b3 100644
--- a/server/lib/src/idm/mod.rs
+++ b/server/lib/src/idm/mod.rs
@@ -5,6 +5,7 @@
 
 pub mod account;
 pub(crate) mod accountpolicy;
+pub(crate) mod application;
 pub(crate) mod applinks;
 pub mod audit;
 pub(crate) mod authsession;
diff --git a/server/lib/src/idm/server.rs b/server/lib/src/idm/server.rs
index 73b516e44..f9c685523 100644
--- a/server/lib/src/idm/server.rs
+++ b/server/lib/src/idm/server.rs
@@ -25,6 +25,10 @@ use super::event::ReadBackupCodeEvent;
 use super::ldap::{LdapBoundToken, LdapSession};
 use crate::credential::{softlock::CredSoftLock, Credential};
 use crate::idm::account::Account;
+use crate::idm::application::{
+    GenerateApplicationPasswordEvent, LdapApplications, LdapApplicationsReadTransaction,
+    LdapApplicationsWriteTransaction,
+};
 use crate::idm::audit::AuditEvent;
 use crate::idm::authsession::{AuthSession, AuthSessionData};
 use crate::idm::credupdatesession::CredentialUpdateSessionMutex;
@@ -77,6 +81,7 @@ pub struct IdmServer {
     /// [Webauthn] verifier/config
     webauthn: Webauthn,
     oauth2rs: Arc<Oauth2ResourceServers>,
+    applications: Arc<LdapApplications>,
 }
 
 /// Contains methods that require writes, but in the context of writing to the idm in memory structures (maybe the query server too). This is things like authentication.
@@ -92,6 +97,7 @@ pub struct IdmServerAuthTransaction<'a> {
     pub(crate) async_tx: Sender<DelayedAction>,
     pub(crate) audit_tx: Sender<AuditEvent>,
     pub(crate) webauthn: &'a Webauthn,
+    pub(crate) applications: LdapApplicationsReadTransaction,
 }
 
 pub struct IdmServerCredUpdateTransaction<'a> {
@@ -118,6 +124,7 @@ pub struct IdmServerProxyWriteTransaction<'a> {
     crypto_policy: &'a CryptoPolicy,
     webauthn: &'a Webauthn,
     pub(crate) oauth2rs: Oauth2ResourceServersWriteTransaction<'a>,
+    pub(crate) applications: LdapApplicationsWriteTransaction<'a>,
 }
 
 pub struct IdmServerDelayed {
@@ -145,7 +152,7 @@ impl IdmServer {
         let (audit_tx, audit_rx) = unbounded();
 
         // Get the domain name, as the relying party id.
-        let (rp_id, rp_name, domain_level, oauth2rs_set) = {
+        let (rp_id, rp_name, domain_level, oauth2rs_set, application_set) = {
             let mut qs_read = qs.read().await?;
             (
                 qs_read.get_domain_name().to_string(),
@@ -153,6 +160,7 @@ impl IdmServer {
                 qs_read.get_domain_version(),
                 // Add a read/reload of all oauth2 configurations.
                 qs_read.get_oauth2rs_set()?,
+                qs_read.get_applications_set()?,
             )
         };
 
@@ -193,6 +201,11 @@ impl IdmServer {
                 e
             })?;
 
+        let applications = LdapApplications::try_from(application_set).map_err(|e| {
+            admin_error!("Failed to load ldap applications - {:?}", e);
+            e
+        })?;
+
         Ok((
             IdmServer {
                 session_ticket: Semaphore::new(1),
@@ -205,6 +218,7 @@ impl IdmServer {
                 audit_tx,
                 webauthn,
                 oauth2rs: Arc::new(oauth2rs),
+                applications: Arc::new(applications),
             },
             IdmServerDelayed { async_rx },
             IdmServerAudit { audit_rx },
@@ -228,6 +242,7 @@ impl IdmServer {
             async_tx: self.async_tx.clone(),
             audit_tx: self.audit_tx.clone(),
             webauthn: &self.webauthn,
+            applications: self.applications.read(),
         })
     }
 
@@ -260,6 +275,7 @@ impl IdmServer {
             crypto_policy: &self.crypto_policy,
             webauthn: &self.webauthn,
             oauth2rs: self.oauth2rs.write(),
+            applications: self.applications.write(),
         })
     }
 
@@ -820,6 +836,67 @@ pub trait IdmServerTransaction<'a> {
             .ok_or(OperationError::InvalidState)
     }
 
+    fn process_ldap_uuid_to_identity(
+        &mut self,
+        uuid: &Uuid,
+        ct: Duration,
+        source: Source,
+    ) -> Result<Identity, OperationError> {
+        let entry = self
+            .get_qs_txn()
+            .internal_search_uuid(*uuid)
+            .map_err(|err| {
+                error!(?err, ?uuid, "Failed to search user by uuid");
+                err
+            })?;
+
+        let (account, account_policy) =
+            Account::try_from_entry_with_policy(entry.as_ref(), self.get_qs_txn())?;
+
+        if !account.is_within_valid_time(ct) {
+            info!("Account is expired or not yet valid.");
+            return Err(OperationError::SessionExpired);
+        }
+
+        // Good to go
+        let anon_entry = if *uuid == UUID_ANONYMOUS {
+            // We already have it.
+            entry
+        } else {
+            // Pull the anon entry for mapping the identity.
+            self.get_qs_txn()
+                .internal_search_uuid(UUID_ANONYMOUS)
+                .map_err(|err| {
+                    error!(
+                        ?err,
+                        "Unable to search anonymous user for privilege bounding."
+                    );
+                    err
+                })?
+        };
+
+        let mut limits = Limits::default();
+        let session_id = Uuid::new_v4();
+
+        // Update limits from account policy
+        if let Some(max_results) = account_policy.limit_search_max_results() {
+            limits.search_max_results = max_results as usize;
+        }
+        if let Some(max_filter) = account_policy.limit_search_max_filter_test() {
+            limits.search_max_filter_test = max_filter as usize;
+        }
+
+        // Users via LDAP are always only granted anonymous rights unless
+        // they auth with an api-token
+        Ok(Identity {
+            origin: IdentType::User(IdentUser { entry: anon_entry }),
+            source,
+            session_id,
+            scope: AccessScope::ReadOnly,
+            limits,
+        })
+    }
+
     #[instrument(level = "debug", skip_all)]
     fn validate_ldap_session(
         &mut self,
@@ -828,48 +905,8 @@ pub trait IdmServerTransaction<'a> {
         ct: Duration,
     ) -> Result<Identity, OperationError> {
         match session {
-            LdapSession::UnixBind(uuid) => {
-                let anon_entry = self
-                    .get_qs_txn()
-                    .internal_search_uuid(UUID_ANONYMOUS)
-                    .map_err(|e| {
-                        admin_error!("Failed to validate ldap session -> {:?}", e);
-                        e
-                    })?;
-
-                let entry = if *uuid == UUID_ANONYMOUS {
-                    anon_entry.clone()
-                } else {
-                    self.get_qs_txn().internal_search_uuid(*uuid).map_err(|e| {
-                        admin_error!("Failed to start auth ldap -> {:?}", e);
-                        e
-                    })?
-                };
-
-                if Account::check_within_valid_time(
-                    ct,
-                    entry
-                        .get_ava_single_datetime(Attribute::AccountValidFrom)
-                        .as_ref(),
-                    entry
-                        .get_ava_single_datetime(Attribute::AccountExpire)
-                        .as_ref(),
-                ) {
-                    // Good to go
-                    let limits = Limits::default();
-                    let session_id = Uuid::new_v4();
-
-                    Ok(Identity {
-                        origin: IdentType::User(IdentUser { entry: anon_entry }),
-                        source,
-                        session_id,
-                        scope: AccessScope::ReadOnly,
-                        limits,
-                    })
-                } else {
-                    // Nope, expired
-                    Err(OperationError::SessionExpired)
-                }
+            LdapSession::UnixBind(uuid) | LdapSession::ApplicationPasswordBind(_, uuid) => {
+                self.process_ldap_uuid_to_identity(uuid, ct, source)
             }
             LdapSession::UserAuthToken(uat) => self.process_uat_to_identity(uat, ct, source),
             LdapSession::ApiToken(apit) => {
@@ -2059,6 +2096,11 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
 
     #[instrument(level = "debug", skip_all)]
     pub fn commit(mut self) -> Result<(), OperationError> {
+        if self.qs_write.get_changed_app() {
+            self.qs_write
+                .get_applications_set()
+                .and_then(|application_set| self.applications.reload(application_set))?;
+        }
         if self.qs_write.get_changed_oauth2() {
             let domain_level = self.qs_write.get_domain_version();
             self.qs_write
@@ -2069,12 +2111,54 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
         }
 
         // Commit everything.
+        self.applications.commit();
         self.oauth2rs.commit();
         self.cred_update_sessions.commit();
 
         trace!("cred_update_session.commit");
         self.qs_write.commit()
     }
+
+    #[instrument(level = "debug", skip_all)]
+    pub fn generate_application_password(
+        &mut self,
+        ev: &GenerateApplicationPasswordEvent,
+    ) -> Result<String, OperationError> {
+        let account = self.target_to_account(ev.target)?;
+
+        // This is intended to be read/copied by a human
+        let cleartext = readable_password_from_random();
+
+        // Create a modlist from the change
+        let modlist = account
+            .generate_application_password_mod(
+                ev.application,
+                ev.label.as_str(),
+                cleartext.as_str(),
+                self.crypto_policy,
+            )
+            .map_err(|e| {
+                admin_error!("Unable to generate application password mod {:?}", e);
+                e
+            })?;
+        trace!(?modlist, "processing change");
+        // Apply it
+        self.qs_write
+            .impersonate_modify(
+                // Filter as executed
+                &filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(ev.target))),
+                // Filter as intended (acp)
+                &filter_all!(f_eq(Attribute::Uuid, PartialValue::Uuid(ev.target))),
+                &modlist,
+                // Provide the event to impersonate
+                &ev.ident,
+            )
+            .map_err(|e| {
+                error!(error = ?e);
+                e
+            })
+            .map(|_| cleartext)
+    }
 }
 
 // Need tests of the sessions and the auth ...
diff --git a/server/lib/src/idm/serviceaccount.rs b/server/lib/src/idm/serviceaccount.rs
index 7f1612c6a..b915a8aa9 100644
--- a/server/lib/src/idm/serviceaccount.rs
+++ b/server/lib/src/idm/serviceaccount.rs
@@ -23,10 +23,6 @@ macro_rules! try_from_entry {
             ));
         }
 
-        let spn = $value.get_ava_single_proto_string(Attribute::Spn).ok_or(
-            OperationError::InvalidAccountState(format!("Missing attribute: {}", Attribute::Spn)),
-        )?;
-
         let jws_key = $value
             .get_ava_single_jws_key_es256(Attribute::JwsEs256PrivateKey)
             .cloned()
@@ -48,7 +44,6 @@ macro_rules! try_from_entry {
         let uuid = $value.get_uuid().clone();
 
         Ok(ServiceAccount {
-            spn,
             uuid,
             valid_from,
             expire,
@@ -59,7 +54,6 @@ macro_rules! try_from_entry {
 }
 
 pub struct ServiceAccount {
-    pub spn: String,
     pub uuid: Uuid,
 
     pub valid_from: Option<OffsetDateTime>,
diff --git a/server/lib/src/repl/consumer.rs b/server/lib/src/repl/consumer.rs
index 63265b431..aa566bc2a 100644
--- a/server/lib/src/repl/consumer.rs
+++ b/server/lib/src/repl/consumer.rs
@@ -242,6 +242,15 @@ impl<'a> QueryServerWriteTransaction<'a> {
             self.changed_flags.insert(ChangeFlag::OAUTH2)
         }
 
+        if !self.changed_flags.contains(ChangeFlag::APPLICATION)
+            && cand
+                .iter()
+                .chain(pre_cand.iter().map(|e| e.as_ref()))
+                .any(|e| e.attribute_equality(Attribute::Class, &EntryClass::Application.into()))
+        {
+            self.changed_flags.insert(ChangeFlag::APPLICATION)
+        }
+
         if !self.changed_flags.contains(ChangeFlag::SYNC_AGREEMENT)
             && cand
                 .iter()
@@ -629,6 +638,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
                 | ChangeFlag::ACP
                 | ChangeFlag::OAUTH2
                 | ChangeFlag::DOMAIN
+                | ChangeFlag::APPLICATION
                 | ChangeFlag::SYSTEM_CONFIG
                 | ChangeFlag::SYNC_AGREEMENT
                 | ChangeFlag::KEY_MATERIAL,
diff --git a/server/lib/src/repl/proto.rs b/server/lib/src/repl/proto.rs
index 681b20671..0987dc747 100644
--- a/server/lib/src/repl/proto.rs
+++ b/server/lib/src/repl/proto.rs
@@ -1,6 +1,7 @@
 use super::cid::Cid;
 use super::entry::EntryChangeState;
 use super::entry::State;
+use crate::be::dbvalue::DbValueApplicationPassword;
 use crate::be::dbvalue::DbValueCertificate;
 use crate::be::dbvalue::DbValueImage;
 use crate::be::dbvalue::DbValueKeyInternal;
@@ -443,6 +444,9 @@ pub enum ReplAttrV1 {
     Certificate {
         set: Vec<DbValueCertificate>,
     },
+    ApplicationPassword {
+        set: Vec<DbValueApplicationPassword>,
+    },
 }
 
 #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
diff --git a/server/lib/src/schema.rs b/server/lib/src/schema.rs
index 4b1fc2885..08e5384d3 100644
--- a/server/lib/src/schema.rs
+++ b/server/lib/src/schema.rs
@@ -242,6 +242,9 @@ impl SchemaAttribute {
             }
 
             SyntaxType::WebauthnAttestationCaList => false,
+            SyntaxType::ApplicationPassword => {
+                matches!(v, PartialValue::Uuid(_)) || matches!(v, PartialValue::Refer(_))
+            }
         };
         if r {
             Ok(())
@@ -305,6 +308,7 @@ impl SchemaAttribute {
                 SyntaxType::KeyInternal => matches!(v, Value::KeyInternal { .. }),
                 SyntaxType::HexString => matches!(v, Value::HexString(_)),
                 SyntaxType::Certificate => matches!(v, Value::Certificate(_)),
+                SyntaxType::ApplicationPassword => matches!(v, Value::ApplicationPassword(..)),
             };
         if r {
             Ok(())
@@ -781,7 +785,9 @@ impl<'a> SchemaWriteTransaction<'a> {
                 a.syntax == SyntaxType::OauthScopeMap ||
                 a.syntax == SyntaxType::OauthClaimMap ||
                 // So that when an rs is removed we trigger removal of the sessions.
-                a.syntax == SyntaxType::Oauth2Session
+                a.syntax == SyntaxType::Oauth2Session ||
+                // When an application is removed we trigger removal of passwords
+                a.syntax == SyntaxType::ApplicationPassword
             // May not need to be a ref type since it doesn't have external links/impact?
             // || a.syntax == SyntaxType::Session
             {
diff --git a/server/lib/src/server/batch_modify.rs b/server/lib/src/server/batch_modify.rs
index 0e48cb516..e6c04cdbf 100644
--- a/server/lib/src/server/batch_modify.rs
+++ b/server/lib/src/server/batch_modify.rs
@@ -207,6 +207,15 @@ impl<'a> QueryServerWriteTransaction<'a> {
             self.changed_flags.insert(ChangeFlag::ACP)
         }
 
+        if !self.changed_flags.contains(ChangeFlag::APPLICATION)
+            && norm_cand
+                .iter()
+                .chain(pre_candidates.iter().map(|e| e.as_ref()))
+                .any(|e| e.attribute_equality(Attribute::Class, &EntryClass::Application.into()))
+        {
+            self.changed_flags.insert(ChangeFlag::APPLICATION)
+        }
+
         if !self.changed_flags.contains(ChangeFlag::OAUTH2)
             && norm_cand
                 .iter()
diff --git a/server/lib/src/server/create.rs b/server/lib/src/server/create.rs
index e462d1775..4511d0970 100644
--- a/server/lib/src/server/create.rs
+++ b/server/lib/src/server/create.rs
@@ -112,6 +112,15 @@ impl<'a> QueryServerWriteTransaction<'a> {
         {
             self.changed_flags.insert(ChangeFlag::ACP)
         }
+
+        if !self.changed_flags.contains(ChangeFlag::APPLICATION)
+            && commit_cand
+                .iter()
+                .any(|e| e.attribute_equality(Attribute::Class, &EntryClass::Application.into()))
+        {
+            self.changed_flags.insert(ChangeFlag::APPLICATION)
+        }
+
         if !self.changed_flags.contains(ChangeFlag::OAUTH2)
             && commit_cand.iter().any(|e| {
                 e.attribute_equality(Attribute::Class, &EntryClass::OAuth2ResourceServer.into())
diff --git a/server/lib/src/server/delete.rs b/server/lib/src/server/delete.rs
index 785ef697e..0b25d9c69 100644
--- a/server/lib/src/server/delete.rs
+++ b/server/lib/src/server/delete.rs
@@ -115,6 +115,15 @@ impl<'a> QueryServerWriteTransaction<'a> {
         {
             self.changed_flags.insert(ChangeFlag::ACP)
         }
+
+        if !self.changed_flags.contains(ChangeFlag::APPLICATION)
+            && del_cand
+                .iter()
+                .any(|e| e.attribute_equality(Attribute::Class, &EntryClass::Application.into()))
+        {
+            self.changed_flags.insert(ChangeFlag::APPLICATION)
+        }
+
         if !self.changed_flags.contains(ChangeFlag::OAUTH2)
             && del_cand.iter().any(|e| {
                 e.attribute_equality(Attribute::Class, &EntryClass::OAuth2ResourceServer.into())
diff --git a/server/lib/src/server/migrations.rs b/server/lib/src/server/migrations.rs
index 020cbc944..e7fca902c 100644
--- a/server/lib/src/server/migrations.rs
+++ b/server/lib/src/server/migrations.rs
@@ -594,6 +594,39 @@ impl<'a> QueryServerWriteTransaction<'a> {
 
         // =========== Apply changes ==============
 
+        let idm_schema_classes = [
+            SCHEMA_ATTR_LINKED_GROUP_DL8.clone().into(),
+            SCHEMA_ATTR_APPLICATION_PASSWORD_DL8.clone().into(),
+            SCHEMA_CLASS_APPLICATION_DL8.clone().into(),
+            SCHEMA_CLASS_PERSON_DL8.clone().into(),
+        ];
+
+        idm_schema_classes
+            .into_iter()
+            .try_for_each(|entry| self.internal_migrate_or_create(entry))
+            .map_err(|err| {
+                error!(?err, "migrate_domain_6_to_7 -> Error");
+                err
+            })?;
+
+        self.reload()?;
+
+        // Update access controls.
+        let idm_data = [
+            IDM_ACP_SELF_READ_DL8.clone().into(),
+            IDM_ACP_SELF_WRITE_DL8.clone().into(),
+            IDM_ACP_APPLICATION_MANAGE_DL8.clone().into(),
+            IDM_ACP_APPLICATION_ENTRY_MANAGER_DL8.clone().into(),
+        ];
+
+        idm_data
+            .into_iter()
+            .try_for_each(|entry| self.internal_migrate_or_create(entry))
+            .map_err(|err| {
+                error!(?err, "migrate_domain_6_to_7 -> Error");
+                err
+            })?;
+
         Ok(())
     }
 
@@ -888,12 +921,12 @@ impl<'a> QueryServerWriteTransaction<'a> {
             IDM_ACP_SERVICE_ACCOUNT_MANAGE_V1.clone(),
             // DL4
             // DL5
-            IDM_ACP_OAUTH2_MANAGE_DL5.clone().into(),
+            IDM_ACP_OAUTH2_MANAGE_DL5.clone(),
             // DL6
-            IDM_ACP_GROUP_ACCOUNT_POLICY_MANAGE_DL6.clone().into(),
-            IDM_ACP_PEOPLE_CREATE_DL6.clone().into(),
-            IDM_ACP_GROUP_MANAGE_DL6.clone().into(),
-            IDM_ACP_ACCOUNT_MAIL_READ_DL6.clone().into(),
+            IDM_ACP_GROUP_ACCOUNT_POLICY_MANAGE_DL6.clone(),
+            IDM_ACP_PEOPLE_CREATE_DL6.clone(),
+            IDM_ACP_GROUP_MANAGE_DL6.clone(),
+            IDM_ACP_ACCOUNT_MAIL_READ_DL6.clone(),
             IDM_ACP_DOMAIN_ADMIN_DL6.clone(),
         ];
 
diff --git a/server/lib/src/server/mod.rs b/server/lib/src/server/mod.rs
index 423da840e..06d030175 100644
--- a/server/lib/src/server/mod.rs
+++ b/server/lib/src/server/mod.rs
@@ -131,6 +131,7 @@ bitflags::bitflags! {
         const SYSTEM_CONFIG =  0b0001_0000;
         const SYNC_AGREEMENT = 0b0010_0000;
         const KEY_MATERIAL   = 0b0100_0000;
+        const APPLICATION    = 0b1000_0000;
     }
 }
 
@@ -664,6 +665,7 @@ pub trait QueryServerTransaction<'a> {
                         .ok_or_else(|| OperationError::InvalidAttribute("Invalid hex string syntax".to_string())),
                     SyntaxType::Certificate => Value::new_certificate_s(value)
                         .ok_or_else(|| OperationError::InvalidAttribute("Invalid x509 certificate syntax".to_string())),
+                    SyntaxType::ApplicationPassword => Err(OperationError::InvalidAttribute("ApplicationPassword values can not be supplied through modification".to_string())),
                 }
             }
             None => {
@@ -719,7 +721,8 @@ pub trait QueryServerTransaction<'a> {
                     | SyntaxType::OauthScopeMap
                     | SyntaxType::Session
                     | SyntaxType::ApiToken
-                    | SyntaxType::Oauth2Session => {
+                    | SyntaxType::Oauth2Session
+                    | SyntaxType::ApplicationPassword => {
                         let un = self.name_to_uuid(value).unwrap_or(UUID_DOES_NOT_EXIST);
                         Ok(PartialValue::Refer(un))
                     }
@@ -978,6 +981,13 @@ pub trait QueryServerTransaction<'a> {
         )))
     }
 
+    fn get_applications_set(&mut self) -> Result<Vec<Arc<EntrySealedCommitted>>, OperationError> {
+        self.internal_search(filter!(f_eq(
+            Attribute::Class,
+            EntryClass::Application.into(),
+        )))
+    }
+
     #[instrument(level = "debug", skip_all)]
     fn consumer_get_state(&mut self) -> Result<ReplRuvRange, OperationError> {
         // Get the current state of "where we are up to"
@@ -1261,9 +1271,7 @@ impl QueryServer {
             let s_uuid = wr.get_db_s_uuid()?;
             let d_uuid = wr.get_db_d_uuid()?;
             let ts_max = wr.get_db_ts_max(curtime)?;
-            #[allow(clippy::expect_used)]
-            wr.commit()
-                .expect("Critical - unable to commit db_s_uuid or db_d_uuid");
+            wr.commit()?;
             (s_uuid, d_uuid, ts_max)
         };
 
@@ -1298,13 +1306,15 @@ impl QueryServer {
 
         let phase = Arc::new(CowCell::new(ServerPhase::Bootstrap));
 
-        #[allow(clippy::expect_used)]
         let resolve_filter_cache = Arc::new(
             ARCacheBuilder::new()
                 .set_size(RESOLVE_FILTER_CACHE_MAX, RESOLVE_FILTER_CACHE_LOCAL)
                 .set_reader_quiesce(true)
                 .build()
-                .expect("Failed to build resolve_filter_cache"),
+                .ok_or_else(|| {
+                    error!("Failed to build filter resolve cache");
+                    OperationError::DB0003FilterResolveCacheBuild
+                })?,
         );
 
         let key_providers = Arc::new(KeyProviders::default());
@@ -1343,10 +1353,12 @@ impl QueryServer {
         // us from competing with writers on the db tickets. This tilts us to write prioritising
         // on db operations by always making sure a writer can get a db ticket.
         let read_ticket = if cfg!(test) {
-            #[allow(clippy::expect_used)]
             self.read_tickets
                 .try_acquire()
-                .expect("unable to acquire db_ticket for qsr")
+                .inspect_err(|err| {
+                    error!(?err, "Unable to acquire read ticket!");
+                })
+                .ok()?
         } else {
             let fut = tokio::time::timeout(
                 Duration::from_millis(DB_LOCK_ACQUIRE_TIMEOUT_MILLIS),
@@ -1371,16 +1383,20 @@ impl QueryServer {
         // and read ticket holders, OR pool_size == 1, and we are waiting on the writer to now
         // complete.
         let db_ticket = if cfg!(test) {
-            #[allow(clippy::expect_used)]
             self.db_tickets
                 .try_acquire()
-                .expect("unable to acquire db_ticket for qsr")
+                .inspect_err(|err| {
+                    error!(?err, "Unable to acquire database ticket!");
+                })
+                .ok()?
         } else {
-            #[allow(clippy::expect_used)]
             self.db_tickets
                 .acquire()
                 .await
-                .expect("unable to acquire db_ticket for qsr")
+                .inspect_err(|err| {
+                    error!(?err, "Unable to acquire database ticket!");
+                })
+                .ok()?
         };
 
         Some((read_ticket, db_ticket))
@@ -1397,16 +1413,12 @@ impl QueryServer {
         let schema = self.schema.read();
 
         let cid_max = self.cid_max.read();
-        #[allow(clippy::expect_used)]
-        let trim_cid = cid_max
-            .sub_secs(CHANGELOG_MAX_AGE)
-            .expect("unable to generate trim cid");
+        let trim_cid = cid_max.sub_secs(CHANGELOG_MAX_AGE)?;
+
+        let be_txn = self.be.read()?;
 
         Ok(QueryServerReadTransaction {
-            be_txn: self
-                .be
-                .read()
-                .expect("unable to create backend read transaction"),
+            be_txn,
             schema,
             d_info: self.d_info.read(),
             system_config: self.system_config.read(),
@@ -1422,11 +1434,13 @@ impl QueryServer {
     #[instrument(level = "debug", skip_all)]
     async fn write_acquire_ticket(&self) -> Option<(SemaphorePermit<'_>, SemaphorePermit<'_>)> {
         // Guarantee we are the only writer on the thread pool
-        #[allow(clippy::expect_used)]
         let write_ticket = if cfg!(test) {
             self.write_ticket
                 .try_acquire()
-                .expect("unable to acquire writer_ticket for qsw")
+                .inspect_err(|err| {
+                    error!(?err, "Unable to acquire write ticket!");
+                })
+                .ok()?
         } else {
             let fut = tokio::time::timeout(
                 Duration::from_millis(DB_LOCK_ACQUIRE_TIMEOUT_MILLIS),
@@ -1450,16 +1464,20 @@ impl QueryServer {
         // *must* be available because pool_size >= 2 and the only other are readers, or
         // pool_size == 1 and we are waiting on a single reader to now complete
         let db_ticket = if cfg!(test) {
-            #[allow(clippy::expect_used)]
             self.db_tickets
                 .try_acquire()
-                .expect("unable to acquire db_ticket for qsw")
+                .inspect_err(|err| {
+                    error!(?err, "Unable to acquire write db_ticket!");
+                })
+                .ok()?
         } else {
-            #[allow(clippy::expect_used)]
             self.db_tickets
                 .acquire()
                 .await
-                .expect("unable to acquire db_ticket for qsw")
+                .inspect_err(|err| {
+                    error!(?err, "Unable to acquire write db_ticket!");
+                })
+                .ok()?
         };
 
         Some((write_ticket, db_ticket))
@@ -1489,10 +1507,7 @@ impl QueryServer {
         // Update the cid now.
         *cid = Cid::new_lamport(cid.s_uuid, curtime, &cid.ts);
 
-        #[allow(clippy::expect_used)]
-        let trim_cid = cid
-            .sub_secs(CHANGELOG_MAX_AGE)
-            .expect("unable to generate trim cid");
+        let trim_cid = cid.sub_secs(CHANGELOG_MAX_AGE)?;
 
         Ok(QueryServerWriteTransaction {
             // I think this is *not* needed, because commit is mut self which should
@@ -2060,6 +2075,11 @@ impl<'a> QueryServerWriteTransaction<'a> {
         self.be_txn.upgrade_reindex(v)
     }
 
+    #[inline]
+    pub(crate) fn get_changed_app(&self) -> bool {
+        self.changed_flags.contains(ChangeFlag::APPLICATION)
+    }
+
     #[inline]
     pub(crate) fn get_changed_oauth2(&self) -> bool {
         self.changed_flags.contains(ChangeFlag::OAUTH2)
diff --git a/server/lib/src/server/modify.rs b/server/lib/src/server/modify.rs
index 68b5093e9..9f370f44b 100644
--- a/server/lib/src/server/modify.rs
+++ b/server/lib/src/server/modify.rs
@@ -214,6 +214,15 @@ impl<'a> QueryServerWriteTransaction<'a> {
             self.changed_flags.insert(ChangeFlag::ACP)
         }
 
+        if !self.changed_flags.contains(ChangeFlag::APPLICATION)
+            && norm_cand
+                .iter()
+                .chain(pre_candidates.iter().map(|e| e.as_ref()))
+                .any(|e| e.attribute_equality(Attribute::Class, &EntryClass::Application.into()))
+        {
+            self.changed_flags.insert(ChangeFlag::APPLICATION)
+        }
+
         if !self.changed_flags.contains(ChangeFlag::OAUTH2)
             && norm_cand
                 .iter()
@@ -415,6 +424,16 @@ impl<'a> QueryServerWriteTransaction<'a> {
         {
             self.changed_flags.insert(ChangeFlag::ACP)
         }
+
+        if !self.changed_flags.contains(ChangeFlag::APPLICATION)
+            && norm_cand
+                .iter()
+                .chain(pre_candidates.iter().map(|e| e.as_ref()))
+                .any(|e| e.attribute_equality(Attribute::Class, &EntryClass::Application.into()))
+        {
+            self.changed_flags.insert(ChangeFlag::APPLICATION)
+        }
+
         if !self.changed_flags.contains(ChangeFlag::OAUTH2)
             && norm_cand.iter().any(|e| {
                 e.attribute_equality(Attribute::Class, &EntryClass::OAuth2ResourceServer.into())
diff --git a/server/lib/src/value.rs b/server/lib/src/value.rs
index 8201855ad..bb590022e 100644
--- a/server/lib/src/value.rs
+++ b/server/lib/src/value.rs
@@ -34,7 +34,7 @@ use webauthn_rs::prelude::{
 
 use crate::be::dbentry::DbIdentSpn;
 use crate::be::dbvalue::DbValueOauthClaimMapJoinV1;
-use crate::credential::{totp::Totp, Credential};
+use crate::credential::{apppwd::ApplicationPassword, totp::Totp, Credential};
 use crate::prelude::*;
 use crate::repl::cid::Cid;
 use crate::server::identity::IdentityId;
@@ -276,6 +276,7 @@ pub enum SyntaxType {
     KeyInternal = 38,
     HexString = 39,
     Certificate = 40,
+    ApplicationPassword = 41,
 }
 
 impl TryFrom<&str> for SyntaxType {
@@ -325,6 +326,7 @@ impl TryFrom<&str> for SyntaxType {
             "KEY_INTERNAL" => Ok(SyntaxType::KeyInternal),
             "HEX_STRING" => Ok(SyntaxType::HexString),
             "CERTIFICATE" => Ok(SyntaxType::Certificate),
+            "APPLICATION_PASSWORD" => Ok(SyntaxType::ApplicationPassword),
             _ => Err(()),
         }
     }
@@ -374,6 +376,7 @@ impl fmt::Display for SyntaxType {
             SyntaxType::KeyInternal => "KEY_INTERNAL",
             SyntaxType::HexString => "HEX_STRING",
             SyntaxType::Certificate => "CERTIFICATE",
+            SyntaxType::ApplicationPassword => "APPLICATION_PASSWORD",
         })
     }
 }
@@ -1227,6 +1230,7 @@ pub enum Value {
     HexString(String),
 
     Certificate(Box<Certificate>),
+    ApplicationPassword(ApplicationPassword),
 }
 
 impl PartialEq for Value {
@@ -2041,6 +2045,10 @@ impl Value {
             Value::AuditLogString(_, s) => {
                 Value::validate_str_escapes(s) && Value::validate_singleline(s)
             }
+            Value::ApplicationPassword(ap) => {
+                Value::validate_str_escapes(&ap.label) && Value::validate_singleline(&ap.label)
+            }
+
             // These have stricter validators so not needed.
             Value::Nsuniqueid(s) => NSUNIQUEID_RE.is_match(s),
             Value::DateTime(odt) => odt.offset() == time::UtcOffset::UTC,
diff --git a/server/lib/src/valueset/apppwd.rs b/server/lib/src/valueset/apppwd.rs
new file mode 100644
index 000000000..87a152b8e
--- /dev/null
+++ b/server/lib/src/valueset/apppwd.rs
@@ -0,0 +1,319 @@
+use crate::be::dbvalue::{DbValueApplicationPassword, DbValueSetV2};
+use crate::credential::{apppwd::ApplicationPassword, Password};
+use crate::prelude::*;
+use crate::repl::proto::ReplAttrV1;
+use crate::schema::SchemaAttribute;
+use std::collections::BTreeMap;
+
+#[derive(Debug, Clone)]
+pub struct ValueSetApplicationPassword {
+    // The map key is application's UUID
+    // The value is a vector instead of BTreeSet to use
+    // PartialValue::Refer instead of having to implement
+    // PartialValue::ApplicationPassword. For example
+    // btreeset.remove takes a full ApplicationPassword
+    // struct.
+    map: BTreeMap<Uuid, Vec<ApplicationPassword>>,
+}
+
+impl ValueSetApplicationPassword {
+    pub fn new(ap: ApplicationPassword) -> Box<Self> {
+        let mut map: BTreeMap<Uuid, Vec<ApplicationPassword>> = BTreeMap::new();
+        map.entry(ap.application).or_default().push(ap);
+        Box::new(ValueSetApplicationPassword { map })
+    }
+
+    fn from_dbv_iter(
+        data: impl Iterator<Item = DbValueApplicationPassword>,
+    ) -> Result<ValueSet, OperationError> {
+        let mut map: BTreeMap<Uuid, Vec<ApplicationPassword>> = BTreeMap::new();
+        for ap in data {
+            let ap = match ap {
+                DbValueApplicationPassword::V1 {
+                    refer,
+                    application_refer,
+                    label,
+                    password,
+                } => {
+                    let password = Password::try_from(password)
+                        .map_err(|()| OperationError::InvalidValueState)?;
+                    ApplicationPassword {
+                        uuid: refer,
+                        application: application_refer,
+                        label,
+                        password,
+                    }
+                }
+            };
+            map.entry(ap.application).or_default().push(ap);
+        }
+        Ok(Box::new(ValueSetApplicationPassword { map }))
+    }
+
+    pub fn from_dbvs2(data: Vec<DbValueApplicationPassword>) -> Result<ValueSet, OperationError> {
+        Self::from_dbv_iter(data.into_iter())
+    }
+
+    pub fn from_repl_v1(data: &[DbValueApplicationPassword]) -> Result<ValueSet, OperationError> {
+        Self::from_dbv_iter(data.iter().cloned())
+    }
+
+    fn to_vec_dbvs(&self) -> Vec<DbValueApplicationPassword> {
+        self.map
+            .iter()
+            .flat_map(|(_, v)| {
+                v.iter().map(|ap| DbValueApplicationPassword::V1 {
+                    refer: ap.uuid,
+                    application_refer: ap.application,
+                    label: ap.label.clone(),
+                    password: ap.password.to_dbpasswordv1(),
+                })
+            })
+            .collect()
+    }
+}
+
+impl ValueSetT for ValueSetApplicationPassword {
+    fn insert_checked(&mut self, value: Value) -> Result<bool, OperationError> {
+        match value {
+            Value::ApplicationPassword(ap) => {
+                let application_entries = self.map.entry(ap.application).or_default();
+
+                if let Some(application_entry) = application_entries
+                    .iter_mut()
+                    .find(|entry_app_password| *entry_app_password == &ap)
+                {
+                    // Overwrite on duplicated labels for the same application.
+                    application_entry.password = ap.password;
+                } else {
+                    // Or just add it.
+                    application_entries.push(ap);
+                }
+                Ok(true)
+            }
+            _ => Err(OperationError::InvalidValueState),
+        }
+    }
+
+    fn clear(&mut self) {
+        self.map.clear();
+    }
+
+    fn remove(&mut self, pv: &PartialValue, _cid: &Cid) -> bool {
+        match pv {
+            PartialValue::Refer(u) => {
+                // Deletes all passwords for the referred application
+                self.map.remove(u).is_some()
+            }
+            PartialValue::Uuid(u) => {
+                // Delete specific application password
+                // TODO Migrate to extract_if when available
+                let mut removed = false;
+                self.map.retain(|_, v| {
+                    let prev = v.len();
+                    // Check the innel vec of passwords related to this application.
+                    v.retain(|y| y.uuid != *u);
+                    let post = v.len();
+                    removed |= post < prev;
+                    // Is the apppwd set for this application id now empty?
+                    !v.is_empty()
+                });
+                removed
+            }
+            _ => false,
+        }
+    }
+
+    fn contains(&self, pv: &PartialValue) -> bool {
+        match pv {
+            PartialValue::Uuid(u) => self.map.values().any(|v| v.iter().any(|ap| ap.uuid == *u)),
+            PartialValue::Refer(u) => self
+                .map
+                .values()
+                .any(|v| v.iter().any(|ap| ap.application == *u)),
+            _ => false,
+        }
+    }
+
+    fn substring(&self, _pv: &PartialValue) -> bool {
+        false
+    }
+
+    fn startswith(&self, _pv: &PartialValue) -> bool {
+        false
+    }
+
+    fn endswith(&self, _pv: &PartialValue) -> bool {
+        false
+    }
+
+    fn lessthan(&self, _pv: &PartialValue) -> bool {
+        false
+    }
+
+    fn len(&self) -> usize {
+        let mut count = 0;
+        for v in self.map.values() {
+            count += v.len();
+        }
+        count
+    }
+
+    fn generate_idx_eq_keys(&self) -> Vec<String> {
+        self.map
+            .keys()
+            .map(|u| u.as_hyphenated().to_string())
+            .collect()
+    }
+
+    fn syntax(&self) -> SyntaxType {
+        SyntaxType::ApplicationPassword
+    }
+
+    fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
+        self.map.iter().all(|(_, v)| {
+            v.iter().all(|ap| {
+                Value::validate_str_escapes(ap.label.as_str())
+                    && Value::validate_singleline(ap.label.as_str())
+            })
+        })
+    }
+
+    fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
+        Box::new(self.map.iter().flat_map(|(_, v)| {
+            v.iter()
+                .map(|ap| format!("App: {} Label: {}", ap.application, ap.label))
+        }))
+    }
+
+    fn to_db_valueset_v2(&self) -> DbValueSetV2 {
+        let data = self.to_vec_dbvs();
+        DbValueSetV2::ApplicationPassword(data)
+    }
+
+    fn to_repl_v1(&self) -> ReplAttrV1 {
+        let set = self.to_vec_dbvs();
+        ReplAttrV1::ApplicationPassword { set }
+    }
+
+    fn to_partialvalue_iter(&self) -> Box<dyn Iterator<Item = PartialValue> + '_> {
+        Box::new(
+            self.map
+                .iter()
+                .flat_map(|(_, v)| v.iter().map(|ap| ap.uuid))
+                .map(PartialValue::Refer),
+        )
+    }
+
+    fn to_value_iter(&self) -> Box<dyn Iterator<Item = Value> + '_> {
+        Box::new(
+            self.map
+                .iter()
+                .flat_map(|(_, v)| v.iter().map(|ap| Value::ApplicationPassword(ap.clone()))),
+        )
+    }
+
+    fn equal(&self, other: &ValueSet) -> bool {
+        if let Some(other) = other.as_application_password_map() {
+            &self.map == other
+        } else {
+            debug_assert!(false);
+            false
+        }
+    }
+
+    fn merge(&mut self, other: &ValueSet) -> Result<(), OperationError> {
+        if let Some(b) = other.as_application_password_map() {
+            mergemaps!(self.map, b)
+        } else {
+            debug_assert!(false);
+            Err(OperationError::InvalidValueState)
+        }
+    }
+
+    fn as_application_password_map(&self) -> Option<&BTreeMap<Uuid, Vec<ApplicationPassword>>> {
+        Some(&self.map)
+    }
+
+    fn as_ref_uuid_iter(&self) -> Option<Box<dyn Iterator<Item = Uuid> + '_>> {
+        // This is what ties us as a type that can be refint checked.
+        Some(Box::new(self.map.keys().copied()))
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::credential::{apppwd::ApplicationPassword, Password};
+    use crate::prelude::*;
+    use crate::valueset::ValueSetApplicationPassword;
+    use kanidm_lib_crypto::CryptoPolicy;
+
+    // Test the remove operation, removing all application passwords for an
+    // applicaiton should also remove the KV pair.
+    #[test]
+    fn test_valueset_application_password_remove() {
+        let app1_uuid = Uuid::new_v4();
+        let app2_uuid = Uuid::new_v4();
+        let ap1_uuid = Uuid::new_v4();
+        let ap2_uuid = Uuid::new_v4();
+        let ap3_uuid = Uuid::new_v4();
+
+        let ap1: ApplicationPassword = ApplicationPassword {
+            uuid: ap1_uuid,
+            application: app1_uuid,
+            label: "apppwd1".to_string(),
+            password: Password::new_pbkdf2(&CryptoPolicy::minimum(), "apppwd1")
+                .expect("Failed to create password"),
+        };
+
+        let ap2: ApplicationPassword = ApplicationPassword {
+            uuid: ap2_uuid,
+            application: app1_uuid,
+            label: "apppwd2".to_string(),
+            password: Password::new_pbkdf2(&CryptoPolicy::minimum(), "apppwd2")
+                .expect("Failed to create password"),
+        };
+
+        let ap3: ApplicationPassword = ApplicationPassword {
+            uuid: ap3_uuid,
+            application: app2_uuid,
+            label: "apppwd3".to_string(),
+            password: Password::new_pbkdf2(&CryptoPolicy::minimum(), "apppwd3")
+                .expect("Failed to create password"),
+        };
+
+        let mut vs: ValueSet = ValueSetApplicationPassword::new(ap1);
+        assert!(vs.len() == 1);
+
+        let res = vs
+            .insert_checked(Value::ApplicationPassword(ap2))
+            .expect("Failed to insert");
+        assert!(res);
+        assert!(vs.len() == 2);
+
+        let res = vs
+            .insert_checked(Value::ApplicationPassword(ap3))
+            .expect("Failed to insert");
+        assert!(res);
+        assert!(vs.len() == 3);
+
+        let res = vs.remove(&PartialValue::Uuid(Uuid::new_v4()), &Cid::new_zero());
+        assert!(!res);
+        assert!(vs.len() == 3);
+
+        let res = vs.remove(&PartialValue::Uuid(ap1_uuid), &Cid::new_zero());
+        assert!(res);
+        assert!(vs.len() == 2);
+
+        let res = vs.remove(&PartialValue::Uuid(ap3_uuid), &Cid::new_zero());
+        assert!(res);
+        assert!(vs.len() == 1);
+
+        let res = vs.remove(&PartialValue::Uuid(ap2_uuid), &Cid::new_zero());
+        assert!(res);
+        assert!(vs.len() == 0);
+
+        let res = vs.as_application_password_map().unwrap();
+        assert!(res.keys().len() == 0);
+    }
+}
diff --git a/server/lib/src/valueset/mod.rs b/server/lib/src/valueset/mod.rs
index c66a125a3..edc7b9ba7 100644
--- a/server/lib/src/valueset/mod.rs
+++ b/server/lib/src/valueset/mod.rs
@@ -18,7 +18,7 @@ use webauthn_rs::prelude::Passkey as PasskeyV4;
 use kanidm_proto::internal::{Filter as ProtoFilter, UiHint};
 
 use crate::be::dbvalue::DbValueSetV2;
-use crate::credential::{totp::Totp, Credential};
+use crate::credential::{apppwd::ApplicationPassword, totp::Totp, Credential};
 use crate::prelude::*;
 use crate::repl::{cid::Cid, proto::ReplAttrV1};
 use crate::schema::SchemaAttribute;
@@ -26,6 +26,7 @@ use crate::server::keys::KeyId;
 use crate::value::{Address, ApiToken, CredentialType, IntentTokenState, Oauth2Session, Session};
 
 pub use self::address::{ValueSetAddress, ValueSetEmailAddress};
+use self::apppwd::ValueSetApplicationPassword;
 pub use self::auditlogstring::{ValueSetAuditLogString, AUDIT_LOG_STRING_CAPACITY};
 pub use self::binary::{ValueSetPrivateBinary, ValueSetPublicBinary};
 pub use self::bool::ValueSetBool;
@@ -63,6 +64,7 @@ pub use self::utf8::ValueSetUtf8;
 pub use self::uuid::{ValueSetRefer, ValueSetUuid};
 
 mod address;
+mod apppwd;
 mod auditlogstring;
 mod binary;
 mod bool;
@@ -392,6 +394,11 @@ pub trait ValueSetT: std::fmt::Debug + DynClone {
         None
     }
 
+    fn as_application_password_map(&self) -> Option<&BTreeMap<Uuid, Vec<ApplicationPassword>>> {
+        debug_assert!(false);
+        None
+    }
+
     fn to_value_single(&self) -> Option<Value> {
         if self.len() != 1 {
             None
@@ -708,6 +715,7 @@ pub fn from_result_value_iter(
         Value::Certificate(c) => ValueSetCertificate::new(c)?,
         Value::WebauthnAttestationCaList(_)
         | Value::PhoneNumber(_, _)
+        | Value::ApplicationPassword(_)
         | Value::Passkey(_, _, _)
         | Value::AttestedPasskey(_, _, _)
         | Value::TotpSecret(_, _)
@@ -802,6 +810,7 @@ pub fn from_value_iter(mut iter: impl Iterator<Item = Value>) -> Result<ValueSet
             debug_assert!(false);
             return Err(OperationError::InvalidValueState);
         }
+        Value::ApplicationPassword(ap) => ValueSetApplicationPassword::new(ap),
     };
 
     for v in iter {
@@ -862,6 +871,7 @@ pub fn from_db_valueset_v2(dbvs: DbValueSetV2) -> Result<ValueSet, OperationErro
         DbValueSetV2::KeyInternal(set) => ValueSetKeyInternal::from_dbvs2(set),
         DbValueSetV2::HexString(set) => ValueSetHexString::from_dbvs2(set),
         DbValueSetV2::Certificate(set) => ValueSetCertificate::from_dbvs2(set),
+        DbValueSetV2::ApplicationPassword(set) => ValueSetApplicationPassword::from_dbvs2(set),
     }
 }
 
@@ -915,5 +925,6 @@ pub fn from_repl_v1(rv1: &ReplAttrV1) -> Result<ValueSet, OperationError> {
         ReplAttrV1::KeyInternal { set } => ValueSetKeyInternal::from_repl_v1(set),
         ReplAttrV1::HexString { set } => ValueSetHexString::from_repl_v1(set),
         ReplAttrV1::Certificate { set } => ValueSetCertificate::from_repl_v1(set),
+        ReplAttrV1::ApplicationPassword { set } => ValueSetApplicationPassword::from_repl_v1(set),
     }
 }