Browse Source

new: [vsftp] new charm

framadate
Valentin Lab 5 years ago
parent
commit
0070661495
  1. 5
      vsftp/build/Dockerfile
  2. 300
      vsftp/hooks/init
  3. 292
      vsftp/lib/common
  4. 41
      vsftp/metadata.yml

5
vsftp/build/Dockerfile

@ -0,0 +1,5 @@
FROM alpine:3.7
RUN apk add --no-cache vsftpd
CMD /usr/sbin/vsftpd /etc/vsftpd/vsftpd.conf

300
vsftp/hooks/init

@ -0,0 +1,300 @@
#!/bin/bash
## Init is run on host
## For now it is run every time the script is launched, but
## it should be launched only once after build.
## Accessible variables are:
## - SERVICE_NAME Name of current service
## - DOCKER_BASE_IMAGE Base image from which this service might be built if any
## - SERVICE_DATASTORE Location on host of the DATASTORE of this service
## - SERVICE_CONFIGSTORE Location on host of the CONFIGSTORE of this service
. lib/common || exit 1
CONFIGFILE="$SERVICE_CONFIGSTORE/etc/vsftpd/vsftpd.conf"
VSFTP_OPTIONS=(
##
## Feature not yet supported (and removed for simplicity's sake, and
## because implication were not though through yet), or in dire need
## of YAML simplification in the options.
##
## No support for anon user for now (for simplification purpose)
#"allow_anon_ssl:bool" "anon_mkdir_write_enable:bool" "anon_other_write_enable:bool"
#"anon_upload_enable:bool" "anon_world_readable_only:bool" "anonymous_enable:bool"
#"force_anon_data_ssl:bool" "force_anon_logins_ssl:bool"
#"no_anon_password:bool"
#"anon_root:string" "chown_username:string" "ftp_username:string"
#"anon_max_rate:numeric" "anon_umask:numeric"
## No support for chroot list
#"chroot_list_enable:bool"
#"connect_from_port_20:bool" ## port not propagated yet to host
## No support for that (requires more YAML love to list emails)
#"secure_email_list_enable:bool"
#"user_config_dir:string"
## To simplify usage
#"userlist_deny:bool"
#"userlist_enable:bool"
##
## Not sure if we need that
##
#"deny_email_enable:bool"
#"guest_enable:bool" "setproctitle_enable:bool"
#"session_support:bool" "tcp_wrappers:bool"
#"banned_email_file:string" "guest_username:string"
#"pam_service_name:string" "ftp_data_port:numeric"
##
## Set by design
##
## Alway background by design (as a dockerized process)
# "background:bool" "listen:bool"
## Using local user functionality
#"chroot_local_user:bool"
#"local_enable:bool"
#"passwd_chroot_enable:bool"
#"virtual_use_local_privs:bool"
#"chroot_list_file:string"
#"email_password_file:string"
#"user_sub_token:string"
#"userlist_file:string"
## Logging options are simplified
#"dual_log_enable:bool" "log_ftp_protocol:bool" "no_log_lock:bool"
#"syslog_enable:bool"
#"xferlog_enable:bool" "xferlog_std_format:bool"
#"vsftpd_log_file:string" "xferlog_file:string"
## Managed by ``ssl`` options
#"implicit_ssl:bool"
#"ssl_enable:bool" "ssl_request_cert:bool" "ssl_sslv2:bool" "ssl_sslv3:bool"
#"ssl_tlsv1:bool"
#"ca_certs_file:string"
#"dsa_cert_file:string"
#"dsa_private_key_file:string"
#"rsa_cert_file:string"
#"rsa_private_key_file:string"
#"ssl_ciphers:string"
## Obscure option not really needed
#"one_process_model:bool"
## Managed by ``pasv`` options
#"pasv_addr_resolve:bool" "pasv_enable:bool"
#"pasv_promiscuous:bool"
#"pasv_max_port:numeric" "pasv_min_port:numeric"
#"pasv_address:string"
## We use root user in docker to launch vsftp
#"run_as_launching_user:bool"
## We don't need to change this in docker
#"listen_port:numeric"
#"listen_address:string" "listen_address6:string"
## Hum, should not be of charm users concern
#"nopriv_user:string"
#"secure_chroot_dir:string"
##
## Permitted
##
## commands
"chmod_enable:bool" "dirlist_enable:bool" "download_enable:bool"
"port_enable:bool" "ls_recurse_enable:bool" "write_enable:bool"
## others
"dirmessage_enable:bool" "ascii_download_enable:bool"
"ascii_upload_enable:bool" "async_abor_enable:bool"
"check_shell:bool" "chown_uploads:bool" "debug_ssl:bool"
"delete_failed_uploads:bool" "force_dot_files:bool"
"hide_ids:bool" "listen_ipv6:bool" "lock_upload_files:bool"
"mdtm_write:bool" "port_promiscuous:bool" "require_cert:bool"
"require_ssl_reuse:bool" "strict_ssl_read_eof:bool"
"strict_ssl_write_shutdown:bool" "text_userdb_names:bool"
"tilde_user_enable:bool" "use_localtime:bool" "use_sendfile:bool"
"validate_cert:bool"
## timeouts
"accept_timeout:numeric" "connect_timeout:numeric" "data_connection_timeout:numeric"
"idle_session_timeout:numeric"
## delays
"delay_failed_login:numeric" "delay_successful_login:numeric"
## modes
"chown_upload_mode:numeric" "file_open_mode:numeric"
"local_max_rate:numeric" "local_umask:numeric"
"max_clients:numeric" "max_login_fails:numeric" "max_per_ip:numeric"
"trans_chunk_size:numeric"
"banner_file:string"
"cmds_allowed:string" "cmds_denied:string"
"deny_file:string" "ftpd_banner:string" "hide_file:string"
"local_root:string" "message_file:string"
"force_local_data_ssl:bool" "force_local_logins_ssl:bool"
"allow_writeable_chroot:bool"
)
VSFTP_OPTIONS_CONCAT=" ${VSFTP_OPTIONS[*]} "
get_ips_from_hostname() {
local hostname="$1"
getent hosts "$hostname" | awk '{ print $1 }'
}
get_ip_from_hostname() {
local hostname="$1"
get_ips_from_hostname "$hostname" | head -n 1
}
mkdir -p "$(dirname "$CONFIGFILE")"
service_def=$(get_compose_service_def "$SERVICE_NAME") || return 1
domain=
users=
pasv=
ssl=
while read-0 key val; do
key_option=${key//-/_}
case "$VSFTP_OPTIONS_CONCAT" in
*" ${key_option}:bool "*)
case "${val,,}" in
true|ok|yes|y)
val=yes
;;
false|ko|nok|no|n)
val=no
;;
*)
die "Invalid value for ${WHITE}$key$NORMAL, please use a boolean value."
;;
esac
;;
*" ${key_option}:numeric "*)
if ! is_int "$val"; then
die "Invalid value for ${WHITE}$key$NORMAL, please use numeric value."
fi
;;
*" ${key_option}:string "*)
:
;;
*)
case "${key//_/-}" in
users) users="$val";;
pasv) pasv="$val";;
ssl) ssl="$val";;
domain) domain="$val";;
*) die "Unknown option ${WHITE}$key$NORMAL.";;
esac
continue
;;
esac
case "$key_option" in
allow_writeable_chroot)
allow_writeable_chroot=1
;;
esac
printf "%s=%s\n" "$key_option" "$val"
done < <(printf "%s" "$service_def" | shyaml key-values-0 options) > "$CONFIGFILE"
if [ -z "$domain" ]; then
if [[ "$SERVICE_NAME" =~ ^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$ ]]; then
domain="$SERVICE_NAME"
fi
fi
make_pasv_options "$pasv" "$domain" >> "$CONFIGFILE" || exit 1
make_ssl_options "$ssl" "$domain" >> "$CONFIGFILE" || exit 1
## Logs
cat <<EOF >> "$CONFIGFILE"
##
## Logs
##
syslog_enable=no
dual_log_enable=no
vsftpd_log_file=/var/log/vsftp/vsftp.log
xferlog_enable=yes
xferlog_std_format=no
EOF
## Dockerisation means those
cat <<EOF >> "$CONFIGFILE"
##
## Dockerisation
##
background=no
## we don't use that feature
passwd_chroot_enable=no
##
## Use local system passwd account
##
local_enable=yes
chroot_local_user=yes
## Seem to be required to avoid 500 OOOps child died
seccomp_sandbox=no
EOF
##
## Users
##
## Note: this creates a file that will be interpreted in the
## entrypoint of the docker image. It also creates group with same gid
## on host to be able to easily share files.
build_script="$(make_build_script "$users" "$allow_writeable_chroot")" || exit 1
echo "build-script:" >&2
echo "$build_script" | prefix " $GRAY|$NORMAL " >&2
docker_update "$SERVICE_NAME" "$build_script" -v "$SERVICE_DATASTORE/home":"/home" || exit 1
config_hash=$(
{
printf "%s\0" "$build_script" "$(cat "$CONFIGFILE")"
} | md5_compat) || exit 1
init-config-add "
$MASTER_BASE_SERVICE_NAME:
labels:
- compose.config_hash=$config_hash
"

