Merge pull request #35 from corvus-migratorius/add-sftp-share
Add sftp share
This commit is contained in:
@@ -17,6 +17,7 @@
|
|||||||
- [nginx](roles/nginx/README.md)
|
- [nginx](roles/nginx/README.md)
|
||||||
- [prometheus](roles/prometheus/README.md)
|
- [prometheus](roles/prometheus/README.md)
|
||||||
- [rclone_yandex](roles/rclone_yandex/README.md)
|
- [rclone_yandex](roles/rclone_yandex/README.md)
|
||||||
|
- [sftp_share](roles/sftp_share/README.md)
|
||||||
- [smartctl_exporter](roles/smartctl_exporter/README.md)
|
- [smartctl_exporter](roles/smartctl_exporter/README.md)
|
||||||
- [ufw](roles/ufw/README.md)
|
- [ufw](roles/ufw/README.md)
|
||||||
- [wg_hub](roles/wg_hub/README.md)
|
- [wg_hub](roles/wg_hub/README.md)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
namespace: genlab
|
namespace: genlab
|
||||||
name: common
|
name: common
|
||||||
version: 0.16.0
|
version: 0.17.0
|
||||||
readme: README.md
|
readme: README.md
|
||||||
authors:
|
authors:
|
||||||
- Alexander Gorelyshev (corvus-migratorius@proton.me)
|
- Alexander Gorelyshev (corvus-migratorius@proton.me)
|
||||||
|
|||||||
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
|
||||||
2
roles/sftp_share/defaults/main.yml
Normal file
2
roles/sftp_share/defaults/main.yml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
---
|
||||||
|
sftp_share_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.common.sftp_share
|
||||||
|
sftp_username: "testusr"
|
||||||
|
sftp_pubkey: "{{ lookup('file', 'ssh/id_ed25519.pub') }}"
|
||||||
|
sftp_root: "/primary/data"
|
||||||
|
sftp_transfers_groupname: "testgrp"
|
||||||
|
sftp_share_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_share_permissions
|
||||||
|
|
||||||
|
- name: Assert that user SFTP directory has correct owner
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- sftp_share_permissions.stat.pw_name == "root"
|
||||||
|
- sftp_share_permissions.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_share_permissions.stat.pw_name }}:{{ sftp_share_permissions.stat.gr_name }}"
|
||||||
|
|
||||||
|
- name: Gather transfers directory info
|
||||||
|
ansible.builtin.stat:
|
||||||
|
path: "{{ sftp_root }}/{{ sftp_username }}-uploads/transfers"
|
||||||
|
register: sftp_share_transfers_stat
|
||||||
|
|
||||||
|
- name: Assert that transfers directory has correct owner
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- sftp_share_transfers_stat.stat.pw_name == sftp_username
|
||||||
|
- sftp_share_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 {{ sftp_share_transfers_stat.stat.pw_name }}:{{ sftp_share_transfers_stat.stat.gr_name }}"
|
||||||
|
|
||||||
|
- name: Assert that transfers directory has correct permissions
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- sftp_share_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_share_permissions.stat.mode }}"
|
||||||
|
|
||||||
|
- name: Get user info
|
||||||
|
ansible.builtin.getent:
|
||||||
|
database: passwd
|
||||||
|
key: "{{ sftp_username }}"
|
||||||
|
register: sftp_share_user_info
|
||||||
|
|
||||||
|
- name: Check user exists
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- sftp_share_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: sftp_share_ssh.stdout != expected
|
||||||
|
register: sftp_share_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_share_sftp
|
||||||
|
failed_when: sftp_share_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_share_mkdir
|
||||||
|
failed_when: sftp_share_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'
|
||||||
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_share_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