diff --git a/Dockerfile b/Dockerfile index 908da7e..a713d48 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,8 +7,8 @@ COPY requirements.txt ./ RUN pip3 install -r requirements.txt - -# Set this to the port you chose in the prometheus-ssh-exporter.py file +# Set this to the port you want to expose EXPOSE 9999 -CMD ["python", "./prometheus-ssh-exporter.py"] \ No newline at end of file +# Set the -p option to the port you exposed above, defaults to 9999 +CMD ["python", "-u", "./prometheus-ssh-exporter.py","-p", "9999"] \ No newline at end of file diff --git a/README.md b/README.md index f0c2e6e..be9a384 100644 --- a/README.md +++ b/README.md @@ -3,4 +3,60 @@ A Prometheus exporter for monitoring SSH connections This is a personal project I wrote because I couldn't find any prometheus exporters that I could monitor my SSH connections with. -Note: The docker file is only for development. This program is not meant to be used in a docker container. \ No newline at end of file +## Installation +The recommended way is to use docker. The image is available on docker hub **flor0/prometheus-ssh-exporter** + +### Docker + +The container can be run using the docker command + +`docker run -d -p :9999 -v /run/utmp:/run/utmp flor0/prometheus-ssh-exporter` + +Simply change the \ to whatever port you want the server to listen to. The default listening port is 9999. + +### Docker compose + +Here is an example docker-compose file. +Change the \ to what you want the server to listen to. + +``` +version: "3" +services: + prometheus-ssh-exporter: + container_name: prometheus-ssh-exporter + image: flor0/prometheus-ssh-exporter + ports: + - :9999 + volumes: + - /run/utmp:/run/utmp + restart: unless-stopped +``` + +### As a python script +Alternatively, you can simply run the prometheus-ssh-exporter.py file with python3. + +The command line arguments are explained if you use `python3 ./prometheus-ssh-exporter.py -h` + +Make sure you set the right external port using the -p or --port argument. + +## Configuring Prometheus + +To have prometheus collect our new metrics, we need to add our server to the prometheus.yml file. +To do that open the /etc/prometheus/prometheus.yml file in an editor and add the lines +``` +- job_name: ssh + static_configs: + - targets: ['localhost:'] +``` +where you replace again the \ with the same port you used in the previous steps. Make sure it's indented correctly! + +## Usage + +You can go to your prometheus dashboard in the web browser and query ssh_num_sessions. +If everything is set up correctly you should get the metrics. + +### Grafana + +I have published a grafana dashboard that can be found at https://snapshots.raintank.io/dashboard/snapshot/bZSxJ2Ig9EkPbl0Ozt5lS9SYADtSlvIY + + diff --git a/prometheus-ssh-exporter.py b/prometheus-ssh-exporter.py index aaea9e8..0a0b087 100644 --- a/prometheus-ssh-exporter.py +++ b/prometheus-ssh-exporter.py @@ -1,26 +1,27 @@ import prometheus_client -import subprocess import time import argparse +import utmp +# These defaults can be overwritten by command line arguments SERVER_HOST = '0.0.0.0' -# Set this port to whatever you want this service to bind to SERVER_PORT = 9999 FETCH_INTERVAL = 5 + class Session: - """ - This class is used to create a Session object containing info on an SSH session - """ + """ This class is used to create a Session object containing info on an SSH session, mainly for readability + Only the fields name, tty, from_, login are actually used for now """ + def __init__(self, name, tty, from_, login, idle, jcpu, pcpu, what): - self.name = name - self.tty = tty - self.from_ = from_ - self.login = login - self.idle = idle - self.jcpu = jcpu - self.pcpu = pcpu - self.what = what + self.name = name # Username that is logged in + self.tty = tty # Which tty is used + self.from_ = from_ # remote IP address + self.login = login # time of login + self.idle = idle # unused + self.jcpu = jcpu # unused + self.pcpu = pcpu # unused + self.what = what # unused def __str__(self): return "%s %s" % (self.name, self.from_) @@ -32,6 +33,7 @@ class Session: return self.login == other.login and self.tty == other.tty and self.from_ == other.from_ def to_dict(self): + # maybe this will be used later return { 'name': self.name, 'tty': self.tty, @@ -50,13 +52,13 @@ class Session: def contains_user_list(user, other_user_list): for other_user in other_user_list: if are_equal(user, other_user): - #print("Found equals: %s and %s" % (user, other_user)) return True - #print("Not found: %s in %s" % (user, other_user_list)) return False def are_equal(user_list, other_user_list): + """ Two SSh sessions are equal if their name, tty, remote IP and login time are equal + The other fields change over time hence they are not used for comparison """ assert len(user_list) == len(other_user_list) for i in range(4): if user_list[i] != other_user_list[i]: @@ -64,47 +66,48 @@ def are_equal(user_list, other_user_list): return True -""" -Returns a list of Session objects -The string generated by the "w -h" command looks like follows: -USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT - pts/0 192.168.0.X 14:57 1:54m 0.12s 0.12s -bash -""" -def get_w_data(): - # Retrieve command output and discard the header - w_data = subprocess.check_output(['w', '-h']).decode('utf-8') - w_data = w_data.split('\n')[:-1] +def get_utmp_data(): + """ + Returns a list of User Objects + The function uses the utmp library. The utmp file contains information about currently logged in users + """ users = [] - # Generate a list of Session objects from the output of the command - for user_string in w_data: - user_string_cleaned = user_string.split() - if len(user_string_cleaned) == 8: - users.append(Session(*user_string_cleaned)) + with open('/var/run/utmp', 'rb') as f: + buffer = f.read() + for record in utmp.read(buffer): + if record.type == utmp.UTmpRecordType.user_process: + users.append(Session(record.user, record.line, record.host, record.sec, 0, 0, 0, 0)) return users + def parse_arguments(): + global FETCH_INTERVAL, SERVER_PORT, SERVER_HOST + parser = argparse.ArgumentParser( - prog='python prometheus-ssh-exporter.py', - description='Prometheus exporter for info about SSH sessions') - parser.add_argument('-H', '--host', type=str, default='0.0.0.0', help='Hostname to bind to') - parser.add_argument('-p', '--port', type=int, default=9999, help='Port for the server to listen to') - parser.add_argument('-i', '--interval', type=int, default=15, help='Interval in seconds to fetch SSH sessions data') + prog='python prometheus-ssh-exporter.py', + description='Prometheus exporter for info about SSH sessions') + parser.add_argument('-H', '--host', type=str, + default='0.0.0.0', help='Hostname to bind to') + parser.add_argument('-p', '--port', type=int, default=9999, + help='Port for the server to listen to') + parser.add_argument('-i', '--interval', type=int, default=15, + help='Interval in seconds to fetch SSH sessions data') + args = parser.parse_args() FETCH_INTERVAL = args.interval SERVER_PORT = args.port SERVER_HOST = args.host -""" -This program exports the number of SSH sessions as a metric "ssh_num_sessions" for prometheus. -It applies a label to each increment or decrement of that number, containing the remote IP address. -That way we can filter by the remote IP in Grafana, getting the number of SSH sessions by IP address, -or sum them up to get the total number of sessions. -""" - if __name__ == '__main__': + """ + This program exports the number of SSH sessions as a metric "ssh_num_sessions" for prometheus. + It applies a label to each increment or decrement of that number, containing the remote IP address. + That way we can filter by the remote IP in Grafana, getting the number of SSH sessions by IP address, + or sum them up to get the total number of sessions. + """ parse_arguments() @@ -112,8 +115,10 @@ if __name__ == '__main__': prometheus_client.start_http_server(SERVER_PORT) print("Started metrics server bound to {}:{}".format(SERVER_HOST, SERVER_PORT)) num_sessions = [] - gauge_num_sessions = prometheus_client.Gauge('ssh_num_sessions', 'Number of SSH sessions', ['remote_ip']) - data = get_w_data() + gauge_num_sessions = prometheus_client.Gauge( + 'ssh_num_sessions', 'Number of SSH sessions', ['remote_ip']) + # data = get_w_data() + data = get_utmp_data() list_data = [user.to_list() for user in data] # Initial metrics @@ -127,7 +132,8 @@ if __name__ == '__main__': while True: list_old_data = list_data - data = get_w_data() + # data = get_w_data() + data = get_utmp_data() list_data = [user.to_list() for user in data] num_sessions = len(data) @@ -135,13 +141,12 @@ if __name__ == '__main__': # Looking for newly found SSH sessions if not contains_user_list(list_data[i], list_old_data): print("Session connected: %s" % list_data[i]) - gauge_num_sessions.labels(remote_ip=list_data[i][2]).dec() - + gauge_num_sessions.labels(remote_ip=list_data[i][2]).inc() + for i in range(len(list_old_data)): # Looking for SSH sessions that no longer exist if not contains_user_list(list_old_data[i], list_data): print("Session disconnected: %s" % list_old_data[i]) - gauge_num_sessions.labels(remote_ip=list_old_data[i][2]).inc() + gauge_num_sessions.labels(remote_ip=list_old_data[i][2]).dec() time.sleep(FETCH_INTERVAL) - diff --git a/requirements.txt b/requirements.txt index 1efedd5..1c77ea8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ prometheus_client==0.16.0 +utmp==21.10.0 \ No newline at end of file