diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml
index dbe3c9cfe1..6c7e48ec09 100644
--- a/.github/BOTMETA.yml
+++ b/.github/BOTMETA.yml
@@ -1341,6 +1341,10 @@ files:
     maintainers: precurse
   $modules/sysrc.py:
     maintainers: dlundgren
+  $modules/systemd_creds_decrypt.py:
+    maintainers: konstruktoid
+  $modules/systemd_creds_encrypt.py:
+    maintainers: konstruktoid
   $modules/sysupgrade.py:
     maintainers: precurse
   $modules/taiga_issue.py:
diff --git a/plugins/modules/systemd_creds_decrypt.py b/plugins/modules/systemd_creds_decrypt.py
new file mode 100644
index 0000000000..a6c7126b2e
--- /dev/null
+++ b/plugins/modules/systemd_creds_decrypt.py
@@ -0,0 +1,157 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2024, Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+module: systemd_creds_decrypt
+short_description: C(systemd)'s C(systemd-creds decrypt) plugin
+description:
+    - This module decrypts input using C(systemd)'s C(systemd-creds decrypt).
+author:
+    - Thomas Sjögren (@konstruktoid)
+version_added: '10.2.0'
+extends_documentation_fragment:
+  - community.general.attributes
+attributes:
+  check_mode:
+    support: full
+    details:
+      - This action does not modify state.
+  diff_mode:
+    support: N/A
+    details:
+      - This action does not modify state.
+options:
+    name:
+        description:
+            - The credential name to validate the embedded credential name.
+        type: str
+        required: false
+    newline:
+        description:
+            - Whether to add a trailing newline character to the end of the output,
+              if not present.
+        type: bool
+        required: false
+        default: false
+    secret:
+        description:
+            - The secret to decrypt.
+        type: str
+        required: true
+    timestamp:
+        description:
+            - The timestamp to use to validate the V(not-after) timestamp that
+              was used during encryption.
+            - Takes a timestamp specification in the format described in
+              V(systemd.time(7\)).
+        type: str
+        required: false
+    transcode:
+        description:
+            - Whether to transcode the output before returning it.
+        type: str
+        choices: [ base64, unbase64, hex, unhex ]
+        required: false
+    user:
+        description:
+            - A user name or numeric UID when decrypting from a specific user context.
+            - If set to the special string V(self) it sets the user to the user
+              of the calling process.
+            - Requires C(systemd) 256 or later.
+        type: str
+        required: false
+notes:
+  - C(systemd-creds) requires C(systemd) 250 or later.
+"""
+
+EXAMPLES = """
+- name: Decrypt secret
+  community.general.systemd_creds_decrypt:
+    name: db
+    secret: "WhQZht+JQJax1aZemmGLxmAAAA..."
+  register: decrypted_secret
+
+- name: Print the decrypted secret
+  ansible.builtin.debug:
+    msg: "{{ decrypted_secret }}"
+"""
+
+RETURN = r"""
+value:
+    description:
+      - The decrypted secret.
+      - Note that Ansible only supports returning UTF-8 encoded strings.
+        If the decrypted secret is binary data, or a string encoded in another
+        way, use O(transcode=base64) or O(transcode=hex) to circument this
+        restriction. You then need to decode the data when using it, for
+        example using the P(ansible.builtin.b64decode#filter) filter.
+    type: str
+    returned: always
+    sample: "access_token"
+"""
+
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+    """Decrypt secret using systemd-creds."""
+    module = AnsibleModule(
+        argument_spec=dict(
+            name=dict(type="str", required=False),
+            newline=dict(type="bool", required=False, default=False),
+            secret=dict(type="str", required=True, no_log=True),
+            timestamp=dict(type="str", required=False),
+            transcode=dict(
+                type="str",
+                choices=["base64", "unbase64", "hex", "unhex"],
+                required=False,
+            ),
+            user=dict(type="str", required=False),
+        ),
+        supports_check_mode=True,
+    )
+
+    cmd = module.get_bin_path("systemd-creds", required=True)
+
+    name = module.params["name"]
+    newline = module.params["newline"]
+    secret = module.params["secret"]
+    timestamp = module.params["timestamp"]
+    transcode = module.params["transcode"]
+    user = module.params["user"]
+
+    decrypt_cmd = [cmd, "decrypt"]
+    if name:
+        decrypt_cmd.append("--name=" + name)
+    else:
+        decrypt_cmd.append("--name=")
+    decrypt_cmd.append("--newline=" + ("yes" if newline else "no"))
+    if timestamp:
+        decrypt_cmd.append("--timestamp=" + timestamp)
+    if transcode:
+        decrypt_cmd.append("--transcode=" + transcode)
+    if user:
+        decrypt_cmd.append("--uid=" + user)
+    decrypt_cmd.extend(["-", "-"])
+
+    rc, stdout, stderr = module.run_command(decrypt_cmd, data=secret, binary_data=True)
+
+    module.exit_json(
+        changed=False,
+        value=stdout,
+        rc=rc,
+        stderr=stderr,
+    )
+
+
+if __name__ == "__main__":
+    main()
diff --git a/plugins/modules/systemd_creds_encrypt.py b/plugins/modules/systemd_creds_encrypt.py
new file mode 100644
index 0000000000..07b68f96f2
--- /dev/null
+++ b/plugins/modules/systemd_creds_encrypt.py
@@ -0,0 +1,149 @@
+#!/usr/bin/python
+
+# Copyright (c) 2024, Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+DOCUMENTATION = r"""
+module: systemd_creds_encrypt
+short_description: C(systemd)'s C(systemd-creds encrypt) plugin
+description:
+    - This module encrypts input using C(systemd)'s C(systemd-creds encrypt).
+author:
+    - Thomas Sjögren (@konstruktoid)
+version_added: '10.2.0'
+extends_documentation_fragment:
+  - community.general.attributes
+attributes:
+  check_mode:
+    support: full
+    details:
+      - This action does not modify state.
+  diff_mode:
+    support: N/A
+    details:
+      - This action does not modify state.
+options:
+    name:
+        description:
+            - The credential name to embed in the encrypted credential data.
+        type: str
+        required: false
+    not_after:
+        description:
+            - The time when the credential shall not be used anymore.
+            - Takes a timestamp specification in the format described in
+              V(systemd.time(7\)).
+        type: str
+        required: false
+    pretty:
+        description:
+            - Pretty print the output so that it may be pasted directly into a
+              unit file.
+        type: bool
+        required: false
+        default: false
+    secret:
+        description:
+            - The secret to encrypt.
+        type: str
+        required: true
+    timestamp:
+        description:
+            - The timestamp to embed into the encrypted credential.
+            - Takes a timestamp specification in the format described in
+              V(systemd.time(7\)).
+        type: str
+        required: false
+    user:
+        description:
+            - A user name or numeric UID to encrypt the credential for.
+            - If set to the special string V(self) it sets the user to the user
+              of the calling process.
+            - Requires C(systemd) 256 or later.
+        type: str
+        required: false
+notes:
+  - C(systemd-creds) requires C(systemd) 250 or later.
+"""
+
+EXAMPLES = """
+- name: Encrypt secret
+  become: true
+  community.general.systemd_creds_encrypt:
+    name: db
+    not_after: +48hr
+    secret: access_token
+  register: encrypted_secret
+
+- name: Print the encrypted secret
+  ansible.builtin.debug:
+    msg: "{{ encrypted_secret }}"
+"""
+
+RETURN = r"""
+value:
+    description: The Base64 encoded encrypted secret.
+    type: str
+    returned: always
+    sample: "WhQZht+JQJax1aZemmGLxmAAAA..."
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main():
+    """Encrypt secret using systemd-creds."""
+    module = AnsibleModule(
+        argument_spec=dict(
+            name=dict(type="str", required=False),
+            not_after=dict(type="str", required=False),
+            pretty=dict(type="bool", default=False),
+            secret=dict(type="str", required=True, no_log=True),
+            timestamp=dict(type="str", required=False),
+            user=dict(type="str", required=False),
+        ),
+        supports_check_mode=True,
+    )
+
+    cmd = module.get_bin_path("systemd-creds", required=True)
+
+    name = module.params["name"]
+    not_after = module.params["not_after"]
+    pretty = module.params["pretty"]
+    secret = module.params["secret"]
+    timestamp = module.params["timestamp"]
+    user = module.params["user"]
+
+    encrypt_cmd = [cmd, "encrypt"]
+    if name:
+        encrypt_cmd.append("--name=" + name)
+    else:
+        encrypt_cmd.append("--name=")
+    if not_after:
+        encrypt_cmd.append("--not-after=" + not_after)
+    if pretty:
+        encrypt_cmd.append("--pretty")
+    if timestamp:
+        encrypt_cmd.append("--timestamp=" + timestamp)
+    if user:
+        encrypt_cmd.append("--uid=" + user)
+    encrypt_cmd.extend(["-", "-"])
+
+    rc, stdout, stderr = module.run_command(encrypt_cmd, data=secret, binary_data=True)
+
+    module.exit_json(
+        changed=False,
+        value=stdout,
+        rc=rc,
+        stderr=stderr,
+    )
+
+
+if __name__ == "__main__":
+    main()
diff --git a/tests/integration/targets/systemd_creds_decrypt/aliases b/tests/integration/targets/systemd_creds_decrypt/aliases
new file mode 100644
index 0000000000..bac6fad493
--- /dev/null
+++ b/tests/integration/targets/systemd_creds_decrypt/aliases
@@ -0,0 +1,11 @@
+# Copyright (c) Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+needs/root
+
+azp/posix/1
+skip/aix
+skip/freebsd
+skip/osx
+skip/macos
diff --git a/tests/integration/targets/systemd_creds_decrypt/tasks/main.yaml b/tests/integration/targets/systemd_creds_decrypt/tasks/main.yaml
new file mode 100644
index 0000000000..2249bd7fa9
--- /dev/null
+++ b/tests/integration/targets/systemd_creds_decrypt/tasks/main.yaml
@@ -0,0 +1,58 @@
+---
+# Copyright (c) Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+- name: Test systemd_creds_decrypt
+  when:
+    - ansible_systemd.version is defined
+    - ansible_systemd.version | int >= 250
+  block:
+    - name: Encrypt secret
+      become: true
+      systemd_creds_encrypt:
+        name: api
+        not_after: +48hr
+        secret: access_token
+      register: encrypted_api_secret
+
+    - name: Print the encrypted secret
+      ansible.builtin.debug:
+        msg: "{{ encrypted_api_secret }}"
+
+    - name: Decrypt secret
+      community.general.systemd_creds_decrypt:
+        name: api
+        newline: false
+        secret: "{{ encrypted_api_secret.value }}"
+      register: decrypted_secret
+
+    - name: Print the decrypted secret
+      ansible.builtin.debug:
+        msg: "{{ decrypted_secret }}"
+
+    - name: Assert that the decrypted secret is the same as the original secret
+      ansible.builtin.assert:
+        that:
+          - decrypted_secret.value == 'access_token'
+        fail_msg: "Decrypted secret is not the same as the original secret"
+        success_msg: "Decrypted secret is the same as the original secret"
+
+    - name: Decrypt secret into hex
+      community.general.systemd_creds_decrypt:
+        name: api
+        newline: false
+        secret: "{{ encrypted_api_secret.value }}"
+        transcode: hex
+      register: decrypted_secret_hex
+
+    - name: Print the trancoded decrypted secret
+      ansible.builtin.debug:
+        msg: "{{ decrypted_secret_hex }}"
+
+    - name: Assert that the decrypted secret is the same as the original secret
+      ansible.builtin.assert:
+        that:
+          - decrypted_secret_hex.value == '6163636573735f746f6b656e'
+        fail_msg: "Decrypted secret is not the same as the original secret"
+        success_msg: "Decrypted secret is the same as the original secret"
diff --git a/tests/integration/targets/systemd_creds_encrypt/aliases b/tests/integration/targets/systemd_creds_encrypt/aliases
new file mode 100644
index 0000000000..bac6fad493
--- /dev/null
+++ b/tests/integration/targets/systemd_creds_encrypt/aliases
@@ -0,0 +1,11 @@
+# Copyright (c) Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+needs/root
+
+azp/posix/1
+skip/aix
+skip/freebsd
+skip/osx
+skip/macos
diff --git a/tests/integration/targets/systemd_creds_encrypt/tasks/main.yaml b/tests/integration/targets/systemd_creds_encrypt/tasks/main.yaml
new file mode 100644
index 0000000000..362fe90bcb
--- /dev/null
+++ b/tests/integration/targets/systemd_creds_encrypt/tasks/main.yaml
@@ -0,0 +1,55 @@
+---
+# Copyright (c) Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+- name: Test systemd_creds_encrypt
+  when:
+    - ansible_systemd.version is defined
+    - ansible_systemd.version | int >= 250
+  block:
+    - name: Encrypt secret
+      become: true
+      systemd_creds_encrypt:
+        name: db
+        not_after: +48hr
+        secret: access_token
+      register: encrypted_secret
+
+    - name: Assert encrypted secret output is base64 encoded
+      ansible.builtin.assert:
+        that:
+          - encrypted_secret.value | b64decode
+        fail_msg: "Encrypted secret is not base64 encoded"
+        success_msg: "Encrypted secret is base64 encoded"
+
+    - name: Print the encrypted secret
+      ansible.builtin.debug:
+        msg: "{{ encrypted_secret }}"
+
+    - name: Assert that SetCredentialEncrypted message is not in the output
+      ansible.builtin.assert:
+        that:
+          - '"SetCredentialEncrypted" not in encrypted_secret.value'
+        fail_msg: "SetCredentialEncrypted is in the output"
+        success_msg: "SetCredentialEncrypted is not in the output"
+
+    - name: Encrypt secret
+      become: true
+      community.general.systemd_creds_encrypt:
+        name: web
+        not_after: +5y
+        pretty: true
+        secret: token
+      register: pretty_encrypted_secret
+
+    - name: Pretty print the encrypted secret
+      ansible.builtin.debug:
+        msg: "{{ pretty_encrypted_secret }}"
+
+    - name: Assert that SetCredentialEncrypted message is in the output
+      ansible.builtin.assert:
+        that:
+          - '"SetCredentialEncrypted=web: " in pretty_encrypted_secret.value'
+        fail_msg: "SetCredentialEncrypted is not in the output"
+        success_msg: "SetCredentialEncrypted is in the output"