add sftp_share role
This commit is contained in:
16
roles/sftp_share/.ansible-lint
Normal file
16
roles/sftp_share/.ansible-lint
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
profile: production
|
||||||
|
strict: true
|
||||||
|
|
||||||
|
# Enable checking of loop variable prefixes in roles
|
||||||
|
loop_var_prefix: "^(__|{role}_)"
|
||||||
|
|
||||||
|
skip_list:
|
||||||
|
- var-naming[no-role-prefix]
|
||||||
|
|
||||||
|
warn_list:
|
||||||
|
- role-name[path]
|
||||||
|
- var-naming[no-role-prefix]
|
||||||
|
|
||||||
|
exclude_paths:
|
||||||
|
- .github/
|
||||||
3
roles/sftp_share/.gitignore
vendored
Normal file
3
roles/sftp_share/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
.ansible
|
||||||
8
roles/sftp_share/.yamllint
Normal file
8
roles/sftp_share/.yamllint
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
rules:
|
||||||
|
brackets:
|
||||||
|
forbid: false
|
||||||
|
min-spaces-inside: 0
|
||||||
|
max-spaces-inside: 2
|
||||||
|
min-spaces-inside-empty: -1
|
||||||
|
max-spaces-inside-empty: 2
|
||||||
73
roles/sftp_share/README.md
Normal file
73
roles/sftp_share/README.md
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
SFTP Share Ansible Role
|
||||||
|
=========
|
||||||
|
|
||||||
|
Creates a user account and directories for secured SFTP data exchange (one user = one share).
|
||||||
|
|
||||||
|
The role creates a dedicated SSH config under `/etc/ssh/sshd_config.d/`, configuring:
|
||||||
|
|
||||||
|
- chroot into the directory of the share
|
||||||
|
- limits commands to `internal-sftp`
|
||||||
|
- further controls SFTP permissions for the share
|
||||||
|
|
||||||
|
Disk quota management is not included.
|
||||||
|
|
||||||
|
Requirements
|
||||||
|
------------
|
||||||
|
|
||||||
|
None
|
||||||
|
|
||||||
|
Role Variables
|
||||||
|
--------------
|
||||||
|
|
||||||
|
`sftp_username` — user account name on the sFTP server
|
||||||
|
`sftp_pubkey` — the public part of the SSH key the user will be using to connect to the server
|
||||||
|
`sftp_root` — directory to put the SFTP shares under
|
||||||
|
`sftp_transfers_groupname` — group owning `{{sftp_root}}/{{sftp_username}}-uploads/transfers`
|
||||||
|
`sftp_permissions` — list of permissions, see below for supported (default: `open,close,read,write,lstat,fstat,opendir,readdir,remove,mkdir,rmdir,realpath,rename`)
|
||||||
|
|
||||||
|
List Of SFTP Operations
|
||||||
|
------------------------
|
||||||
|
`open` — open a file for reading or writing
|
||||||
|
`close` — close an opened file
|
||||||
|
`read` — read data from a file
|
||||||
|
`write` — write data to a file
|
||||||
|
`lstat` — get file attributes without following symlinks
|
||||||
|
`fstat` — get attributes of an opened file
|
||||||
|
`setstat` — change file attributes (chmod, chown, etc.)
|
||||||
|
`fsetstat` — change attributes of an opened file
|
||||||
|
`opendir` — open a directory for reading
|
||||||
|
`readdir` — read the contents of a directory
|
||||||
|
`remove` — delete a file
|
||||||
|
`mkdir` — create a new directory
|
||||||
|
`rmdir` — remove an empty directory
|
||||||
|
`realpath` — resolve absolute path of a file or directory (inside chroot)
|
||||||
|
`stat` — get file attributes (follows symlinks)
|
||||||
|
`rename` — rename a file or directory
|
||||||
|
`readlink` — read the target of a symbolic link
|
||||||
|
`symlink` — create a symbolic link
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
- Default: `open,close,read,write,lstat,fstat,opendir,readdir,remove,mkdir,rmdir,realpath,rename`
|
||||||
|
- Read only: `open,close,read,lstat,fstat,opendir,readdir,realpath,stat`
|
||||||
|
- Required to establish an SFTP connection: `realpath`
|
||||||
|
|
||||||
|
Dependencies
|
||||||
|
------------
|
||||||
|
|
||||||
|
None
|
||||||
|
|
||||||
|
Example Playbook
|
||||||
|
----------------
|
||||||
|
|
||||||
|
See `molecule/default/converge.yml` for an example role invocation.
|
||||||
|
|
||||||
|
License
|
||||||
|
-------
|
||||||
|
|
||||||
|
BSD
|
||||||
|
|
||||||
|
Author Information
|
||||||
|
------------------
|
||||||
|
|
||||||
|
malyuk.ss@genlab.llc
|
||||||
11
roles/sftp_share/conda.dev.yml
Normal file
11
roles/sftp_share/conda.dev.yml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
name: ansible-sftp-share
|
||||||
|
channels:
|
||||||
|
- conda-forge
|
||||||
|
dependencies:
|
||||||
|
- python~=3.12.0
|
||||||
|
- pip>=24.2
|
||||||
|
- actionlint
|
||||||
|
- pip:
|
||||||
|
- -r requirements.txt
|
||||||
|
- -r requirements.ci.txt
|
||||||
9
roles/sftp_share/conda.prod.yml
Normal file
9
roles/sftp_share/conda.prod.yml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
name: ansible-sftp-share
|
||||||
|
channels:
|
||||||
|
- conda-forge
|
||||||
|
dependencies:
|
||||||
|
- python~=3.12.0
|
||||||
|
- pip>=24.2
|
||||||
|
- pip:
|
||||||
|
- -r requirements.txt
|
||||||
2
roles/sftp_share/defaults/main.yml
Normal file
2
roles/sftp_share/defaults/main.yml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
---
|
||||||
|
sftp_permissions: "open,close,read,write,lstat,fstat,opendir,readdir,remove,mkdir,rmdir,realpath,rename"
|
||||||
7
roles/sftp_share/handlers/main.yml
Normal file
7
roles/sftp_share/handlers/main.yml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
- name: Restart sshd
|
||||||
|
ansible.builtin.systemd_service:
|
||||||
|
name: ssh
|
||||||
|
state: restarted
|
||||||
|
enabled: true
|
||||||
|
daemon_reload: true
|
||||||
17
roles/sftp_share/meta/main.yml
Normal file
17
roles/sftp_share/meta/main.yml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
galaxy_info:
|
||||||
|
role_name: "sftp_share"
|
||||||
|
namespace: genlab
|
||||||
|
author: "Sergey Malyuk"
|
||||||
|
company: "Genlab, LLC"
|
||||||
|
description: ""
|
||||||
|
license: "MIT"
|
||||||
|
min_ansible_version: "2.1"
|
||||||
|
|
||||||
|
platforms:
|
||||||
|
- name: "Ubuntu"
|
||||||
|
versions: [ "focal", "jammy", "noble" ]
|
||||||
|
|
||||||
|
galaxy_tags: [ ]
|
||||||
|
|
||||||
|
dependencies: []
|
||||||
39
roles/sftp_share/molecule/default/converge.yml
Normal file
39
roles/sftp_share/molecule/default/converge.yml
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
- name: Converge
|
||||||
|
hosts: all
|
||||||
|
|
||||||
|
pre_tasks:
|
||||||
|
- name: Create test group
|
||||||
|
ansible.builtin.group:
|
||||||
|
name: testgrp
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Ensure OpenSSH server is installed
|
||||||
|
ansible.builtin.apt:
|
||||||
|
name: openssh-server
|
||||||
|
state: present
|
||||||
|
update_cache: true
|
||||||
|
|
||||||
|
- name: Ensure the `/root/.ssh` directory exists
|
||||||
|
ansible.builtin.file:
|
||||||
|
state: directory
|
||||||
|
path: "/root/.ssh"
|
||||||
|
mode: "0700"
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
|
||||||
|
- name: Provide an SSH private key for testing purposes
|
||||||
|
ansible.builtin.copy:
|
||||||
|
src: "ssh/id_ed25519"
|
||||||
|
dest: "/root/.ssh/id_ed25519"
|
||||||
|
mode: "0600"
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
|
||||||
|
roles:
|
||||||
|
- role: genlab.sftp_share
|
||||||
|
sftp_username: "testusr"
|
||||||
|
sftp_pubkey: "{{ lookup('file', 'ssh/id_ed25519.pub') }}"
|
||||||
|
sftp_root: "/primary/data"
|
||||||
|
sftp_transfers_groupname: "testgrp"
|
||||||
|
sftp_permissions: "open,close,read,lstat,fstat,opendir,readdir,realpath,stat" # read-only
|
||||||
27
roles/sftp_share/molecule/default/molecule.yml
Normal file
27
roles/sftp_share/molecule/default/molecule.yml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
dependency:
|
||||||
|
name: galaxy
|
||||||
|
|
||||||
|
driver:
|
||||||
|
name: docker
|
||||||
|
|
||||||
|
platforms:
|
||||||
|
- name: ubuntu
|
||||||
|
image: geerlingguy/docker-${MOLECULE_DISTRO:-ubuntu2404}-ansible:latest
|
||||||
|
pre_build_image: true
|
||||||
|
command: ${MOLECULE_DOCKER_COMMAND:-""}
|
||||||
|
volumes:
|
||||||
|
- /sys/fs/cgroup:/sys/fs/cgroup:rw
|
||||||
|
cgroupns_mode: host
|
||||||
|
privileged: true
|
||||||
|
|
||||||
|
provisioner:
|
||||||
|
name: ansible
|
||||||
|
|
||||||
|
verifier:
|
||||||
|
name: ansible
|
||||||
|
|
||||||
|
lint: |
|
||||||
|
set -e
|
||||||
|
yamllint .
|
||||||
|
ansible-lint .
|
||||||
7
roles/sftp_share/molecule/default/ssh/id_ed25519
Normal file
7
roles/sftp_share/molecule/default/ssh/id_ed25519
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||||
|
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||||
|
QyNTUxOQAAACBSSKh3Oc0GgnvSzCmvloaoBVpa3/ZcEUhimzih81XMUAAAAJCpucRAqbnE
|
||||||
|
QAAAAAtzc2gtZWQyNTUxOQAAACBSSKh3Oc0GgnvSzCmvloaoBVpa3/ZcEUhimzih81XMUA
|
||||||
|
AAAEDKy8xJ6QdOj+DG1oromiQ0TmwnTWFcMwjUMdqIZFd8blJIqHc5zQaCe9LMKa+WhqgF
|
||||||
|
Wlrf9lwRSGKbOKHzVcxQAAAAC3Jvb3RAdWJ1bnR1AQI=
|
||||||
|
-----END OPENSSH PRIVATE KEY-----
|
||||||
1
roles/sftp_share/molecule/default/ssh/id_ed25519.pub
Normal file
1
roles/sftp_share/molecule/default/ssh/id_ed25519.pub
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFJIqHc5zQaCe9LMKa+WhqgFWlrf9lwRSGKbOKHzVcxQ root@ubuntu
|
||||||
91
roles/sftp_share/molecule/default/verify.yml
Normal file
91
roles/sftp_share/molecule/default/verify.yml
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
---
|
||||||
|
- name: Verify
|
||||||
|
hosts: all
|
||||||
|
gather_facts: false
|
||||||
|
any_errors_fatal: true
|
||||||
|
vars:
|
||||||
|
sftp_username: "testusr"
|
||||||
|
sftp_pubkey: "{{ lookup('file', 'ssh/id_ed25519.pub') }}"
|
||||||
|
sftp_root: "/primary/data"
|
||||||
|
sftp_transfers_groupname: "testgrp"
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Gather user SFTP directory info
|
||||||
|
ansible.builtin.stat:
|
||||||
|
path: "{{ sftp_root }}/{{ sftp_username }}-uploads"
|
||||||
|
register: sftp_root_stat
|
||||||
|
|
||||||
|
- name: Assert that user SFTP directory has correct owner
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- sftp_root_stat.stat.pw_name == "root"
|
||||||
|
- sftp_root_stat.stat.gr_name == "root"
|
||||||
|
success_msg: "SFTP directory {{ sftp_root }}/{{ sftp_username }}-uploads has correct owner root:root"
|
||||||
|
fail_msg: |
|
||||||
|
"SFTP directory {{ sftp_root }}/{{ sftp_username }}-uploads does not have correct owner root:root,
|
||||||
|
actual owner is {{ sftp_root_stat.stat.pw_name }}:{{ sftp_root_stat.stat.gr_name }}"
|
||||||
|
|
||||||
|
- name: Gather transfers directory info
|
||||||
|
ansible.builtin.stat:
|
||||||
|
path: "{{ sftp_root }}/{{ sftp_username }}-uploads/transfers"
|
||||||
|
register: transfers_stat
|
||||||
|
|
||||||
|
- name: Assert that transfers directory has correct owner
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- transfers_stat.stat.pw_name == sftp_username
|
||||||
|
- transfers_stat.stat.gr_name == sftp_transfers_groupname
|
||||||
|
success_msg: "Directory {{ sftp_root }}/{{ sftp_username }}-uploads/transfers has correct owner {{ sftp_username }}:{{ sftp_transfers_groupname }}"
|
||||||
|
fail_msg: |
|
||||||
|
"Directory {{ sftp_root }}/{{ sftp_username }}-uploads/transfers does not have correct owner {{ sftp_username }}:{{ sftp_transfers_groupname }},
|
||||||
|
actual owner is {{ transfers_stat.stat.pw_name }}:{{ transfers_stat.stat.gr_name }}"
|
||||||
|
|
||||||
|
- name: Assert that transfers directory has correct permissions
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- transfers_stat.stat.mode == "2770"
|
||||||
|
success_msg: "Directory {{ sftp_root }}/{{ sftp_username }}-uploads/transfers has correct permissions 2770"
|
||||||
|
fail_msg: |
|
||||||
|
"Directory {{ sftp_root }}/{{ sftp_username }}-uploads/transfers does not have correct permissions 2770,
|
||||||
|
actual permissions are {{ sftp_root_stat.stat.mode }}"
|
||||||
|
|
||||||
|
- name: Get user info
|
||||||
|
ansible.builtin.getent:
|
||||||
|
database: passwd
|
||||||
|
key: "{{ sftp_username }}"
|
||||||
|
register: user_info
|
||||||
|
|
||||||
|
- name: Check user exists
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- user_info is defined
|
||||||
|
success_msg: "User {{ sftp_username }} exists"
|
||||||
|
fail_msg: "User {{ sftp_username }} does not exist"
|
||||||
|
|
||||||
|
- name: Check that sshd refuses non-SFTP connection attempts
|
||||||
|
changed_when: false
|
||||||
|
failed_when: ssh.stdout != expected
|
||||||
|
register: ssh
|
||||||
|
vars:
|
||||||
|
expected: "This service allows sftp connections only."
|
||||||
|
ansible.builtin.command: "ssh -l {{ sftp_username }} 127.0.0.1 -o StrictHostKeyChecking=no exit"
|
||||||
|
|
||||||
|
- name: Check that sFTP chroots into /transfers
|
||||||
|
changed_when: false
|
||||||
|
register: sftp
|
||||||
|
failed_when: sftp.stdout != expected
|
||||||
|
vars:
|
||||||
|
expected: "sftp> pwd\nRemote working directory: /transfers"
|
||||||
|
ansible.builtin.shell:
|
||||||
|
executable: /bin/bash
|
||||||
|
cmd: 'set -o pipefail; echo "pwd" | sftp -b - -o StrictHostKeyChecking=no {{ sftp_username | quote }}@127.0.0.1'
|
||||||
|
|
||||||
|
- name: Check that non-whitelisted sFTP permissions are denied
|
||||||
|
changed_when: false
|
||||||
|
register: sftp_mkdir
|
||||||
|
failed_when: sftp_mkdir.stderr != expected
|
||||||
|
vars:
|
||||||
|
expected: 'remote mkdir "/transfers/foo": Permission denied'
|
||||||
|
ansible.builtin.shell:
|
||||||
|
executable: /bin/bash
|
||||||
|
cmd: 'set -o pipefail; echo "mkdir foo" | sftp -b - -o StrictHostKeyChecking=no {{ sftp_username | quote }}@127.0.0.1'
|
||||||
6
roles/sftp_share/renovate.json
Normal file
6
roles/sftp_share/renovate.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": [
|
||||||
|
"config:recommended"
|
||||||
|
]
|
||||||
|
}
|
||||||
6
roles/sftp_share/requirements.ci.txt
Normal file
6
roles/sftp_share/requirements.ci.txt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
ansible-lint
|
||||||
|
molecule==24.12.0
|
||||||
|
molecule-plugins[docker]
|
||||||
|
docker~=7.1.0
|
||||||
|
requests==2.31.0 # pinned to the latest version not breaking Docker SDK
|
||||||
|
yamllint
|
||||||
1
roles/sftp_share/requirements.txt
Normal file
1
roles/sftp_share/requirements.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ansible~=11.11.0
|
||||||
3
roles/sftp_share/requirements.yml
Normal file
3
roles/sftp_share/requirements.yml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# requirements file
|
||||||
|
---
|
||||||
|
collections: []
|
||||||
38
roles/sftp_share/tasks/main.yml
Normal file
38
roles/sftp_share/tasks/main.yml
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
- name: Create SFTP user - {{ sftp_username }}
|
||||||
|
ansible.builtin.user:
|
||||||
|
name: "{{ sftp_username }}"
|
||||||
|
state: present
|
||||||
|
create_home: true
|
||||||
|
shell: /sbin/nologin
|
||||||
|
|
||||||
|
- name: Create SFTP user upload directory - {{ sftp_username }}
|
||||||
|
ansible.builtin.file:
|
||||||
|
state: directory
|
||||||
|
path: "{{ sftp_root }}/{{ sftp_username }}-uploads/"
|
||||||
|
mode: '0755'
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
|
||||||
|
- name: Create transfers directory - {{ sftp_username }}
|
||||||
|
ansible.builtin.file:
|
||||||
|
state: directory
|
||||||
|
path: "{{ sftp_root }}/{{ sftp_username }}-uploads/transfers/"
|
||||||
|
mode: '2770'
|
||||||
|
owner: "{{ sftp_username }}"
|
||||||
|
group: "{{ sftp_transfers_groupname }}"
|
||||||
|
|
||||||
|
- name: Push sshd config - {{ sftp_username }}
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: sshd_config.conf.j2
|
||||||
|
dest: "/etc/ssh/sshd_config.d/60-sftp-jail-{{ sftp_username }}.conf"
|
||||||
|
mode: '0600'
|
||||||
|
owner: "root"
|
||||||
|
group: "root"
|
||||||
|
notify: Restart sshd
|
||||||
|
|
||||||
|
- name: Push public ssh key - {{ sftp_username }}
|
||||||
|
ansible.posix.authorized_key:
|
||||||
|
user: "{{ sftp_username }}"
|
||||||
|
key: "{{ sftp_pubkey }}"
|
||||||
|
state: present
|
||||||
5
roles/sftp_share/templates/sshd_config.conf.j2
Normal file
5
roles/sftp_share/templates/sshd_config.conf.j2
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
Match User {{ sftp_username }}
|
||||||
|
ChrootDirectory /primary/data/{{ sftp_username }}-uploads
|
||||||
|
ForceCommand internal-sftp -u 007 -p {{ sftp_permissions }} -d /transfers
|
||||||
|
AllowTcpForwarding no
|
||||||
|
X11Forwarding no
|
||||||
1
roles/sftp_share/vars/main.yml
Normal file
1
roles/sftp_share/vars/main.yml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
---
|
||||||
Reference in New Issue
Block a user