replace the linux-only firewall patching with a portable exposure model
The catalogue publishes 51 compose files with the bare form 'ports: [80]',
which binds a random host port on 0.0.0.0: every service answers the internet.
ufw-docker existed to take that back afterwards, as root, on linux only,
because docker writes its own firewall rules and ufw never sees those ports.
Publishing where you mean to solves it at the source. Verified against the
daemon: '- 80' gives 0.0.0.0:32768, '127.0.0.1::80' gives 127.0.0.1:32769.
Same on macOS and on linux, no privilege, and visible in docker ps.
A stack binds with ${MYOS_BIND_PRIVATE|PUBLIC|MESH} and declares what it
means with <PREFIX>_SERVICE[_<port>]_EXPOSE. myos expose reads the resolved
compose configuration and reports what would be opened; --strict fails when a
port faces the world without declaring it, which is what an agent runs against
a server it did not set up.
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
#shellcheck shell=sh
|
||||
# myos expose [--strict] what the stacks publish, and to whom
|
||||
#
|
||||
# Reads the resolved compose configuration, so it reports what `myos up` would
|
||||
# open rather than what happens to be running. A port bound to 0.0.0.0 answers
|
||||
# the internet: on linux docker writes its own firewall rules and the host
|
||||
# firewall does not see it. --strict exits 1 when a port is world-bound
|
||||
# without the stack declaring that scope.
|
||||
myos_cmd_expose() {
|
||||
_strict=false
|
||||
case ${MYOS_ARGS:-}${MYOS_VARS:-} in *--strict*) _strict=true ;; esac
|
||||
|
||||
_rows=$(myos_expose_rows)
|
||||
[ -n "$_rows" ] || { printf 'no published port: nothing is reachable from outside the docker network\n'; return 0; }
|
||||
|
||||
printf '%s%-20s %-14s %-22s %-6s %s%s\n' \
|
||||
"$MYOS_C_HIGHLIGHT" STACK SERVICE "PUBLISHED ON" PORT SCOPE "$MYOS_C_RESET"
|
||||
_bad=0
|
||||
_oIFS=$IFS; IFS='
|
||||
'
|
||||
for _row in $_rows; do
|
||||
IFS=$_oIFS
|
||||
_st=${_row%%|*}; _rest=${_row#*|}
|
||||
_sv=${_rest%%|*}; _rest=${_rest#*|}
|
||||
_on=${_rest%%|*}; _rest=${_rest#*|}
|
||||
_pt=${_rest%%|*}; _sc=${_rest#*|}
|
||||
case ${_on%:*} in
|
||||
0.0.0.0|''|'::'|'*')
|
||||
[ "$_sc" = public ] || _bad=$((_bad + 1))
|
||||
printf '%-20s %-14s %s%-22s%s %-6s %s\n' \
|
||||
"$_st" "$_sv" "$MYOS_C_WARN" "$_on" "$MYOS_C_RESET" "$_pt" "$_sc" ;;
|
||||
*)
|
||||
printf '%-20s %-14s %-22s %-6s %s\n' "$_st" "$_sv" "$_on" "$_pt" "$_sc" ;;
|
||||
esac
|
||||
IFS='
|
||||
'
|
||||
done
|
||||
IFS=$_oIFS
|
||||
|
||||
if [ "$_bad" -gt 0 ]; then
|
||||
myos_warning "$_bad port(s) reachable from anywhere without declaring the public scope"
|
||||
myos_warning "bind them: ports: [\"\${MYOS_BIND_PRIVATE}::<port>\"]"
|
||||
[ "$_strict" = true ] && return "$MYOS_E_FAIL"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# myos_expose_rows STACK|SERVICE|ADDR:PORT|CONTAINER_PORT|SCOPE for every
|
||||
# published port of the requested stacks
|
||||
myos_expose_rows() {
|
||||
for _ref in $MYOS_STACKS; do
|
||||
_files=$(myos_stack_compose_files "$_ref" 2>/dev/null) || continue
|
||||
[ -n "$_files" ] || continue
|
||||
_fw=$(myos_framework_compose_files)
|
||||
[ -n "$_fw" ] && _files="$_files
|
||||
$_fw"
|
||||
_app=$(myos_stack_name "$_ref")
|
||||
_project=$(myos_project_name "$(myos_scope "$_ref")" "$USER" "$ENV" "$_app")
|
||||
DRYRUN=false myos_compose "$_project" "$_files" -- config 2>/dev/null |
|
||||
myos_expose_parse "$_ref" "$(myos_stack_prefix "$_ref")"
|
||||
done
|
||||
}
|
||||
|
||||
# myos_expose_parse STACK NAME (compose config on stdin)
|
||||
# compose normalises every port to the long form, so one shape is enough
|
||||
myos_expose_parse() {
|
||||
awk -v stack="$1" '
|
||||
/^services:/ { insvc = 1; next }
|
||||
insvc && /^ [a-zA-Z0-9_.-]+:/ { svc = $1; sub(/:$/, "", svc); inports = 0 }
|
||||
insvc && /^ ports:/ { inports = 1; next }
|
||||
inports && /^ [a-z]/ { inports = 0 }
|
||||
inports && /host_ip:/ { ip = $2 }
|
||||
inports && /published:/ { pub = $2; gsub(/"/, "", pub) }
|
||||
inports && /target:/ { tgt = $2 }
|
||||
inports && /protocol:/ {
|
||||
printf "%s|%s|%s:%s|%s\n", stack, svc, (ip == "" ? "0.0.0.0" : ip), pub, tgt
|
||||
ip = ""; pub = ""; tgt = ""
|
||||
}
|
||||
' | while IFS='|' read -r _s _v _o _t; do
|
||||
printf '%s|%s|%s|%s|%s\n' "$_s" "$_v" "$_o" "$_t" "$(myos_expose_scope "$2" "$_v" "$_t")"
|
||||
done
|
||||
}
|
||||
@@ -58,6 +58,10 @@ myos_context_defaults() {
|
||||
myos_default_HOST() { myos_addprefix "${HOSTNAME:-}." "$(myos_var DOMAIN)"; }
|
||||
myos_default_HOSTNAME() { printf '%s' "${HOSTNAME:-}"; }
|
||||
myos_default_DOMAINNAME() { myos_firstword "$(myos_var DOMAIN)"; }
|
||||
# the addresses a stack binds its published ports to
|
||||
myos_default_MYOS_BIND_PUBLIC() { myos_bind public; }
|
||||
myos_default_MYOS_BIND_PRIVATE() { myos_bind private; }
|
||||
myos_default_MYOS_BIND_MESH() { myos_bind mesh; }
|
||||
myos_default_MACHINE() { uname -m 2>/dev/null; }
|
||||
myos_default_SYSTEM() { uname -s 2>/dev/null; }
|
||||
myos_default_HOST_COMPOSE_PROJECT_NAME() { printf '%s' "${HOSTNAME:-}"; }
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
#shellcheck shell=sh
|
||||
# shellcheck disable=SC3028 # HOSTNAME is a myos variable, set by bin/myos
|
||||
# expose: which addresses a published port binds to.
|
||||
#
|
||||
# Docker writes its own firewall rules, so on linux a port published with
|
||||
# `-p 8080:80` answers the internet whatever the host firewall says. ufw-docker
|
||||
# patches that afterwards, on linux only, as root.
|
||||
#
|
||||
# The portable answer is to publish where you mean to in the first place:
|
||||
# `-p 127.0.0.1:8080:80` only ever listens on the loopback, identically on
|
||||
# linux and on macOS, with no firewall and no privilege. A stack says which
|
||||
# scope a port belongs to, and myos resolves the address.
|
||||
#
|
||||
# public the internet: a load balancer, a public DNS or mail service
|
||||
# mesh the private network between the hosts of the fleet
|
||||
# private this host only: everything the load balancer reaches for you
|
||||
#
|
||||
# MYOS_BIND_<SCOPE> overrides any of them.
|
||||
|
||||
# myos_bind SCOPE the address a port of that scope binds to
|
||||
myos_bind() {
|
||||
case $1 in
|
||||
public) printf '%s' "${MYOS_BIND_PUBLIC:-0.0.0.0}" ;;
|
||||
mesh) printf '%s' "${MYOS_BIND_MESH:-$(myos_bind_mesh)}" ;;
|
||||
private|*) printf '%s' "${MYOS_BIND_PRIVATE:-127.0.0.1}" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# myos_bind_mesh the address of the mesh interface, empty when there is none.
|
||||
# Falls back to the private address so that a stack scoped to the mesh on a
|
||||
# host that has none stays local rather than becoming public.
|
||||
myos_bind_mesh() {
|
||||
_if=${MYOS_MESH_IFACE:-}
|
||||
if [ -z "$_if" ]; then
|
||||
for _c in easytier tun0 tailscale0 mycelium wg0; do
|
||||
if myos_iface_addr "$_c" >/dev/null 2>&1 && [ -n "$(myos_iface_addr "$_c")" ]; then
|
||||
_if=$_c; break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
[ -n "$_if" ] || { printf '%s' "${MYOS_BIND_PRIVATE:-127.0.0.1}"; return 0; }
|
||||
_a=$(myos_iface_addr "$_if")
|
||||
[ -n "$_a" ] || _a=${MYOS_BIND_PRIVATE:-127.0.0.1}
|
||||
printf '%s' "$_a"
|
||||
}
|
||||
|
||||
# myos_iface_addr NAME the first address of an interface, on linux or macOS
|
||||
myos_iface_addr() {
|
||||
if myos_have ip; then
|
||||
ip -o addr show "$1" 2>/dev/null | awk '$3 ~ /^inet6?$/ {sub(/\/.*/,"",$4); print $4; exit}'
|
||||
elif myos_have ifconfig; then
|
||||
ifconfig "$1" 2>/dev/null | awk '$1 == "inet" || $1 == "inet6" {print $2; exit}'
|
||||
fi
|
||||
}
|
||||
|
||||
# myos_stack_prefix REF the prefix the settings of a stack use.
|
||||
# A host stack is prefixed by HOST_, which is how the catalogue names them:
|
||||
# HOST_FABIO_SERVICE_9998_TAGS, HOST_FTPS_UFW_DOCKER. Everything else uses the
|
||||
# stack name alone: SUPABASE_KONG_SERVICE_8000_TAGS.
|
||||
myos_stack_prefix() {
|
||||
_n=$(myos_upper "$(myos_stack_name "$1")")
|
||||
case $(myos_scope "$1") in
|
||||
host) printf 'HOST_%s' "$_n" ;;
|
||||
user) printf 'USER_%s' "$_n" ;;
|
||||
*) printf '%s' "$_n" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# myos_expose_scope PREFIX SERVICE PORT the scope a stack declares for a port:
|
||||
# <PREFIX>_SERVICE_<port>_EXPOSE, then <PREFIX>_SERVICE_EXPOSE, then the same
|
||||
# two on the service name, else private
|
||||
myos_expose_scope() {
|
||||
_u=$(myos_upper "$1")
|
||||
for _n in "${_u}_SERVICE_${3}_EXPOSE" "${_u}_SERVICE_EXPOSE" \
|
||||
"$(myos_upper "$2")_SERVICE_${3}_EXPOSE" "$(myos_upper "$2")_SERVICE_EXPOSE"; do
|
||||
_s=$(myos_var "$_n")
|
||||
[ -n "$_s" ] && { printf '%s' "$_s"; return 0; }
|
||||
done
|
||||
printf 'private'
|
||||
}
|
||||
Reference in New Issue
Block a user