我正在运行一个生产服务器(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。如果在特定时刻有数十或数百个用户登录,则增加该行。否则这应该可以满足大多数服务器的需求。