我的问题

我的问题

我正在运行一个生产服务器(Debian 10,标准 OpenSSH 包),该服务器运行 Pure-FTPD 以用于旧连接,并运行 SFTP 以用于我们所有当前连接。SFTP 服务器设置了一个 chroot jail,它通过用户 chroot jail 中的绑定设备进行记录。rsyslog 会获取这些信息并将其发送到 /var/log/sftp.log,之后我使用 logstash 解析该文件并将所有内容转发到我们的超级用户的可视化服务器。超级用户登录可视化服务器即可在一个地方查看所有 SFTP 和 FTP/FTPS 日志。

pure-ftpd 日志的格式符合我们的超级用户喜欢的方式:

pure-ftpd: (testuser@hostname) [NOTICE] /home/ftpusers/testuser//outbound/testfile.pdf downloaded  (1765060 bytes, 5989.55KB/sec)

这很棒,因为它在一行中显示了确切的用户以及他们上传或下载的确切文件。但是,对于 SFTP 来说,情况就不那么好了:

internal-sftp[8848]: session opened for local user testuser from [{ip_address}]
internal-sftp[8848]: opendir "/inbound"
internal-sftp[8848]: realpath "/inbound/."
internal-sftp[8848]: open "/inbound/testfile.pdf" flags WRITE,CREATE,TRUNCATE mode 0666
internal-sftp[8848]: close "/inbound/testfile.pdf" bytes read 0 written 1734445

在这种情况下,日志很容易跟踪。testuser登录、写入文件、完成。但是我们一次有许多用户登录,并且来自多个 internal-sftp 实例的日志可能同时出现。如果发生这种情况,跟踪用户活动的唯一方法是搜索用户名testuser,找到记录的进程 ID(8848在上面的示例中),然后查找具有该进程 ID 的任何消息。许多用户通过 cronjob 登录,因此这种情况每 2 分钟左右发生一次……当我们有 300 个用户定期登录时,您可以想象搜索这么多进程 ID 会很麻烦。

我的问题

有没有办法在 sftp-internal 的每条日志消息前面加上生成日志的用户名称?这必须在 chroot jail 中才能工作。我找不到任何有关如何修改 rsyslog 生成的消息以包含用户名的信息。

我希望从我的 SFTP 日志中看到类似的内容:

internal-sftp[8848]: (testuser) open "/inbound/testfile.pdf" flags WRITE,CREATE,TRUNCATE mode 0666
internal-sftp[8848]: (testuser) close "/inbound/testfile.pdf" bytes read 0 written 1734445

当前配置状态

我的流程链如下:

ssh -> sftp-internal -> rsyslog(在 local3.* 上)-> 文件 /var/log/sftp.log -> logstash -> 导出到可视化服务器

摘自我的 /etc/ssh/sshd_config 中的 chroot 组

Match Group sftpusers 
        ChrootDirectory %h
        AuthorizedKeysFile %h/.ssh/authorized_keys
        ForceCommand internal-sftp -f local3 -l INFO
        # ForceCommand internal-sftp -l VERBOSE
        AllowTcpForwarding no
        X11Forwarding no

和我的 /etc/rsyslog.d/sftp.conf

local3.*        -/var/log/sftp.log

类似问题:

这个问题是关于 SFTP 记录到单独文件的内容,但它提到waybackmachine 条目中有一篇旧文章,其中包含漂亮的格式化 SFTP 日志条目,使其看起来像标准 xferlog。文章提到了一个 Perl 脚本(圣杯),它将为您格式化它,但遗憾的是,链接已失效。我可以编写一个 Python 或 Perl 脚本,查找传输的特定消息,获取进程 ID,并反向搜索以找到用户,然后将重新格式化的 xfer 消息与用户名一起打印到文件中。但肯定有人以前解决过这个问题,并且有更好的解决方案。

感谢您的任何帮助。

答案1

我能够使用 Python 和 systemd 构建解决方案。这是非常虽然快捷又粗糙,但对我而言还是有用的。我获取一个 sftp 内部日志文件并将其转储到重新格式化的文件中。我不修改原件,以防此格式化程序发生任何错误。

Python 脚本

这将注销 rsyslog 以进行监控,并响应来自 systemd 的 SEGINT。是的,这应该使用比列表更好的东西,但 python 没有内置环形缓冲区或正式排队系统(如果我遗漏了什么,请给我留言)。无论如何...这不是 C!

#!/usr/bin/python3

import logging
import re
import sys
import time


