diff --git a/README-servicedelegationtarget.md b/README-servicedelegationtarget.md
new file mode 100644
index 0000000000000000000000000000000000000000..b6da5e866ede3e0064bdfdd4489b85d1f923afa3
--- /dev/null
+++ b/README-servicedelegationtarget.md
@@ -0,0 +1,133 @@
+Servicedelegationtarget module
+============
+
+Description
+-----------
+
+The servicedelegationtarget module allows to ensure presence and absence of servicedelegationtargets and servicedelegationtarget members.
+
+Features
+--------
+
+* Servicedelegationtarget management
+
+
+Supported FreeIPA Versions
+--------------------------
+
+FreeIPA versions 4.4.0 and up are supported by the ipaservicedelegationtarget module.
+
+Host princpals are only usable with IPA versions 4.9.0 and up.
+
+
+Requirements
+------------
+
+**Controller**
+* Ansible version: 2.8+
+
+**Node**
+* Supported FreeIPA version (see above)
+
+
+Usage
+=====
+
+Example inventory file
+
+```ini
+[ipaserver]
+ipaserver.test.local
+```
+
+
+Example playbook to make sure servicedelegationtarget delegation-target is present:
+
+```yaml
+---
+- name: Playbook to manage IPA servicedelegationtarget
+  hosts: ipaserver
+  become: no
+
+  tasks:
+  - name: Ensure servicedelegationtarget delegation-target is present
+    ipaservicedelegationtarget:
+      ipaadmin_password: SomeADMINpassword
+      name: delegation-target
+```
+
+
+Example playbook to make sure servicedelegationtarget delegation-target member principal test/example.com is present:
+
+```yaml
+---
+- name: Playbook to manage IPA servicedelegationtarget
+  hosts: ipaserver
+  become: no
+
+  tasks:
+  - name: Ensure servicedelegationtarget delegation-target member principal test/example.com is present
+    ipaservicedelegationtarget:
+      ipaadmin_password: SomeADMINpassword
+      name: delegation-target
+      principal: test/example.com
+      action: member
+```
+
+
+Example playbook to make sure servicedelegationtarget delegation-target member principal test/example.com is absent:
+
+```yaml
+---
+- name: Playbook to manage IPA servicedelegationtarget
+  hosts: ipaserver
+  become: no
+
+  tasks:
+  - name: Ensure servicedelegationtarget delegation-target member principal test/example.com is absent
+    ipaservicedelegationtarget:
+      ipaadmin_password: SomeADMINpassword
+      name: delegation-target
+      principal: test/example.com
+      action: member
+      state: absent
+    state: absent
+```
+
+
+Example playbook to make sure servicedelegationtarget delegation-target is absent:
+
+```yaml
+---
+- name: Playbook to manage IPA servicedelegationtarget
+  hosts: ipaserver
+  become: no
+
+  tasks:
+  - name: Ensure servicedelegationtarget delegation-target is absent
+    ipaservicedelegationtarget:
+      ipaadmin_password: SomeADMINpassword
+      name: delegation-target
+      state: absent
+```
+
+
+Variables
+---------
+
+Variable | Description | Required
+-------- | ----------- | --------
+`ipaadmin_principal` | The admin principal is a string and defaults to `admin` | no
+`ipaadmin_password` | The admin password is a string and is required if there is no admin ticket available on the node | no
+`ipaapi_context` | The context in which the module will execute. Executing in a server context is preferred. If not provided context will be determined by the execution environment. Valid values are `server` and `client`. | no
+`ipaapi_ldap_cache` | Use LDAP cache for IPA connection. The bool setting defaults to yes. (bool) | no
+`name` \| `cn` | The list of servicedelegationtarget name strings. | yes
+`principal` |  The list of principals. A principal can be of the format: fqdn, fqdn@REALM, service/fqdn, service/fqdn@REALM, host/fqdn, host/fqdn@REALM, alias$, alias$@REALM, where fqdn and fqdn@REALM are host principals and the same as host/fqdn and host/fqdn@REALM. Host princpals are only usable with IPA versions 4.9.0 and up. | no
+`action` | Work on servicedelegationtarget or member level. It can be on of `member` or `servicedelegationtarget` and defaults to `servicedelegationtarget`. | no
+`state` | The state to ensure. It can be one of `present`, `absent`, default: `present`. | no
+
+
+Authors
+=======
+
+Thomas Woerner
diff --git a/README.md b/README.md
index 72f8b58cdc91b17f06810d8cd3587bb15488c33f..a50b7d82bffa9322044079ce37ed0e3112a2b515 100644
--- a/README.md
+++ b/README.md
@@ -36,6 +36,7 @@ Features
 * Modules for self service management
 * Modules for server management
 * Modules for service management
