Skip to content

Commit

Permalink
Add Prometheus monitoring of the number of ssh sessions
Browse files Browse the repository at this point in the history
  • Loading branch information
lpsinger committed Mar 28, 2023
1 parent 6898b63 commit dae7259
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 3 deletions.
10 changes: 7 additions & 3 deletions server/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@ FROM debian:stable-slim
RUN \
# Turn off password authentication for ssh
echo openssh-server openssh-server/password-authentication boolean false | debconf-set-selections && \
# Install openssh-server
# Install openssh-server and Python dependencies for Prometheus collector
apt-get update && \
apt-get -y install --no-install-recommends openssh-server && \
apt-get -y install --no-install-recommends \
openssh-server \
python-is-python3 \
python3-click \
python3-prometheus-client && \
rm -rf /var/lib/apt/lists/* && \
# Nuke SSH host keys so that we regenerate them at container run time
rm /etc/ssh/ssh_host_* && \
Expand All @@ -15,7 +19,7 @@ RUN \
mkdir /run/sshd

COPY sshd_config /etc/ssh/sshd_config.d/00base.conf
COPY entrypoint.sh /
COPY entrypoint.sh sshd-prometheus-exporter.py /

ENTRYPOINT ["/entrypoint.sh"]
HEALTHCHECK --start-period=1s --interval=1s --timeout=1s CMD test -s /run/sshd.pid
3 changes: 3 additions & 0 deletions server/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,8 @@ then
install -D -o tunnel -g nogroup <(printf "%s\n" "${SSH_AUTHORIZED_KEYS}") ~tunnel/.ssh/authorized_keys
fi

# Start Prometheus collector
/sshd-prometheus-exporter.py &

# Start sshd
/usr/sbin/sshd -De
100 changes: 100 additions & 0 deletions server/sshd-prometheus-exporter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
#!/usr/bin/env python

import enum
import ipaddress
import logging
import time
import urllib

import click
import prometheus_client

log = logging.getLogger(__name__)

TcpState = enum.Enum('TcpState', [
'TCP_ESTABLISHED',
'TCP_SYN_SENT',
'TCP_SYN_RECV',
'TCP_FIN_WAIT1',
'TCP_FIN_WAIT2',
'TCP_TIME_WAIT',
'TCP_CLOSE',
'TCP_CLOSE_WAIT',
'TCP_LAST_ACK',
'TCP_LISTEN',
'TCP_CLOSING',
])


def parse_address(address: str):
host, port = address.split(':')
return ipaddress.ip_address(bytes.fromhex(host)), int(port, 16)


def parse_net_tcp(path: str):
"""Read and parse network statistics in the /proc/net/tcp format.
See https://www.kernel.org/doc/html/v5.10/networking/proc_net_tcp.html.
"""
with open(path) as f:
keys = next(f).split()
for line in f:
values = line.split()
mapping = dict(zip(keys, values))
mapping['sl'] = int(mapping['sl'].rstrip(':'))
mapping['local_address'] = parse_address(mapping['local_address'])
try:
mapping['remote_address'] = parse_address(mapping['remote_address'])
except KeyError:
mapping['remote_address'] = parse_address(mapping.pop('rem_address'))
mapping['st'] = TcpState(int(mapping['st'], 16))
yield mapping


connections = prometheus_client.Gauge(
'connections', 'Number of active SSH sessions', namespace='sshd')


filenames = ('/proc/net/tcp', '/proc/net/tcp6')


def filter_stat(mapping):
return (mapping['local_address'][1] == 22 and
mapping['st'] == TcpState.TCP_ESTABLISHED)


@connections.set_function
def count_connections():
return sum(
filter_stat(mapping)
for filename in filenames
for mapping in parse_net_tcp(filename))


def host_port(host_port_str):
# Parse netloc like it is done for HTTP URLs.
# This ensures that we will get the correct behavior for hostname:port
# splitting even for IPv6 addresses.
return urllib.parse.urlparse(f'http://{host_port_str}')


@click.command()
@click.option(
'--prometheus', type=host_port, default=':8000', show_default=True,
help='Hostname and port to listen on for Prometheus metric reporting')
@click.option(
'--loglevel', type=click.Choice(logging._levelToName.values()),
default='DEBUG', show_default=True, help='Log level')
def main(prometheus, loglevel):
logging.basicConfig(level=loglevel)

prometheus_client.start_http_server(prometheus.port,
prometheus.hostname or '0.0.0.0')
log.info('Prometheus listening on %s', prometheus.netloc)

while True:
time.sleep(1)


if __name__ == '__main__':
main()

0 comments on commit dae7259

Please sign in to comment.