292
vsftp/lib/common

@ -0,0 +1,292 @@
# -*- mode: shell-script -*-
ssl_get_plugin_fun() {
local cfg="$1"
type="$(echo "$cfg" | shyaml -y get-type 2>/dev/null)" || return 1
case "$type" in
bool)
err "Option ${WHITE}ssl${NORMAL} value '$cfg' is not supported."
return 1
;;
str)
keys=("$cfg")
;;
struct)
keys=( $(echo "$cfg" | shyaml keys 2>/dev/null) )
;;
*)
err "Invalid ${WHITE}ssl${NORMAL} value type '$type': please provide a string or a struct."
return 1
;;
esac
for key in "${keys[@]}"; do
target_relation="cert-provider"
fun="ssl_plugin_${target_relation}"
while read-0 relation_name target_service relation_config tech_dep; do
[ "$relation_name" == "${target_relation}" ] || continue
[ "$target_service" == "$key" ] || continue
verb "Corresponding plugin ${DARKGREEN}found${NORMAL}" \
"in ${DARKBLUE}$relation_name${NORMAL}/${DARKYELLOW}$key${NORMAL}"
ssl_cfg=$(printf "%s" "$cfg" | shyaml get-value "$key" 2>/dev/null) || true
merged_config=$(merge_yaml_str "$relation_config" "$ssl_cfg") || return 1
printf "%s\0" "$fun" "$key" "$merged_config"
return 0
done < <(get_service_relations "$SERVICE_NAME") || return 1
case "$key" in
cert|ca-cert|key)
:
;;
*)
err "Invalid key '$key' in ${WHITE}ssl${NORMAL}:" \
"no corresponding services declared in ${DARKBLUE}${target_relation}$NORMAL"
return 1
;;
esac
done
}
ssl_fallback_vars() {
local cfg="$1" DOMAIN="$2" cert key ca_cert
base_dir=/etc/ssl
print "%s\0" \
"$base_dir"/certs/"${DOMAIN}".pem \
"$base_dir"/private/"${DOMAIN}".key \
"$base_dir"/certs/"${DOMAIN}"-ca.pem
print "%s\0" "$base_dir"/cert.pem "$base_dir"/privkey.pem "$base_dir"/chain.pem
}
ssl_fallback_prepare() {
local cfg="$1" service="$2" l_cert l_key l_ca_cert
shift 2
read-0 l_cert l_key l_ca_cert < <(ssl_fallback_vars "$cfg" "$@")
dst="$CONFIGSTORE/$BASE_SERVICE_NAME"
volumes=""
for label in cert key ca-cert; do
if content=$(echo "$cfg" | shyaml get-value "$label" 2>/dev/null); then
location="$(eval echo "\$l_${label//-/_}")"
echo "$content" | file_put "$dst$location"
config_hash=$(printf "%s\0" "$config_hash" "$label" "$content" | md5_compat)
volumes="$volumes
- $dst$location:$location:ro"
fi
done
if [ "$volumes" ]; then
init-config-add "\
$MASTER_TARGET_SERVICE_NAME:
volumes:
$volumes
"
fi
}
ssl_plugin_cert-provider_vars() {
local cfg="$1" DOMAIN="$2"
base_dir="/etc/letsencrypt/live/$DOMAIN"
print "%s\0" "$base_dir"/cert.pem "$base_dir"/privkey.pem "$base_dir"/chain.pem
}
ssl_plugin_cert-provider_prepare() {
local cfg="$1" service="$2" options
shift 2
options=$(yaml_key_val_str "options" "$cfg") || return 1
service_config=$(yaml_key_val_str "$service" "$options")
compose --debug --add-compose-content "$service_config" run --rm --service-ports "$service" \
crt create "$@" || {
err "Failed to launch letsencrypt for certificate creation."
return 1
}
init-config-add "\
$MASTER_TARGET_SERVICE_NAME:
volumes:
- $DATASTORE/$service/etc/letsencrypt:/etc/letsencrypt:ro
" || return 1
}
make_pasv_options() {
local pasv="$1"
if [[ "${pasv,,}" =~ ^(false|no|n|0|disable|disabled)$ ]]; then
echo "pasv_enable=no"
return 0
fi
ports="$(printf "%s" "$pasv" | shyaml get-value -q "ports")"
if [ "$ports" ]; then
if [[ "$ports" =~ ^[0-9]+-[0-9]+$ ]]; then
port_min=${ports##*-}
port_max=${ports%%-*}
if [ "$port_min" -gt "$port_max" ]; then
die "Invalid value for ${WHITE}ports${NORMAL}: first port in range has to be lesser than second."
fi
else
die "Invalid value for ${WHITE}ports${NORMAL}: please specify a port range (ie: '123-345')."
fi
fi
port_min=${port_min:-21100}
port_max=${port_max:-21110}
host_address="$(printf "%s" "$pasv" | shyaml get-value -q "address")"
if [ -z "$host_address" ]; then
if [[ "$SERVICE_NAME" =~ ^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$ ]]; then
host_address="$SERVICE_NAME"
fi
fi
if [ -z "$host_address" ]; then
echo "## Could not find appropriate host return address"
echo "pasv_enable=no"
return 0
fi
if ! str_is_ipv4 "$host_address"; then
host_address="$(get_ip_from_hostname "$host_address")" || {
err "Can't resolve given hostname '$host_address'"
return 1
}
else
err "You should NOT provide a direct IP for ${WHITE}pasv.address${NORMAL}," \
"try a resolvable address."
return 1
fi
cat <<EOF
pasv_address=$host_address
pasv_enable=yes
pasv_min_port=$port_min
pasv_max_port=$port_max
EOF
init-config-add "
$SERVICE_NAME:
ports:
- \"$port_min-$port_max:$port_min-$port_max\"
"
}
make_ssl_options() {
local ssl="$1"
if [ -z "$ssl" ]; then
target_relation="cert-provider"
ssl=no
while read-0 rn ts rc td; do
[ "$rn" == "${target_relation}" ] || continue
info "A ${DARKBLUE}$target_relation${NORMAL} ${DARKYELLOW}$ts${NORMAL} declared as 'ssl' value"
ssl="$ts"
break
done < <(get_service_relations "$SERVICE_NAME")
fi
if [[ "${ssl,,}" =~ false|no|n|0|disable|disabled ]]; then
echo "ssl_enable=no"
return 0
fi
cat <<EOF
##
## SSL conf
##
ssl_enable=yes
ssl_tlsv1=yes
ssl_sslv2=no
ssl_sslv3=no
ssl_ciphers=HIGH
implicit_ssl=no
EOF
read-0 SSL_PLUGIN_FUN SSL_CFG_VALUE SSL_CFG_OPTIONS < <(ssl_get_plugin_fun "$ssl") || return 1
read-0 cert key ca_cert < <("$SSL_PLUGIN_FUN"_vars "$SSL_CFG_OPTIONS" "$SSL_CFG_VALUE") || return 1
"$SSL_PLUGIN_FUN"_prepare "$SSL_CFG_OPTIONS" "$SSL_CFG_VALUE" || return 1
cat <<EOF
rsa_cert_file=$cert
rsa_private_key_file=$key
ca_certs_file=$ca_cert
EOF
}
make_build_script() {
local users_def="$1" allow_writeable_chroot="$2"
if [ -z "$users_def" ]; then
return 0
fi
fixed_groups_code=""
groups_code=""
declare -A created_groups
while read-0 user user_def; do
first_group=
groups=()
first=1
while read-0 group; do
[ "${created_groups[$group]}" ] && continue
if [[ "$group" == *":"* ]]; then
gid=${group##*:}
group=${group%%:*}
fixed_groups_code+="addgroup -g \"$gid\" \"$group\""$'\n'
else
groups_code+="addgroup \"$group\""$'\n'
fi
created_groups[$group]=1
echo "X" >&2
if [ "$first" ]; then
first_group="$group"
first=
else
remaining_groups+=("$group")
fi
groups+=("$group")
done < <(echo "$user_def" | shyaml get-values-0 groups 2>/dev/null)
password=$(echo "$user_def" | shyaml get-value password 2>/dev/null) ||
password=$(gen_password 14)
uid=$(echo "$user_def" | shyaml get-value uid 2>/dev/null)
useradd_options=(
"-D" ## don't assign a password
"-s" "/bin/false" ## default shell
)
if [ "$uid" ]; then
useradd_options+=("-u" "$uid") ## force uid
fi
if [ "$first_group" ]; then
useradd_options+=("-G" "$first_group") ## force main group
fi
code+="adduser ${useradd_options[*]} \"$user\""$'\n'
code+="mkdir -p \"/home/$user\""$'\n'
if [ "$allow_writeable_chroot" ]; then
code+="chown $user \"/home/$user\""$'\n' ## sanitize
else
code+="chown root:root \"/home/$user\""$'\n' ## sanitize
fi
code+="chmod 755 \"/home/$user\""$'\n' ## sanitize
code+="echo '$user:$password' | chpasswd"$'\n'
for group in "${remaining_groups[@]}"; do
code+="adduser \"$user\" \"$group\""$'\n'
done
done < <(echo "$users_def" | shyaml key-values-0)
echo -n "$fixed_groups_code"
echo -n "$groups_code"
echo -n "$code"
}

41
vsftp/metadata.yml

@ -0,0 +1,41 @@
config-resources:
- /etc/vsftpd/vsftpd.conf
data-resources:
- /home
- /var/log/vsftp
default-options:
ftpd-banner: "Welcome to FTP Server"
dirmessage-enable: yes
delay-failed-login: 2
max-clients: 10
max-per-ip: 5
max-login-fails: 3
## to check what these are
write-enable: yes
local-umask: "022" ## remember to send a string here
users:
docker-compose:
ports:
- "21:21"
provides:
ftp-access:
tech-dep: reversed
uses:
log-rotate:
constraint: recommended
auto: pair
solves:
disk-leak: "/var/log/vsftp"
cert-provider:
constraint: optional
auto: pair
limit: n
solves:
feature: "SSL certificate generation"
Loading…
Cancel
Save