跳转到: 导航, 搜索

Solum/Logging

为了防止机密信息意外泄露给未经授权的 Solum 用户,有一些指导原则可以帮助隔离这些机密数据,以便在后端日志管理工具中轻松/准确地进行过滤。 为什么这很重要? 近期 OpenStack 在日志记录方面出现了一些安全问题

等等...

日志应具有一种格式,以便对机密数据进行分组,尤其是在记录诸如以下数据时:

  • 异常: 除非开发人员确定异常永远不会包含机密信息,否则应将异常标识为机密。 历史上,这在数据库异常方面尤其成问题,因为数据库异常可能包含实际字段数据。
    • 建议解析特定的异常或错误,并向用户提供抽象/安全版本
  • 密码: 绝不记录明文密码
  • 私钥: 绝不记录明文私钥
  • PII: 尽可能减少个人身份信息 (PII) 的日志记录
  • 本地服务器状态: 避免记录本地服务器状态,这些状态可能会为攻击者提供提示(示例:文件路径、代码文件名、用户帐户名称、PRNG 状态)
  • Tenant/Project ID 检查: 如果用户标识符(tenant/project ID)不在日志记录中,或者与当前经过身份验证的用户不匹配,则不要向用户显示此日志数据
  • 记录不安全配置: 如果 Solum 配置选项导致系统进入潜在的不安全状态,则记录一条消息供操作员查看


OpenStack 的 Oslo Log 能够创建带有机密数据部分的格式化日志。 以下示例包含两个变量数据:key_name,它不是机密数据(并且将等于“ssh”),以及 key_value,它是一个机密密钥,不应向 Solum 管理员/操作员以外的任何人可见。

注意:这是一个为简单起见而设计的示例。 如果 key_value 是公共 ssh 密钥,则可能没有必要将其从属于它的授权用户那里隐藏在日志中。 如果 key_value 是私有 ssh 密钥,则根本不应将其记录到日志中。

糟糕的示例

LOG.debug("用户设置 %s 密钥为值 %s" % [key_name, key_value])


修改/好的示例

LOG.debug("用户设置 %s 密钥" % [key_name], extra={private={value=key_value}})

请注意,extra->private 结构用于在日志中保存所有机密数据,以便在用户查看日志之前可以将其过滤掉。 在此示例中,密钥值被移动到“private”字典中,这使得从日志中过滤掉机密数据更容易,因为如果 Solum 遵循这些指南,将有一个关键字用于在日志条目中查找。 经过身份验证的用户可以看到 ssh 密钥已更改,但 Solum 操作员可以在日志中看到实际的 ssh 密钥值。


日志级别定义:http://stackoverflow.com/questions/2031163/when-to-use-log-level-warn-vs-error 这是一篇关于何时使用每个日志级别的很好的文章。 以下是简要说明

  • Debug:显示所有内容,并且由于生成的日志量巨大,可能不适合正常的生产操作
  • Info:通常指示成功的服务启动/停止、版本等非错误相关数据
  • Warning:指示可能存在系统性问题;潜在的预测性故障通知
  • Error:发生错误,管理员应研究该事件
  • Critical:发生错误,系统可能不稳定;立即寻求管理员的帮助


Oslo Log 和 Oslo Context 示例

这是 Oslo log 和 Oslo context 组合的示例

这演示了操纵记录的上下文数据的能力并修改格式。 这使得能够根据使用的日志格式过滤某些私有数据。 当将新的上下文添加到 RequestContext(或者在大多数情况下继承的类)时,日志格式可能会更改,以便将新的上下文添加到日志中。 solum.conf.sample 文件具有 logging_context_format_string 配置值,该值可以通过配置文件启用修改上下文日志记录。

优点

  • 使用所有基本的 Oslo 代码并模仿其他项目

缺点

  • 某些 Oslo RequestContext 字段可能不适用于 Solum,并且需要忽略
  • 对机密数据的控制和识别可能不太直观。 只有 Oslo Log 的 logging_context_format_string 定义了这一点。
    • 审查员必须仔细审查所有未来的日志条目,才能成功防止机密数据泄露。