+* Modules for service delegation target management
 * Modules for sudocmd management
 * Modules for sudocmdgroup management
 * Modules for sudorule management
@@ -450,6 +451,7 @@ Modules in plugin/modules
 * [ipaselfservice](README-selfservice.md)
 * [ipaserver](README-server.md)
 * [ipaservice](README-service.md)
+* [ipaservicedelegationtarget](README-servicedelegationtarget.md)
 * [ipasudocmd](README-sudocmd.md)
 * [ipasudocmdgroup](README-sudocmdgroup.md)
 * [ipasudorule](README-sudorule.md)
diff --git a/playbooks/servicedelegationtarget/servicedelegationtarget-absent.yml b/playbooks/servicedelegationtarget/servicedelegationtarget-absent.yml
new file mode 100644
index 0000000000000000000000000000000000000000..2314daa2be1d9c4460b67a14dc467c26db1ac079
--- /dev/null
+++ b/playbooks/servicedelegationtarget/servicedelegationtarget-absent.yml
@@ -0,0 +1,10 @@
+---
+- name: Servicedelegationtarget absent example
+  hosts: ipaserver
+  become: no
+
+  tasks:
+  - name: Ensure servicedelegationtarget test-delegation-target is absent
+    ipaservicedelegationtarget:
+     name: test-delegation-target
+     state: absent
diff --git a/playbooks/servicedelegationtarget/servicedelegationtarget-member-absent.yml b/playbooks/servicedelegationtarget/servicedelegationtarget-member-absent.yml
new file mode 100644
index 0000000000000000000000000000000000000000..60036fefaa07721440482e2b53a54dbc5b5edaf2
--- /dev/null
+++ b/playbooks/servicedelegationtarget/servicedelegationtarget-member-absent.yml
@@ -0,0 +1,12 @@
+---
+- name: Servicedelegationtarget absent example
+  hosts: ipaserver
+  become: no
+
+  tasks:
+  - name: Ensure member test/example.com is absent in servicedelegationtarget test-delegation-target
+    ipaservicedelegationtarget:
+     name: test-delegation-target
+     principal: test/example.com
+     action: member
+     state: absent
diff --git a/playbooks/servicedelegationtarget/servicedelegationtarget-member-present.yml b/playbooks/servicedelegationtarget/servicedelegationtarget-member-present.yml
new file mode 100644
index 0000000000000000000000000000000000000000..3907d573fc87672e607692536ab82f56cd80b2e1
--- /dev/null
+++ b/playbooks/servicedelegationtarget/servicedelegationtarget-member-present.yml
@@ -0,0 +1,11 @@
+---
+- name: Servicedelegationtarget member present example
+  hosts: ipaserver
+  become: no
+
+  tasks:
+  - name: Ensure member test/example.com is present in servicedelegationtarget test-delegation-target
+    ipaservicedelegationtarget:
+     name: test-delegation-target
+     principal: test/example.com
+     action: member
diff --git a/playbooks/servicedelegationtarget/servicedelegationtarget-present.yml b/playbooks/servicedelegationtarget/servicedelegationtarget-present.yml
new file mode 100644
index 0000000000000000000000000000000000000000..868d411e55905d9a4bd336174f9fd946c5bef05f
--- /dev/null
+++ b/playbooks/servicedelegationtarget/servicedelegationtarget-present.yml
@@ -0,0 +1,9 @@
+---
+- name: Servicedelegationtarget present example
+  hosts: ipaserver
+  become: no
+
+  tasks:
+  - name: Ensure servicedelegationtarget test-delegation-target is present
+    ipaservicedelegationtarget:
+     name: test-delegation-target
diff --git a/plugins/module_utils/ansible_freeipa_module.py b/plugins/module_utils/ansible_freeipa_module.py
index b9e32369b594c9d32bb1d5dccc453cec495abbfc..6485ec8ff0960637c4016ea8c736f41d8acbd47a 100644
--- a/plugins/module_utils/ansible_freeipa_module.py
+++ b/plugins/module_utils/ansible_freeipa_module.py
@@ -86,6 +86,7 @@ else:
     from ipaplatform.paths import paths
     from ipalib.krb_utils import get_credentials_if_valid
     from ipapython.dnsutil import DNSName
