commit 08cd2799ce5fcc6244c917f8f31c5932c996ee71
Author: Markus Pesch <markus.pesch@cryptic.systems>
Date:   Mon Apr 28 11:28:52 2025 +0200

    Initial COmmit

diff --git a/.ansible-lint b/.ansible-lint
new file mode 100644
index 0000000..23a163f
--- /dev/null
+++ b/.ansible-lint
@@ -0,0 +1,4 @@
+---
+
+exclude_paths:
+- .gitea/
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..ad4c311
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,12 @@
+# EditorConfig is awesome: https://EditorConfig.org
+
+# top-most EditorConfig file
+root = true
+
+[*]
+indent_style = space
+indent_size = 2
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = false
diff --git a/.gitea/workflows/ansible-linters.yaml b/.gitea/workflows/ansible-linters.yaml
new file mode 100644
index 0000000..09dfc7c
--- /dev/null
+++ b/.gitea/workflows/ansible-linters.yaml
@@ -0,0 +1,20 @@
+name: Ansible Linter
+
+on:
+  pull_request:
+    types: [ "opened", "reopened", "synchronize" ]
+  push:
+    branches: [ '**' ]
+    tags-ignore: [ '**' ]
+
+jobs:
+  ansible-lint:
+    runs-on:
+    - ubuntu-latest
+    steps:
+    - uses: actions/checkout@v4
+    - name: Run ansible-lint
+      uses: ansible/ansible-lint@v25.2.1
+      with:
+        args: "--config-file .ansible-lint"
+        setup_python: "true"
diff --git a/.gitea/workflows/markdown-linters.yaml b/.gitea/workflows/markdown-linters.yaml
new file mode 100644
index 0000000..4ec7a3a
--- /dev/null
+++ b/.gitea/workflows/markdown-linters.yaml
@@ -0,0 +1,19 @@
+name: Lint Markdown files
+
+on:
+  pull_request:
+    types: [ "opened", "reopened", "synchronize" ]
+  push:
+    branches: [ '**' ]
+    tags-ignore: [ '**' ]
+
+jobs:
+  markdown-lint:
+    container:
+      image: git.cryptic.systems/volker.raschek/markdownlint:0.44.0
+    runs-on:
+    - ubuntu-latest
+    steps:
+    - uses: actions/checkout@v4.2.2
+    - name: Lint Markdown files
+      run: markdownlint --config .markdownlint.yaml .
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..feaf22e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+.ansible
\ No newline at end of file
diff --git a/.markdownlint.yaml b/.markdownlint.yaml
new file mode 100644
index 0000000..0cca4db
--- /dev/null
+++ b/.markdownlint.yaml
@@ -0,0 +1,137 @@
+# markdownlint YAML configuration
+# https://github.com/DavidAnson/markdownlint/blob/main/schema/.markdownlint.yaml
+
+# Default state for all rules
+default: true
+
+# Path to configuration file to extend
+extends: null
+
+# MD003/heading-style/header-style - Heading style
+MD003:
+  # Heading style
+  style: "atx"
+
+# MD004/ul-style - Unordered list style
+MD004:
+  style: "dash"
+
+# MD007/ul-indent - Unordered list indentation
+MD007:
+  # Spaces for indent
+  indent: 2
+  # Whether to indent the first level of the list
+  start_indented: false
+
+# MD009/no-trailing-spaces - Trailing spaces
+MD009:
+  # Spaces for line break
+  br_spaces: 2
+  # Allow spaces for empty lines in list items
+  list_item_empty_lines: false
+  # Include unnecessary breaks
+  strict: false
+
+# MD010/no-hard-tabs - Hard tabs
+MD010:
+  # Include code blocks
+  code_blocks: true
+
+# MD012/no-multiple-blanks - Multiple consecutive blank lines
+MD012:
+  # Consecutive blank lines
+  maximum: 1
+
+# MD013/line-length - Line length
+MD013:
+  # Number of characters
+  line_length: 120
+  # Number of characters for headings
+  heading_line_length: 120
+  # Number of characters for code blocks
+  code_block_line_length: 120
+  # Include code blocks
+  code_blocks: false
+  # Include tables
+  tables: false
+  # Include headings
+  headings: true
+  # Strict length checking
+  strict: false
+  # Stern length checking
+  stern: false
+
+# MD022/blanks-around-headings/blanks-around-headers - Headings should be surrounded by blank lines
+MD022:
+  # Blank lines above heading
+  lines_above: 1
+  # Blank lines below heading
+  lines_below: 1
+
+# MD025/single-title/single-h1 - Multiple top-level headings in the same document
+MD025:
+  # Heading level
+  level: 1
+  # RegExp for matching title in front matter
+  front_matter_title: "^\\s*title\\s*[:=]"
+
+# MD026/no-trailing-punctuation - Trailing punctuation in heading
+MD026:
+  # Punctuation characters
+  punctuation: ".,;:!。,;:!"
+
+# MD029/ol-prefix - Ordered list item prefix
+MD029:
+  # List style
+  style: "one_or_ordered"
+
+# MD030/list-marker-space - Spaces after list markers
+MD030:
+  # Spaces for single-line unordered list items
+  ul_single: 1
+  # Spaces for single-line ordered list items
+  ol_single: 1
+  # Spaces for multi-line unordered list items
+  ul_multi: 1
+  # Spaces for multi-line ordered list items
+  ol_multi: 1
+
+# MD033/no-inline-html - Inline HTML
+MD033:
+  # Allowed elements
+  allowed_elements: []
+
+# MD035/hr-style - Horizontal rule style
+MD035:
+  # Horizontal rule style
+  style: "---"
+
+# MD036/no-emphasis-as-heading/no-emphasis-as-header - Emphasis used instead of a heading
+MD036:
+  # Punctuation characters
+  punctuation: ".,;:!?。,;:!?"
+
+# MD041/first-line-heading/first-line-h1 - First line in a file should be a top-level heading
+MD041:
+  # Heading level
+  level: 1
+  # RegExp for matching title in front matter
+  front_matter_title: "^\\s*title\\s*[:=]"
+
+# MD044/proper-names - Proper names should have the correct capitalization
+MD044:
+  # List of proper names
+  names:
+  - gitea
+  # Include code blocks
+  code_blocks: false
+
+# MD046/code-block-style - Code block style
+MD046:
+  # Block style
+  style: "fenced"
+
+# MD048/code-fence-style - Code fence style
+MD048:
+  # Code fence syle
+  style: "backtick"
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..a00d0c5
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,14 @@
+{
+  "ansible.python.interpreterPath": "/bin/python",
+  "files.associations": {
+    "**/.gitea/**/*.yml": "yaml",
+    "**/.gitea/**/*.yaml": "yaml",
+    ".drone.yml": "yaml",
+    "docker-compose*.yml": "dockercompose",
+    "*.yml": "ansible",
+    "*.yaml": "ansible",
+    ".yamllint": "yaml",
+    ".yamllint.yml": "yaml",
+    ".yamllint.yaml": "yaml"
+  }
+}
\ No newline at end of file
diff --git a/.yamllint.yaml b/.yamllint.yaml
new file mode 100644
index 0000000..f22686c
--- /dev/null
+++ b/.yamllint.yaml
@@ -0,0 +1,17 @@
+#
+# Documentation:
+# https://yamllint.readthedocs.io/en/stable/
+#
+
+rules:
+  brackets:
+    forbid: false
+    min-spaces-inside: 0
+    max-spaces-inside: 2
+    min-spaces-inside-empty: 0
+    max-spaces-inside-empty: 0
+  indentation:
+    spaces: 2
+    indent-sequences: false
+  line-length:
+    max: 360
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..89dc507
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2025 Markus Pesch
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b5b182e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,24 @@
+# sudo_users
+
+[![Ansible Role](https://img.shields.io/ansible/role/d/58433)](https://galaxy.ansible.com/volker_raschek/root)
+
+With following role can be created sudoers files in `/etc/sudoers.d`. For
+example to grant a user special perimssions to execute a program as root.
+
+## Supported distributions
+
+- Arch Linux
+- Debian
+- Fedora
+- RHEL
+- Ubuntu 20.04
+
+## Features
+
+- Installing sudo
+- Configuring drop-on files in `/etc/sudoers.d`
+
+## Configuring
+
+In the default directory are examples how to configure the network stack. Copy the
+defaults into your `host_vars` or `group_vars` and adapt the examples.
diff --git a/default/main.yml b/default/main.yml
new file mode 100644
index 0000000..bbbdbfc
--- /dev/null
+++ b/default/main.yml
@@ -0,0 +1,12 @@
+---
+
+root: {}
+#   password: ""
+#   ssh:
+#     config:
+#     - Host: "*"
+#       StrictHostKeyChecking: "no"
+#       UserKnownHostFile: /dev/null
+#     authorized_keys:
+#     - filename: markus@markus-pc.pub
+#       # command: "" # optional
diff --git a/meta/main.yml b/meta/main.yml
new file mode 100644
index 0000000..4d40a5e
--- /dev/null
+++ b/meta/main.yml
@@ -0,0 +1,23 @@
+galaxy_info:
+  namespace: volker_raschek
+  role_name: "root"
+  author: "Markus Pesch"
+  description: Role to configure root
+  company: Cryptic Systems
+  license: MIT
+  min_ansible_version: "2.9"
+  platforms:
+  - name: ArchLinux
+    versions:
+    - all
+  - name: Ubuntu
+    versions:
+    - all
+  - name: Fedora
+    versions:
+    - "35"
+
+  galaxy_tags:
+  - root
+
+dependencies: []
diff --git a/renovate.json b/renovate.json
new file mode 100644
index 0000000..84f88f7
--- /dev/null
+++ b/renovate.json
@@ -0,0 +1,9 @@
+{
+  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
+  "extends": [
+    "local>volker.raschek/renovate-config:default#master",
+    "local>volker.raschek/renovate-config:container#master",
+    "local>volker.raschek/renovate-config:actions#master",
+    "local>volker.raschek/renovate-config:regexp#master"
+  ]
+}
\ No newline at end of file
diff --git a/tasks/main.yml b/tasks/main.yml
new file mode 100644
index 0000000..5457063
--- /dev/null
+++ b/tasks/main.yml
@@ -0,0 +1,115 @@
+---
+
+- name: Set or unset root password
+  block:
+  - name: Set root password
+    ansible.builtin.user:
+      name: root
+      password: "{{ root.password | password_hash('sha512') }}"
+      password_lock: false
+    when: root.password is defined and root.password | length > 0 and root.password != "!"
+    no_log: true
+
+  - name: Update ansible_become_pass
+    ansible.builtin.set_fact:
+      ansible_become_pass: "{{ root.password }}"
+    when: root.password is defined and root.password | length > 0 and root.password != "!"
+    no_log: true
+
+  - name: Lock root login
+    ansible.builtin.user:
+      name: root
+      password_lock: true
+    when: root.password is defined and root.password | length <= 0
+
+- name: Create .ssh directory
+  ansible.builtin.file:
+    path: "/root/.ssh"
+    state: directory
+    owner: "root"
+    group: "root"
+    mode: "0700"
+
+- name: "Create authorized_keys file"
+  ansible.builtin.template:
+    src: root/.ssh/authorized_keys.j2
+    dest: "/root/.ssh/authorized_keys"
+    owner: "root"
+    group: "root"
+    mode: "0600"
+  when: root.ssh is defined and
+        root.ssh.authorized_keys is defined and
+        root.ssh.authorized_keys | length > 0
+
+- name: "Remove authorized_keys file"
+  ansible.builtin.file:
+    path: "/root/.ssh/authorized_keys"
+    state: absent
+  when: root.ssh is defined and
+        root.ssh.authorized_keys is defined and
+        root.ssh.authorized_keys | length <= 0 or
+        root.ssh is defined and
+        root.ssh.authorized_keys is not defined or
+        root.ssh is not defined
+
+- name: "Generate private and public SSH key"
+  when: root.ssh is defined and
+        (
+          root.ssh.private_keys is not defined or
+          root.ssh.private_keys | length <= 0
+        )
+  block:
+  - name: "Check if private SSH key already exists"
+    ansible.builtin.stat:
+      path: "/root/.ssh/{{ inventory_hostname_short }}.key"
+    register: stat_result
+  - name: "Generate private SSH key"
+    community.crypto.openssh_keypair:
+      path: "/root/.ssh/{{ inventory_hostname_short }}.key"
+      type: ed25519
+      size: 512
+      state: present
+      comment: "root@{{ inventory_hostname_short }}"
+      force: false
+    when: not stat_result.stat.exists
+
+- name: "Create private and public SSH key"
+  when: root.ssh is defined and
+        root.ssh.private_keys is defined and
+        root.ssh.private_keys | length > 0
+  block:
+  - name: "Create private SSH keys"
+    ansible.builtin.copy:
+      src: "{{ playbook_dir }}/ssh/private_keys/{{ item }}"
+      dest: "/root/.ssh/{{ item }}"
+      owner: "root"
+      group: "root"
+      mode: "0600"
+    with_items:
+    - "{{ root.ssh.private_keys }}"
+
+  - name: "Extract public SSH keys from private keys"
+    ansible.builtin.shell:
+    args:
+      executable: /bin/bash
+      cmd: "ssh-keygen -y -f /root/.ssh/{{ item }} > /root/.ssh/{{ item }}.pub"
+    register: _root_ssh_pub_key_extraction
+    changed_when: _root_ssh_pub_key_extraction.rc == 0
+    failed_when: _root_ssh_pub_key_extraction.rc > 0
+    with_items:
+    - "{{ root.ssh.private_keys }}"
+
+- name: "Create custom SSH client config"
+  ansible.builtin.template:
+    src: root/.ssh/config.j2
+    dest: /root/.ssh/config
+    owner: "root"
+    group: "root"
+    mode: "0644"
+  when: root.ssh.config is defined and root.ssh.config | length >= 0
+
+- name: "Remove custom SSH client config"
+  ansible.builtin.file:
+    path: "/root/.ssh/config"
+    state: absent
+  when: root.ssh.config is not defined
diff --git a/templates/root/.ssh/authorized_keys.j2 b/templates/root/.ssh/authorized_keys.j2
new file mode 100644
index 0000000..13d3c4f
--- /dev/null
+++ b/templates/root/.ssh/authorized_keys.j2
@@ -0,0 +1,10 @@
+#
+# {{ ansible_managed }}
+#
+{% for authorized_key in root.ssh.authorized_keys %}
+{% if authorized_key.command is defined and authorized_key.command | length > 0 %}
+command="{{ authorized_key.command }}" {{ lookup('file', 'ssh/authorized_keys/' + authorized_key.filename ) }}
+{% else %}
+{{ lookup('file', 'ssh/authorized_keys/' + authorized_key.filename ) }}
+{% endif %}
+{% endfor %}
diff --git a/templates/root/.ssh/config.j2 b/templates/root/.ssh/config.j2
new file mode 100644
index 0000000..208ba3a
--- /dev/null
+++ b/templates/root/.ssh/config.j2
@@ -0,0 +1,15 @@
+#jinja2: lstrip_blocks: True
+#
+# {{ ansible_managed }}
+#
+
+{% for config in root.ssh.config %}
+{% for property, value in config.items() %}
+{% if property == "Host" %}
+{{ property }} {{ value }}
+{% else %}
+  {{ property }} {{ value }}
+{% endif %}
+{% endfor %}
+
+{% endfor %}