from oslo.config import cfg
from solum.openstack.common import context
from solum.openstack.common import log

LOG = log.getLogger(__name__)
log.setup('solum')
CONTEXT = context.RequestContext(user='John Doe', tenant='test tenant')
LOG.error('Standard context log output', context=CONTEXT)

CONF = cfg.CONF
CONF.logging_context_format_string = (
    "%(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s "
    "{'private': {'user': %(user)s, 'tenant': %(tenant)s}} "
    "%(instance)s%(message)s]}")
LOG.error('Modified context log output', context=CONTEXT)

输出

2014-01-23 20:48:30.156 22345 ERROR __main__ [req-1a007cc5-cce8-4a63-8572-99e1148c784c John Doe test tenant] Standard context log output
2014-01-23 20:48:30.157 22345 ERROR __main__ {'private': {'user': John Doe, 'tenant': test tenant}} Modified context log output]}

Oslo Log 和 Oslo Context 与 SecurityContext 结合示例

SecurityContext 继承 Oslo RequestContext 并添加了此拉取请求中的一些功能:https://review.openstack.org/#/c/63201/ ... 识别公共(用户可以看到此数据的日志/通知)与私有(只有 Solum 操作员才能看到此数据)。

注意:这未经测试、国际化、使用了错误的异常等。 它只是一个可能的架构示例,用于与社区讨论。

优点

  • 轻松、直观地识别和分类机密数据
  • 对 get_pub() 和 get_priv() 的基本机密性强制执行(get_pub() 的机密数据将导致异常)
  • 通过使用 set_pub() 或 set_priv() 可以扩展以添加更多上下文字段
  • 自动结构化上下文日志记录输出
  • 可以在完成字段后添加清除机密数据内存的功能
  • 数据的公开/私有指定发生在数据被放入安全上下文时,而不是在日志记录时。 这比在日志记录时进行操作更安全。

缺点

  • 也许有点过多的开销/过度设计
  • 性能下降?
  • 可能存在尚未发现的集成复杂性


from oslo.config import cfg
from solum import security_context
from solum.openstack.common import log

LOG = log.getLogger(__name__)
log.setup('solum')
CONTEXT = security_context.SecurityContext()
CONTEXT.set_pub(user='Bill', blahkey='blah value')
CONF = cfg.CONF
CONF.logging_context_format_string = (
    "%(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s "
    "---%(public_context)s---, ===%(private_context)s=== "
    "%(instance)s%(message)s]}")
LOG.error('Modified context log output', context=CONTEXT)

print CONTEXT.get_pub('user')
print "Now an error case, try to get user from private data..."
print CONTEXT.get_priv('user')

SecurityContext 类

from solum.openstack.common import context


class SecurityContext(context.RequestContext):
    def __init__(self):
        super(self.__class__, self).__init__()
        self.read_only = None
        self.show_deleted = None
        self.is_admin = None
        self._priv_data = {}
        self._pub_data = {}

    def set_pub(self, **kwargs):
        for key, val in kwargs.iteritems():
            if key in self._priv_data:
                raise Exception("%s was previously marked as private." % (
                    key))
            self.__dict__[key] = val
            self._pub_data[key] = val

    def get_pub(self, search_key):
        return self._pub_data[search_key]

    def set_priv(self, **kwargs):
        for key, val in kwargs.iteritems():
            if key in self._pub_data:
                raise Exception("%s was previously marked as public." % (
                    key))
            self.__dict__[key] = val
            self._priv_data[key] = val

    def get_priv(self, search_key):
        return self._priv_data[search_key]

    def to_dict(self):
        # Oslo Log requires a 'request_id' value!
        return {'request_id': self.request_id,
                'public_context': self._pub_data,
                'private_context': self._priv_data}

输出

2014-01-27 21:40:07.641 8206 ERROR __main__ ---{'user': 'Bill', 'blahkey': 'blah value'}---, ==={}=== Modified context log output]}
Bill
Now an error case, try to get user from private data...
2014-01-27 21:40:07.642 8206 CRITICAL solum [-] 'user'