a glimpse of ssh remote servers via sshls

Suppose one has access to many servers via ssh, the config file has been properly managed, sometimes it is still difficult to remember the alias for each server. It would be much better to run a command line to print out a list of server names and public key comments associated with the server from the ssh config file. In this article, a python based utility namely sshls1is presented so that you could make sure which server you want to connect.

ssh config

SSH config is just a plaintext file that contains info about how to connect to a server with a specific setup, so that users don’t need to provide parameters like hostname, user name, path to private key, etc over and over again.

Include ~/.ssh/ssh_config.d/*

Host aws-web01
    HostName aws-web01.amazon.com
    User admin
    IdentityFile ~/.ssh/aws-web01_key
    # admin@aws-web01.amazon.com (Production web server on AWS)

Host azure-db02-backup
    HostName azure-db02-backup.microsoft.com
    User backup
    IdentityFile ~/.ssh/azure-db02-backup_key
    # backup@azure-db02-backup.microsoft.com (Backup database server on Azure)

Host gcp-app-server-03
    HostName gcp-app-server-03.googlecloud.com
    User dev
    IdentityFile ~/.ssh/gcp-app-server-03_key
    # dev@gcp-app-server-03.googlecloud.com (Application server for development on GCP)

Host alicloud-file-server-04
    HostName alicloud-file-server-04.alibabacloud.com
    User files
    IdentityFile ~/.ssh/alicloud-file-server-04_key
    # files@alicloud-file-server-04.alibabacloud.com (Primary file storage server on Alibaba Cloud)

Host aws-proxy-05
    HostName aws-proxy-05.amazon.com
    User proxy
    IdentityFile ~/.ssh/aws-proxy-05_key
    # proxy@aws-proxy-05.amazon.com (Security proxy server on AWS)
Plaintext

typical approach

There are a few posts that stress on this issue, what they did are quite simple: use a one line command with regex, for example, sed. I used to incorporate this approach to my workflow, later on I found out some problems with this approach.

sed -n '/^Host.*[^*]$/s/Host //p' ~/.ssh/config;
sed -n '/^Host.*[^*]$/s/Host //p' ~/.ssh/ssh_config.d/*;
Bash

the road less travelled

flaws of the current approach

  • Lack of output format control: setting up a specific format using sed requires a user to dive deeper into sed or other command line tools.
  • Config directory management capability: sometimes one or multiple directories are referred, additional config files exist inside those directories. The tool needs to handle this situation properly.
  • Host name string search: a tool is able to search within the host name to list the servers that a user is interested.

In this article, a python based script is proposed to handle this task:

handling the config file

In order to gather info from ssh config file, this function is created as follows, to collect the servers info including server alias and private key path, as well as directories to other config files. Those strings could be extracted with regex. As the config file is organised by blocks for all servers, the order of server alias and identity file will still be paired without special process. Those info will be placed in the servers list.

def parse_ssh_config(file_path):
    servers = []
    included_files = []
    current_host = None
    key_file = None
    with open(file_path, 'r') as file:
        lines = file.readlines()
        for line in lines:
            match_host = re.match(r'^\s*Host\s+(.+)', line)
            match_include = re.match(r'^\s*Include\s+(.+)', line)
            match_identity_file = re.match(r'^\s*IdentityFile\s+(.+)', line)

            if match_host:
                current_host = match_host.group(1).strip()
                key_file = None  # Reset key file for the new host
            elif match_include:
                included_files.append(match_include.group(1).strip())
            elif match_identity_file and current_host:
                key_file = match_identity_file.group(1).strip()+'.pub'

            if current_host and key_file:
                servers.append( (current_host, key_file) )
                current_host = None  # Reset current host after capturing its key file

    return servers, included_files
Bash

getting all servers’ info

This is a recursive function that is used to manage all the other config files in the include paths. There could be a potential bug where 2 config files include each other so that the script runs infinitely.

def get_all_servers(main_config, processed_files=None):
    if processed_files is None:
        processed_files = set()

    servers, includes = parse_ssh_config(main_config)
    processed_files.add(main_config)

    for include_file in includes:
        for inc_file in pl.Path(os.path.expanduser(include_file)).parent.rglob('*'):
            if inc_file.is_file() and not inc_file.name.startswith(r'.') and inc_file not in processed_files:
                inc_servers, inc_includes = get_all_servers(inc_file)
                servers.extend(inc_servers)
                includes.extend(inc_includes)

    return servers, includes
Bash

getting the public key comment

The public key directory could be retrieved from the config file blocks, by parsing the path into the function, the public key file content could be read and by using regex with after substring of “== “, the first matched part could be retrieved, which is the comment.

# Function to extract comment from public key
def get_public_key_comment(public_key_path):
    public_key_path =pl.Path(os.path.expanduser(public_key_path))
    try:
        with open(public_key_path, 'r') as file:
            line = file.readline()
            match = re.search(r'== (.+)', line)
            if match:
                return match.group(1).strip()
    except IOError:
        return '(Key file not found)'
    return '(No comment)'
Python

main function

The main function could be used to tweak the output of the servers’ info. server_data variable contains the server alias and the comments. The output list could be sorted by alias order without considering upper&lower case. In addition, the maximum length of a server alias could be calculated, which could be used to format the output list via the last print line.

# Main function
def main():
    ssh_config_file = os.path.expanduser('~/.ssh/config')

    # Get all server names and their associated key files from ssh config
    server_data, _ = get_all_servers(ssh_config_file)
    # Sort server data by server name
    server_data.sort(key=lambda x: x[0].lower())

    # Get the longest server name for alignment
    max_length = max(len(name) for name, _ in server_data) if server_data else 0

    # Print server names and corresponding public key comments
    for server, key_file in server_data:
        comment = get_public_key_comment(key_file)
        print(f"{server.ljust(max_length)} : {comment}")
Python

sshls application demo

I use ChatGPT to generate a list of servers with their configurations as follows.

Generated List of Server Info with server alias and public key comments separated by colon.
aws-web01: admin@aws-web01.amazon.com (Production web server on AWS)
azure-db02-backup: backup@azure-db02-backup.microsoft.com (Backup database server on Azure)
gcp-app-server-03: dev@gcp-app-server-03.googlecloud.com (Application server for development on GCP)
alicloud-file-server-04: files@alicloud-file-server-04.alibabacloud.com (Primary file storage server on Alibaba Cloud)
aws-proxy-05: proxy@aws-proxy-05.amazon.com (Security proxy server on AWS)
company-mail-srv-06: mail@mail-srv-06.ibm.com (Corporate mail server)
harvard-test-env-07: tester@test-env-07.harvard.edu (Testing environment server at Harvard University)
ci-server-08: ci@ci-server-08.github.com (Continuous Integration server)
gcp-dev-api-09: api@dev-api-09.googlecloud.com (Development API server on GCP)
backup-node-10: backup@backup-node-10.nasa.gov (NASA's backup server)
stanford-cache-11: cache@cache-11.stanford.edu (Stanford University cache server)
company-data-warehouse-12: data@data-warehouse-12.apple.com (Apple's data warehouse)
prod-api-13: api@prod-api-13.microsoft.com (Production API server at Microsoft)
qa-server-14: qa@qa-server-14.startupxyz.com (QA server for testing at Startup XYZ)
log-server-15: logs@log-server-15.ibm.com (Log aggregation server at IBM)
monitor-16: monitor@monitor-16.microsoft.com (Monitoring server at Microsoft)
build-agent-17: build@build-agent-17.gitlab.com (Build agent for CI/CD at GitLab)
analytics-18: analytics@analytics-18.oracle.com (Analytics processing server at Oracle)
storage-node-19: storage@storage-node-19.amazon.com (Storage node at Amazon)
web-server-20: web@web-server-20.cloudflare.com (Web server at Cloudflare)
auth-server-21: auth@auth-server-21.facebook.com (Authentication server at Facebook)
aws-db-master-22: master@aws-db-master-22.amazon.com (Master database server on AWS)
redis-23: redis@redis-23.cachefly.com (Redis caching server at CacheFly)
vpn-gateway-24: vpn@vpn-gateway-24.cisco.com (VPN gateway server at Cisco)
api-gateway-25: gateway@api-gateway-25.salesforce.com (API gateway server at Salesforce)
bastion-host-26: bastion@bastion-host-26.splunk.com (Bastion host server at Splunk)
file-share-27: share@file-share-27.dropbox.com (File share server at Dropbox)
db-replica-28: replica@db-replica-28.oracle.com (Database replica server at Oracle)
bigdata-node-29: bigdata@bigdata-node-29.cloudera.com (Big data processing node at Cloudera)
backup-server-30: backup@backup-server-30.redhat.com (Backup server at Red Hat)
vpn-node-31: vpn@vpn-node-31.paloaltonetworks.com (VPN node at Palo Alto Networks)
mainframe-32: mainframe@mainframe-32.ibm.com (Mainframe server at IBM)
docker-host-33: docker@docker-host-33.docker.com (Docker host server at Docker)
ldap-server-34: ldap@ldap-server-34.okta.com (LDAP directory server at Okta)
app-dev-35: dev@app-dev-35.heroku.com (Development application server at Heroku)
dns-server-36: dns@dns-server-36.cloudflare.com (DNS server at Cloudflare)
web-cache-37: cache@web-cache-37.fastly.com (Web cache server at Fastly)
db-analytics-38: analytics@db-analytics-38.google.com (Database for analytics at Google)
media-server-39: media@media-server-39.netflix.com (Media streaming server at Netflix)
storage-array-40: storage@storage-array-40.purestorage.com (Storage array server at Pure Storage)
nfs-server-41: nfs@nfs-server-41.netapp.com (NFS shared server at NetApp)
vpn-access-42: vpn@vpn-access-42.fortinet.com (VPN access server at Fortinet)
intranet-portal-43: portal@intranet-portal-43.adobe.com (Intranet portal server at Adobe)
db-failover-44: failover@db-failover-44.mysql.com (Database failover server at MySQL)
log-collector-45: logs@log-collector-45.elastic.co (Log collector at Elastic)
iot-gateway-46: iot@iot-gateway-46.siemens.com (IoT gateway server at Siemens)
data-node-47: data@data-node-47.hpe.com (Data cluster node at HPE)
backup-dr-48: dr@backup-dr-48.vmware.com (Disaster recovery backup server at VMware)
compute-node-49: compute@compute-node-49.nvidia.com (Compute node at Nvidia)
k8s-master-50: k8s@k8s-master-50.kubernetes.io (Kubernetes master server at Kubernetes)
Plaintext

Those servers info could be used to generate a config file and test the sshls command. By running the sshls utility results in the following output:

alicloud-file-server-04  :  files@alicloud-file-server-04.alibabacloud.com (Primary file storage server on Alibaba Cloud)
analytics-18             :  analytics@analytics-18.oracle.com (Analytics processing server at Oracle)
api-gateway-25           :  gateway@api-gateway-25.salesforce.com (API gateway server at Salesforce)
app-dev-35               :  dev@app-dev-35.heroku.com (Development application server at Heroku)
auth-server-21           :  auth@auth-server-21.facebook.com (Authentication server at Facebook)
aws-db-master-22         :  master@aws-db-master-22.amazon.com (Master database server on AWS)
aws-proxy-05             :  proxy@aws-proxy-05.amazon.com (Security proxy server on AWS)
aws-web01                :  admin@aws-web01.amazon.com (Production web server on AWS)
azure-db02-backup        :  backup@azure-db02-backup.microsoft.com (Backup database server on Azure)
backup-dr-48             :  dr@backup-dr-48.vmware.com (Disaster recovery backup server at VMware)
backup-node-10           :  backup@backup-node-10.nasa.gov (NASA's backup server)
backup-server-30         :  backup@backup-server-30.redhat.com (Backup server at Red Hat)
bastion-host-26          :  bastion@bastion-host-26.splunk.com (Bastion host server at Splunk)
bigdata-node-29          :  bigdata@bigdata-node-29.cloudera.com (Big data processing node at Cloudera)
build-agent-17           :  build@build-agent-17.gitlab.com (Build agent for CI/CD at GitLab)
ci-server-08             :  ci@ci-server-08.github.com (Continuous Integration server)
company-data-warehouse-12:  data@data-warehouse-12.apple.com (Apple's data warehouse)
company-mail-srv-06      :  mail@mail-srv-06.ibm.com (Corporate mail server)
compute-node-49          :  compute@compute-node-49.nvidia.com (Compute node at Nvidia)
data-node-47             :  data@data-node-47.hpe.com (Data cluster node at HPE)
db-analytics-38          :  analytics@db-analytics-38.google.com (Database for analytics at Google)
db-failover-44           :  failover@db-failover-44.mysql.com (Database failover server at MySQL)
db-replica-28            :  replica@db-replica-28.oracle.com (Database replica server at Oracle)
dns-server-36            :  dns@dns-server-36.cloudflare.com (DNS server at Cloudflare)
docker-host-33           :  docker@docker-host-33.docker.com (Docker host server at Docker)
file-share-27            :  share@file-share-27.dropbox.com (File share server at Dropbox)
gcp-app-server-03        :  dev@gcp-app-server-03.googlecloud.com (Application server for development on GCP)
gcp-dev-api-09           :  api@dev-api-09.googlecloud.com (Development API server on GCP)
harvard-test-env-07      :  tester@test-env-07.harvard.edu (Testing environment server at Harvard University)
intranet-portal-43       :  portal@intranet-portal-43.adobe.com (Intranet portal server at Adobe)
iot-gateway-46           :  iot@iot-gateway-46.siemens.com (IoT gateway server at Siemens)
k8s-master-50            :  k8s@k8s-master-50.kubernetes.io (Kubernetes master server at Kubernetes)
ldap-server-34           :  ldap@ldap-server-34.okta.com (LDAP directory server at Okta)
log-collector-45         :  logs@log-collector-45.elastic.co (Log collector at Elastic)
log-server-15            :  logs@log-server-15.ibm.com (Log aggregation server at IBM)
mainframe-32             :  mainframe@mainframe-32.ibm.com (Mainframe server at IBM)
media-server-39          :  media@media-server-39.netflix.com (Media streaming server at Netflix)
monitor-16               :  monitor@monitor-16.microsoft.com (Monitoring server at Microsoft)
nfs-server-41            :  nfs@nfs-server-41.netapp.com (NFS shared server at NetApp)
prod-api-13              :  api@prod-api-13.microsoft.com (Production API server at Microsoft)
qa-server-14             :  qa@qa-server-14.startupxyz.com (QA server for testing at Startup XYZ)
redis-23                 :  redis@redis-23.cachefly.com (Redis caching server at CacheFly)
stanford-cache-11        :  cache@cache-11.stanford.edu (Stanford University cache server)
storage-array-40         :  storage@storage-array-40.purestorage.com (Storage array server at Pure Storage)
storage-node-19          :  storage@storage-node-19.amazon.com (Storage node at Amazon)
vpn-access-42            :  vpn@vpn-access-42.fortinet.com (VPN access server at Fortinet)
vpn-gateway-24           :  vpn@vpn-gateway-24.cisco.com (VPN gateway server at Cisco)
vpn-node-31              :  vpn@vpn-node-31.paloaltonetworks.com (VPN node at Palo Alto Networks)
web-cache-37             :  cache@web-cache-37.fastly.com (Web cache server at Fastly)
web-server-20            :  web@web-server-20.cloudflare.com (Web server at Cloudflare)
Bash
  1. ssh list[]

Posted

in

,

by

Tags:

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

🧭