+    from ipapython import kerberos
     from ansible.module_utils.basic import AnsibleModule
     from ansible.module_utils._text import to_text
     from ansible.module_utils.common.text.converters import jsonify
@@ -550,6 +551,87 @@ else:
             return False
         return True
 
+    def servicedelegation_normalize_principals(module, principal):
+        """
+        Normalize servicedelegation principals.
+
+        The principals can be service and with IPA 4.9.0+ also host principals.
+        """
+
+        def _normalize_principal_name(name, realm):
+            # Normalize principal name
+            # Copied from ipaserver/plugins/servicedelegation.py
+            try:
+                princ = kerberos.Principal(name, realm=realm)
+            except ValueError as _err:
+                raise ipalib_errors.ValidationError(
+                    name='principal',
+                    reason="Malformed principal: %s" % str(_err))
+
+            if len(princ.components) == 1 and \
+               not princ.components[0].endswith('$'):
+                nprinc = 'host/' + unicode(princ)
+            else:
+                nprinc = unicode(princ)
+            return nprinc
+
+        def _check_exists(module, _type, name):
+            # Check if item of type _type exists using the show command
+            try:
+                module.ipa_command("%s_show" % _type, name, {})
+            except ipalib_errors.NotFound as e:
+                msg = str(e)
+                if "%s not found" % _type in msg:
+                    return False
+                module.fail_json(msg="%s_show failed: %s" % (_type, msg))
+            return True
+
+        ipa_realm = module.ipa_get_realm()
+        _principal = []
+        for _princ in principal:
+            princ = _princ
+            realm = ipa_realm
+
+            # Get principal and realm from _princ if there is a realm
+            if '@' in _princ:
+                princ, realm = _princ.rsplit('@', 1)
+
+            # Lowercase principal
+            princ = princ.lower()
+
+            # Normalize principal
+            try:
+                nprinc = _normalize_principal_name(princ, realm)
+            except ipalib_errors.ValidationError as err:
+                module.fail_json(msg="%s: %s" % (_princ, str(err)))
+            princ = unicode(nprinc)
+
+            # Check that host principal exists
+            if princ.startswith("host/"):
+                if module.ipa_check_version("<", "4.9.0"):
+                    module.fail_json(
+                        msg="The use of host principals is not supported "
+                        "by your IPA version")
+
+                # Get host FQDN (no leading 'host/' and no trailing realm)
+                # (There is no removeprefix and removesuffix in Python2)
+                _host = princ[5:]
+                if _host.endswith("@%s" % realm):
+                    _host = _host[:-len(realm) - 1]
+
+                # Seach for host
+                if not _check_exists(module, "host", _host):
+                    module.fail_json(msg="Host '%s' does not exist" % _host)
+
+            # Check the service principal exists
+            else:
+                if not _check_exists(module, "service", princ):
+                    module.fail_json(msg="Service %s does not exist" % princ)
+
+            _principal.append(princ)
+
+        return _principal
+
     def exit_raw_json(module, **kwargs):
         """
         Print the raw parameters in JSON format, without masking.
diff --git a/plugins/modules/ipaservicedelegationtarget.py b/plugins/modules/ipaservicedelegationtarget.py
new file mode 100644
index 0000000000000000000000000000000000000000..a17accaef267763cf42edc0481e3ef6f12754bfb
--- /dev/null
+++ b/plugins/modules/ipaservicedelegationtarget.py
@@ -0,0 +1,270 @@
+# -*- coding: utf-8 -*-
+
+# Authors:
+#   Thomas Woerner <twoerner@redhat.com>
+#
+# Copyright (C) 2022 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import (absolute_import, division, print_function)
+
+__metaclass__ = type
+
+ANSIBLE_METADATA = {
+    "metadata_version": "1.0",
+    "supported_by": "community",
+    "status": ["preview"],
+}
+
+DOCUMENTATION = """
+---
+module: ipaservicedelegationtarget
+short description: Manage FreeIPA servicedelegationtarget
+description: |
+  Manage FreeIPA servicedelegationtarget and servicedelegationtarget members
+extends_documentation_fragment:
+  - ipamodule_base_docs
+options:
+  name:
+    description: The list of servicedelegationtarget name strings.
+    required: true
+    aliases: ["cn"]
+  principal:
+    description: |
+      The list of principals. A principal can be of the format:
+      fqdn, fqdn@REALM, service/fqdn, service/fqdn@REALM, host/fqdn,
+      host/fqdn@REALM, alias$, alias$@REALM, where fqdn and fqdn@REALM
+      are host principals and the same as host/fqdn and host/fqdn@REALM.
+      Host princpals are only usable with IPA versions 4.9.0 and up.
+    required: false
+  action:
+    description: Work on servicedelegationtarget or member level.
+    choices: ["servicedelegationtarget", "member"]
+    default: servicedelegationtarget
+    required: false
+  state:
+    description: The state to ensure.
+    choices: ["present", "absent"]
+    default: present
+    required: true
+"""
+
+EXAMPLES = """
+# Ensure servicedelegationtarget delegation-target is present
+- ipaservicedelegationtarget:
+    ipaadmin_password: SomeADMINpassword
+    name: delegation-target
+
+# Ensure servicedelegationtarget delegation-target member principal
+# test/example.com is present
+- ipaservicedelegationtarget:
+    ipaadmin_password: SomeADMINpassword
+    name: delegation-target
+    principal: test/example.com
+    action: member
+
+# Ensure servicedelegationtarget delegation-target member principal
+# test/example.com is absent
+- ipaservicedelegationtarget:
+    ipaadmin_password: SomeADMINpassword
+    name: delegation-target
+    principal: test/example.com
+    action: member
+    state: absent
+
+# Ensure servicedelegationtarget delegation-target is absent
+- ipaservicedelegationtarget:
+    ipaadmin_password: SomeADMINpassword
+    name: delegation-target
+    state: absent
+"""
+
+RETURN = """
+"""
+
+
+from ansible.module_utils.ansible_freeipa_module import \
+    IPAAnsibleModule, gen_add_del_lists, gen_add_list, gen_intersection_list, \
+    servicedelegation_normalize_principals
+from ansible.module_utils import six
+
+if six.PY3:
+    unicode = str
+
+
+def find_servicedelegationtarget(module, name):
+    """Find if a servicedelegationtarget with the given name already exist."""
+    try:
+        _result = module.ipa_command("servicedelegationtarget_show", name,
+                                     {"all": True})
+    except Exception:  # pylint: disable=broad-except
+        # An exception is raised if servicedelegationtarget name is not found.
+        return None
+    else:
+        return _result["result"]
+
+
+def main():
+    ansible_module = IPAAnsibleModule(
+        argument_spec=dict(
+            # general
+            name=dict(type="list", aliases=["cn"], default=None,
+                      required=True),
+            # present
+            principal=dict(required=False, type='list', default=None),
+
+            action=dict(type="str", default="servicedelegationtarget",
+                        choices=["member", "servicedelegationtarget"]),
+            # state
+            state=dict(type="str", default="present",
+                       choices=["present", "absent"]),
+        ),
+        supports_check_mode=True,
+    )
+
+    ansible_module._ansible_debug = True
+
+    # Get parameters
+
+    # general
+    names = ansible_module.params_get("name")
+
+    # present
+    principal = ansible_module.params_get("principal")
+
+    action = ansible_module.params_get("action")
+
+    # state
+    state = ansible_module.params_get("state")
+
+    # Check parameters
+
+    invalid = []
+
+    if state == "present":
+        if len(names) != 1:
+            ansible_module.fail_json(
+                msg="Only one servicedelegationtarget can be added at a time.")
+
+    if state == "absent":
+        if len(names) < 1:
+            ansible_module.fail_json(msg="No name given.")
+        if action == "servicedelegationtarget":
+            invalid.append("principal")
+
+    ansible_module.params_fail_used_invalid(invalid, state, action)
+
+    # Init
+
+    changed = False
+    exit_args = {}
+
+    # Connect to IPA API
+    with ansible_module.ipa_connect():
+
+        # Normalize principals
+        if principal:
+            principal = servicedelegation_normalize_principals(ansible_module,
+                                                               principal)
+
+        commands = []
+        principal_add = principal_del = []
+        for name in names:
+            # Make sure servicedelegationtarget exists
+            res_find = find_servicedelegationtarget(ansible_module, name)
+
+            # Create command
+            if state == "present":
+
+                if action == "servicedelegationtarget":
+                    # A servicedelegationtarget does not have normal options.
+                    # There is no servicedelegationtarget-mod command.
+                    # Principal members are handled with the _add_member and
+                    # _remove_member commands further down.
+                    if res_find is None:
+                        commands.append([name, "servicedelegationtarget_add",
+                                         {}])
+                        res_find = {}
+
+                    # Generate addition and removal lists
+                    principal_add, principal_del = gen_add_del_lists(
+                        principal, res_find.get("memberprincipal"))
+
+                elif action == "member":
+                    if res_find is None:
+                        ansible_module.fail_json(
+                            msg="No servicedelegationtarget '%s'" % name)
+
+                    # Reduce add lists for principal
+                    # to new entries only that are not in res_find.
+                    if principal is not None and \
+                       "memberprincipal" in res_find:
+                        principal_add = gen_add_list(
+                            principal, res_find["memberprincipal"])
+                    else:
+                        principal_add = principal
+
+            elif state == "absent":
+                if action == "servicedelegationtarget":
+                    if res_find is not None:
+                        commands.append([name, "servicedelegationtarget_del",
+                                         {}])
+
+                elif action == "member":
+                    if res_find is None:
+                        ansible_module.fail_json(
+                            msg="No servicedelegationtarget '%s'" % name)
+
+                    # Reduce del lists of principal
+                    # to the entries only that are in res_find.
+                    if principal is not None:
+                        principal_del = gen_intersection_list(
+                            principal, res_find.get("memberprincipal"))
+                    else:
+                        principal_del = principal
+
+            else:
+                ansible_module.fail_json(msg="Unkown state '%s'" % state)
+
+            # Handle members
+
+            # Add principal members
+            if principal_add is not None and len(principal_add) > 0:
+                commands.append(
+                    [name, "servicedelegationtarget_add_member",
+                     {
+                         "principal": principal_add,
+                     }])
+            # Remove principal members
+            if principal_del is not None and len(principal_del) > 0:
+                commands.append(
+                    [name, "servicedelegationtarget_remove_member",
+                     {
+                         "principal": principal_del,
+                     }])
+
+        # Execute commands
+
+        changed = ansible_module.execute_ipa_commands(
+            commands, fail_on_member_errors=True)
+
+    # Done
+
+    ansible_module.exit_json(changed=changed, **exit_args)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/tests/servicedelegationtarget/test_servicedelegationtarget.yml b/tests/servicedelegationtarget/test_servicedelegationtarget.yml
new file mode 100644
index 0000000000000000000000000000000000000000..bf133e0d41422b601a0a6ef47b6da51accbb7ff5
--- /dev/null
+++ b/tests/servicedelegationtarget/test_servicedelegationtarget.yml
@@ -0,0 +1,124 @@
+---
+- name: Test servicedelegationtarget
+  hosts: "{{ ipa_test_host | default('ipaserver') }}"
+  # Change "become" or "gather_facts" to "yes",
+  # if you test playbook requires any.
+  become: no
+  gather_facts: yes
+
+  tasks:
+
+  # CLEANUP TEST ITEMS
+
+  - name: Ensure servicedelegationtarget test-delegation-target is absent
+    ipaservicedelegationtarget:
+      ipaadmin_password: SomeADMINpassword
+      ipaapi_context: "{{ ipa_context | default(omit) }}"
+      name: test-delegation-target
+      state: absent
+
+  - name: Ensure service is absent
+    ipaservice:
+      ipaadmin_password: SomeADMINpassword
+      ipaapi_context: "{{ ipa_context | default(omit) }}"
+      name: "{{ 'test-service/' + ansible_facts['fqdn'] }}"
+      state: absent
+      continue: yes
+
+  # CREATE TEST ITEMS
+
+  - name: Ensure service test-sevice is present
+    ipaservice:
+      ipaadmin_password: SomeADMINpassword
+      ipaapi_context: "{{ ipa_context | default(omit) }}"
+      name: "{{ 'test-service/' + ansible_facts['fqdn'] }}"
+    register: result
+    failed_when: not result.changed or result.failed
+
+  # TESTS
+
+  - name: Ensure servicedelegationtarget test-delegation-target is present
+    ipaservicedelegationtarget:
+      ipaadmin_password: SomeADMINpassword
+      ipaapi_context: "{{ ipa_context | default(omit) }}"
+      name: test-delegation-target
+    register: result
+    failed_when: not result.changed or result.failed
+
+  - name: Ensure servicedelegationtarget test-delegation-target is present again
+    ipaservicedelegationtarget:
+      ipaadmin_password: SomeADMINpassword
+      ipaapi_context: "{{ ipa_context | default(omit) }}"
+      name: test-delegation-target
+    register: result
+    failed_when: result.changed or result.failed
+
+  - name: Ensure servicedelegationtarget test-delegation-target member principal "{{ 'test-service/' + ansible_facts['fqdn'] }}" is present
+    ipaservicedelegationtarget:
+      ipaadmin_password: SomeADMINpassword
+      ipaapi_context: "{{ ipa_context | default(omit) }}"
+      name: test-delegation-target
+      principal: "{{ 'test-service/' + ansible_facts['fqdn'] }}"
+      action: member
+    register: result
+    failed_when: not result.changed or result.failed
+
+  - name: Ensure servicedelegationtarget test-delegation-target member principal "{{ 'test-service/' + ansible_facts['fqdn'] }}" is present again
+    ipaservicedelegationtarget:
+      ipaadmin_password: SomeADMINpassword
+      ipaapi_context: "{{ ipa_context | default(omit) }}"
+      name: test-delegation-target
+      principal: "{{ 'test-service/' + ansible_facts['fqdn'] }}"
+      action: member
+    register: result
+    failed_when: result.changed or result.failed
+
+  - name: Ensure servicedelegationtarget test-delegation-target member principal "{{ 'test-service/' + ansible_facts['fqdn'] }}" is absent
+    ipaservicedelegationtarget:
+      ipaadmin_password: SomeADMINpassword
+      ipaapi_context: "{{ ipa_context | default(omit) }}"
+      name: test-delegation-target
+      principal: "{{ 'test-service/' + ansible_facts['fqdn'] }}"
+      action: member
+      state: absent
+    register: result
+    failed_when: not result.changed or result.failed
+
+  - name: Ensure servicedelegationtarget test-delegation-target member principal "{{ 'test-service/' + ansible_facts['fqdn'] }}" is present absent
+    ipaservicedelegationtarget:
+      ipaadmin_password: SomeADMINpassword
+      ipaapi_context: "{{ ipa_context | default(omit) }}"
+      name: test-delegation-target
+      principal: "{{ 'test-service/' + ansible_facts['fqdn'] }}"
+      action: member
+      state: absent
+    register: result
+    failed_when: result.changed or result.failed
+
+  - name: Ensure servicedelegationtarget test-delegation-target is absent
+    ipaservicedelegationtarget:
+      ipaadmin_password: SomeADMINpassword
+      ipaapi_context: "{{ ipa_context | default(omit) }}"
+      name: test-delegation-target
+      state: absent
+    register: result
+    failed_when: not result.changed or result.failed
+
+  - name: Ensure servicedelegationtarget test-delegation-target is absent again
+    ipaservicedelegationtarget:
+      ipaadmin_password: SomeADMINpassword
+      ipaapi_context: "{{ ipa_context | default(omit) }}"
+      name: test-delegation-target
+      state: absent
+    register: result
+    failed_when: result.changed or result.failed
+
+  # CLEANUP TEST ITEMS
+
+  - name: Ensure service is absent
+    ipaservice:
+      ipaadmin_password: SomeADMINpassword
+      ipaapi_context: "{{ ipa_context | default(omit) }}"
+      name: "{{ 'test-service/' + ansible_facts['fqdn'] }}"
+      state: absent
+      continue: yes
diff --git a/tests/servicedelegationtarget/test_servicedelegationtarget_client_context.yml b/tests/servicedelegationtarget/test_servicedelegationtarget_client_context.yml
new file mode 100644
index 0000000000000000000000000000000000000000..04927b93d064517fafe81a73f996cd12c560c950
--- /dev/null
+++ b/tests/servicedelegationtarget/test_servicedelegationtarget_client_context.yml
@@ -0,0 +1,39 @@
+---
+- name: Test servicedelegationtarget
+  hosts: ipaclients, ipaserver
+  # Change "become" or "gather_facts" to "yes",
+  # if you test playbook requires any.
+  become: no
+  gather_facts: no
+
+  tasks:
+  - name: Include FreeIPA facts.
+    include_tasks: ../env_freeipa_facts.yml
+
+  # Test will only be executed if host is not a server.
+  - name: Execute with server context in the client.
+    ipaservicedelegationtarget:
+      ipaadmin_password: SomeADMINpassword
+      ipaapi_context: server
+      name: ThisShouldNotWork
+    register: result
+    failed_when: not (result.failed and result.msg is regex("No module named '*ipaserver'*"))
+    when: ipa_host_is_client
+
+# Import basic module tests, and execute with ipa_context set to 'client'.
+# If ipaclients is set, it will be executed using the client, if not,
+# ipaserver will be used.
+#
+# With this setup, tests can be executed against an IPA client, against
+# an IPA server using "client" context, and ensure that tests are executed
+# in upstream CI.
+
+- name: Test servicedelegationtarget using client context, in client host.
+  import_playbook: test_servicedelegationtarget.yml
+  when: groups['ipaclients']
+  vars:
+    ipa_test_host: ipaclients
+
+- name: Test servicedelegationtarget using client context, in server host.
+  import_playbook: test_servicedelegationtarget.yml
+  when: groups['ipaclients'] is not defined or not groups['ipaclients']
diff --git a/tests/servicedelegationtarget/test_servicedelegationtarget_hostprincipal.yml b/tests/servicedelegationtarget/test_servicedelegationtarget_hostprincipal.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f59d0c45136585f393039377219369c342347562
--- /dev/null
+++ b/tests/servicedelegationtarget/test_servicedelegationtarget_hostprincipal.yml
@@ -0,0 +1,148 @@
+---
+- name: Test servicedelegationtarget_hostprincipal
+  hosts: "{{ ipa_test_host | default('ipaserver') }}"
+  become: no
+  gather_facts: yes
+
+  tasks:
+  # setup
+  - include_tasks: ../env_freeipa_facts.yml
+
+  # host principals are only possible with IPA 4.9.0+
+  - block:
+
+    # SET FACTS
+
+    - name: Get Domain from server name
+      set_fact:
+        ipaserver_domain: "{{ ansible_facts['fqdn'].split('.')[1:] | join ('.') }}"
+      when: ipaserver_domain is not defined
+
+    - name: Get REALM from server name
+      set_fact:
+        ipaserver_realm: "{{ ipaserver_domain | upper }}"
+      when: ipaserver_realm is not defined
+
+    - name: Set test-host fqdn
+      set_fact:
+        test_host_fqdn: "{{ 'test-host.' + ipaserver_domain }}"
+        test_host_fqdn_realm: "{{ 'test-host.' + ipaserver_domain + '@' + ipaserver_realm }}"
+
+    # CLEANUP TEST ITEMS
+
+    - name: Ensure servicedelegationtarget test-delegation-target is absent
+      ipaservicedelegationtarget:
+        ipaadmin_password: SomeADMINpassword
+        ipaapi_context: "{{ ipa_context | default(omit) }}"
+        name: test-delegation-target
+        state: absent
+
+    - name: Ensure host is absent
+      ipahost:
+        ipaadmin_password: SomeADMINpassword
+        ipaapi_context: "{{ ipa_context | default(omit) }}"
+        name: "{{ test_host_fqdn }}"
+        state: absent
+
+    # CREATE TEST ITEMS
+
+    - name: Ensure host is present
+      ipahost:
+        ipaadmin_password: SomeADMINpassword
+        ipaapi_context: "{{ ipa_context | default(omit) }}"
+        name: "{{ test_host_fqdn }}"
+        force: yes
+
+    - name: Ensure servicedelegationtarget test-delegation-target is present
+      ipaservicedelegationtarget:
+        ipaadmin_password: SomeADMINpassword
+        ipaapi_context: "{{ ipa_context | default(omit) }}"
+        name: test-delegation-target
+      register: result
+      failed_when: not result.changed or result.failed
+
+    # TESTS
+
+    - name: Ensure servicedelegationtarget test-delegation-target member host principal "{{ test_host_fqdn }}" is present
+      ipaservicedelegationtarget:
+        ipaadmin_password: SomeADMINpassword
+        ipaapi_context: "{{ ipa_context | default(omit) }}"
+        name: test-delegation-target
+        principal: "{{ test_host_fqdn }}"
+        action: member
+      register: result
+      failed_when: not result.changed or result.failed
+
+    - name: Ensure servicedelegationtarget test-delegation-target member host principal "{{ test_host_fqdn }}" is present again
+      ipaservicedelegationtarget:
+        ipaadmin_password: SomeADMINpassword
+        ipaapi_context: "{{ ipa_context | default(omit) }}"
+        name: test-delegation-target
+        principal: "{{ test_host_fqdn }}"
+        action: member
+      register: result
+      failed_when: result.changed or result.failed
+
+    - name: Ensure servicedelegationtarget test-delegation-target member host principal "{{ test_host_fqdn_realm }}" is present unchanged
+      ipaservicedelegationtarget:
+        ipaadmin_password: SomeADMINpassword
+        ipaapi_context: "{{ ipa_context | default(omit) }}"
+        name: test-delegation-target
+        principal: "{{ test_host_fqdn_realm }}"
+        action: member
+      register: result
+      failed_when: result.changed or result.failed
+
+    - name: Ensure servicedelegationtarget test-delegation-target member host principal "{{ 'host/' + test_host_fqdn_realm }}" is present unchanged
+      ipaservicedelegationtarget:
+        ipaadmin_password: SomeADMINpassword
+        ipaapi_context: "{{ ipa_context | default(omit) }}"
+        name: test-delegation-target
+        principal: "{{ 'host/' + test_host_fqdn_realm }}"
+        action: member
+      register: result
+      failed_when: result.changed or result.failed
+
+    - name: Ensure servicedelegationtarget test-delegation-target member host principal "{{ test_host_fqdn_realm }}" is absent
+      ipaservicedelegationtarget:
+        ipaadmin_password: SomeADMINpassword
+        ipaapi_context: "{{ ipa_context | default(omit) }}"
+        name: test-delegation-target
+        principal: "{{ test_host_fqdn_realm }}"
+        action: member
+        state: absent
+      register: result
+      failed_when: not result.changed or result.failed
+
+    - name: Ensure servicedelegationtarget test-delegation-target member host principal "{{ test_host_fqdn }}" is absent unchanged
+      ipaservicedelegationtarget:
+        ipaadmin_password: SomeADMINpassword
+        ipaapi_context: "{{ ipa_context | default(omit) }}"
+        name: test-delegation-target
+        principal: "{{ test_host_fqdn }}"
+        action: member
+        state: absent
+      register: result
+      failed_when: result.changed or result.failed
+
+    # CLEANUP TEST ITEMS
+
+    - name: Ensure servicedelegationtarget test-delegation-target is absent
+      ipaservicedelegationtarget:
+        ipaadmin_password: SomeADMINpassword
+        ipaapi_context: "{{ ipa_context | default(omit) }}"
+        name: test-delegation-target
+        state: absent
+      register: result
+      failed_when: not result.changed or result.failed
+
+    - name: Ensure host is absent
+      ipahost:
+        ipaadmin_password: SomeADMINpassword
+        ipaapi_context: "{{ ipa_context | default(omit) }}"
+        name: "{{ test_host_fqdn }}"
+        state: absent
+      register: result
+      failed_when: not result.changed or result.failed
+
+    when: ipa_version is version('4.9.0', '>=')