class SFTPLogFormatter:

    def __init__(self, infile: str, outfile: str):
        self.logger = logging.getLogger(__name__)
        self.logger.setLevel(logging.DEBUG)
        stdout_handler = logging.StreamHandler()
        stdout_handler.setLevel(logging.DEBUG)
        stdout_handler.setFormatter(logging.Formatter('%(levelname)8s | %(message)s'))
        self.logger.addHandler(stdout_handler)

        self.infile = open(infile, 'r')

        # append to file and keep only 1 lines in a write buffer (write almost
        # immediately)
        self.outfile = open(outfile, 'a', 1)

    def start(self):
        try:
            self.logger.info('starting formatter')
            self.run()
        except KeyboardInterrupt:
            self.logger.warning('SIGINT received, gracefully exiting')
            self.stop()

    @staticmethod
    def tail_file(file_obj):
        while True:
            line = file_obj.readline()
            # sleep if file hasn't been updated
            if not line:
                time.sleep(1)
                continue

            yield line

    def run(self):
        self.infile.seek(0, 2)  # jump to end of file for `tail -f` type behavior
        lines_read = []
        for line in self.tail_file(self.infile): # tail a file like `tail -f`
            lines_read.insert(0, line) # treat the list like a stack
            lines_read = lines_read[:2000] # trim stack since python does not have ring buffers

            modifyline_match = re.match(r'(.*)\[(\d+)\]: (open|close|remove name) (.*)', line) 
            if not modifyline_match:
                self.logger.info(line)
                self.outfile.write(line)
                continue

            modify_line_procid = modifyline_match.group(2)

            self.logger.debug(f'searching for session open statement for open|close file match string: \"{modifyline_match.group(0)}\"')
            open_session_regex = rf'.*\[{modify_line_procid}\]: session opened for local user (.*) from.*'
            open_session_match = None
            for prevline in lines_read[1:]:
                open_session_match = re.match(open_session_regex, prevline)
                if open_session_match:
                    self.logger.debug(f'found session open string: \"{open_session_match.group(0)}\"')
                    break
            else:
                # we found nothing
                self.logger.debug('could not find open session string for: \"{modifyline_match.group(0)}\"')
                continue

            modify_line_start = modifyline_match.group(1)
            modify_line_operator = modifyline_match.group(3)
            modify_line_details = modifyline_match.group(4)

            username = open_session_match.group(1)

            log_str = f'{modify_line_start}[{modify_line_procid}]: (user={username}) {modify_line_operator} {modify_line_details}\n'
            self.logger.info(log_str)
            self.outfile.write(log_str)

    def stop(self):
        self.logger.info('cleaning up')
        try:
            self.infile.close()
        except Exception as e:
            self.logger.error(f'failure while closing infile: {e}')

        try:
            self.outfile.close()
        except Exception as e:
            self.logger.error(f'failure while closing outfile: {e}')

        self.logger.info('exit')
        sys.exit(0)


if __name__ == '__main__':
    infile = sys.argv[1]
    outfile = sys.argv[2]
    service = SFTPLogFormatter(infile, outfile)
    service.start()

服务文件

以下服务文件已在 systemd 中创建并启用。

[Unit]
Description=Format log messages from sftp to have the username on any file reads, writes, and deletes, making multi-user logs much easier to read.
After=network.target

[Service]
User=root
Type=simple
ExecStart=/usr/bin/python3 /home/admin/services/format_sftp_logs_with_username.py /var/log/sftp.log /var/log/sftp_with_usernames.log
KillSignal=SIGINT

[Install]
WantedBy=multi-user.target

结果

这会导致以下日志消息。请注意 (user=XYZ) 的添加。

Feb 11 21:22:01 ip-10-20-0-96 internal-sftp[18241]: session opened for local user testuser from [127.0.0.1]
Feb 11 21:22:02 ip-10-20-0-96 internal-sftp[18241]: opendir "/"
Feb 11 21:22:02 ip-10-20-0-96 internal-sftp[18241]: closedir "/"
Feb 11 21:22:05 ip-10-20-0-96 internal-sftp[18241]: opendir "/inbound"
Feb 11 21:22:05 ip-10-20-0-96 internal-sftp[18241]: closedir "/inbound"
Feb 11 21:22:10 ip-10-20-0-96 internal-sftp[18241]: opendir "/inbound/"
Feb 11 21:22:10 ip-10-20-0-96 internal-sftp[18241]: closedir "/inbound/"
Feb 11 21:22:12 ip-10-20-0-96 internal-sftp[18241]: (user=testuser) open "/inbound/mailhog-deployment.yaml" flags READ mode 0666
Feb 11 21:22:12 ip-10-20-0-96 internal-sftp[18241]: (user=testuser) close "/inbound/mailhog-deployment.yaml" bytes read 815 written 0
Feb 11 21:22:13 ip-10-20-0-96 internal-sftp[18241]: opendir "/inbound/"
Feb 11 21:22:13 ip-10-20-0-96 internal-sftp[18241]: closedir "/inbound/"
Feb 11 21:22:14 ip-10-20-0-96 internal-sftp[18241]: opendir "/inbound/"
Feb 11 21:22:14 ip-10-20-0-96 internal-sftp[18241]: closedir "/inbound/"
Feb 11 21:22:14 ip-10-20-0-96 internal-sftp[18241]: (user=testuser) remove name "/inbound/mailhog-deployment.yaml"
Feb 11 21:22:18 ip-10-20-0-96 internal-sftp[18241]: (user=testuser) open "/inbound/mailhog-deployment.yaml" flags WRITE,CREATE,TRUNCATE mode 0644
Feb 11 21:22:18 ip-10-20-0-96 internal-sftp[18241]: (user=testuser) close "/inbound/mailhog-deployment.yaml" bytes read 0 written 815
Feb 11 21:22:19 ip-10-20-0-96 internal-sftp[18241]: session closed for local user testuser from [127.0.0.1]

限制

缓冲区有 2000 行后视功能来查找进程 ID。如果在特定时刻有数十或数百个用户登录,则增加该行。否则这应该可以满足大多数服务器的需求。

相关内容