photo credits: clint adair

TLS 和网络

TLS(传输层安全协议)是 SSL(安全套接字层)的兼容后继者,是…的基础
首页 » 博客 » TLS 和网络

这是系列中的第三篇帖子: “Python Web 应用程序开发人员必须了解的关于安全性的最基本知识。”

传输层安全协议

TLS(传输层安全协议)是 SSL(安全套接字层)的兼容后继者,是 “https” 安全 Web 流量的基础,并提供经过身份验证的加密。

过时的 TLS 版本允许不安全的算法。我们必须确保仅支持正确的 TLS 版本。渗透测试或自动化扫描工具可以验证这一点。

TLS 是一种混合密码系统:它同时使用对称和非对称算法。例如,非对称算法(如签名算法)可用于验证对等方身份,而公钥加密算法或 DiffieHellman 交换可用于协商共享密钥和验证证书。在对称方面,流密码(包括本机密码和操作模式下的分组密码)用于加密正在传输的实际数据,而 MAC 算法用于验证该数据。TLS 是世界上最常见的密码系统,因此也可能是研究最多的。

– Crypto 101

您系统中处理 TLS 协议的部分被称为执行 “TLS 终止”。如果您在网络的第一个入口点终止 TLS,那么您内部网络上的流量可能未加密。 

TLS 的四个主要组成部分是

  1. 握手
  2. 证书验证
  3. 加密(密码)
  4. 数据验证 (MAC)

握手

TLS 握手的目的是通过交换消息在客户端和服务器之间建立安全连接,以

  • 相互验证:服务器验证客户端的证书。客户端验证服务器是否具有与证书关联的私钥。 
  • 协商会话密钥:双方生成会话密钥,用于加密和解密会话期间的所有通信。 
  • 建立加密算法:双方协商他们将使用的加密算法。 
  • 验证服务器:服务器使用公钥向客户端证明其身份。 
  • 签名数据:数据使用消息身份验证码 (MAC) 签名,以确保其完整性

此处使用了几种不同的密码算法。

早期版本的 TLS(SSL 2.0 具有未经身份验证的握手)容易受到握手过程中的 “降级攻击”,攻击者可以在其中强制使用较弱的加密算法。您的 TLS 终止技术(系统中接收 TLS 通信的部分)不得支持过时的 TLS 版本或不安全的密码算法。

证书验证

TLS 证书可用于验证对等方身份,但我们如何验证证书?这通过证书颁发机构的标准 TLS 信任模型来完成。TLS 客户端附带受信任的证书颁发机构列表,通常与您的操作系统或浏览器一起提供。这些机构只有在验证其身份后才会签署证书,并且会收取费用。要伪造证书,必须有一个证书颁发机构被攻破。

当 TLS 客户端连接到服务器时,该服务器会提供证书链。通常,他们自己的证书由中间 CA 证书签名,该证书由另一个证书签名,依此类推,直到由受信任的根证书颁发机构签名。由于客户端已拥有该根证书的副本,因此他们可以验证从根证书开始的签名链。

– Crypto 101

默认情况下,大多数网络库(如 requests 或 httpx)在证书验证失败时不会与服务器通信。这可能表明存在 “中间人” (MITM) 攻击,您可能没有与您认为在交谈的对象交谈,或者您的通信已被拦截并正在被监听。

如果您想签署自己的证书,则必须在您的系统上将自己设置为证书颁发机构。 

对于本地开发,您可能正在使用自签名证书,可以关闭此验证。

import httpx
# Making a get request with certificate verification disabled
r = httpx.get("https://example.org", verify=False)

客户端证书存在于 TLS 中,供通信双方验证其身份,但未广泛使用。mTLS 解决了这个问题。

TLS 证书称为 X.509 证书,证书颁发机构 (CA) 将需要证书签名请求 (CSR) 才能创建证书。密码学库有一个教程,其中包含显示如何执行此操作的代码

本教程还向您展示如何创建带有签名中间体的根 CA。

自签名证书

对于开发环境中的 TLS 通信,您可能会很乐意关闭证书验证并使用自签名证书。这些证书将在浏览器中显示为不安全的警告。

 您可以使用密码学库中的工具生成自签名证书,格式为服务器使用的 .pem 文件。

我们首先创建一个新的私钥

>>> from cryptography import x509
>>> from cryptography.x509.oid import NameOID
>>> from cryptography.hazmat.primitives import hashes
>>>
>>> # Generate our key
>>> key = rsa.generate_private_key(
...     public_exponent=65537,
...     key_size=2048,
... )
>>> # Write our key to disk for safe keeping
>>> with open("path/to/store/key.pem", "wb") as f:
...     f.write(key.private_bytes(
...         encoding=serialization.Encoding.PEM,
...         format=serialization.PrivateFormat.TraditionalOpenSSL,
...         encryption_algorithm=serialization.BestAvailableEncryption(b"passphrase"),
...     ))

然后我们生成证书本身

>>> # Various details about who we are. For a self-signed certificate the
>>> # subject and issuer are always the same.
>>> subject = issuer = x509.Name([
...     x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
...     x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "California"),
...     x509.NameAttribute(NameOID.LOCALITY_NAME, "San Francisco"),
...     x509.NameAttribute(NameOID.ORGANIZATION_NAME, "My Company"),
...     x509.NameAttribute(NameOID.COMMON_NAME, "mysite.com"),
... ])
>>> cert = x509.CertificateBuilder().subject_name(
...     subject
... ).issuer_name(
...     issuer
... ).public_key(
...     key.public_key()
... ).serial_number(
...     x509.random_serial_number()
... ).not_valid_before(
...     datetime.datetime.now(datetime.timezone.utc)
... ).not_valid_after(
...     # Our certificate will be valid for 10 days
...     datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=10)
... ).add_extension(
...     x509.SubjectAlternativeName([x509.DNSName("localhost")]),
...     critical=False,
... # Sign our certificate with our private key
... ).sign(key, hashes.SHA256())
>>> # Write our certificate out to disk.
>>> with open("path/to/certificate.pem", "wb") as f:
...     f.write(cert.public_bytes(serialization.Encoding.PEM))

现在我们有了一个私钥和证书,可以用于本地测试。

使用 certifi 获取证书

系统证书存储可能会过期,导致通信中断,因为无法验证较新的证书。另一种选择是使用 certifi 包,该包捆绑了来自 Mozilla 的最新证书集合。

要引用已安装的证书颁发机构 (CA) 捆绑包,您可以使用提供的 where 函数

>>> import certifi
>>> certifi.where()
'/usr/local/lib/python3.12/site-packages/certifi/cacert.pem'

使用 certify 证书和 OAuth 2 JWT 的示例。

jwks_url = (  "https://login.microsoftonline.com/"  f"{tenant_id}/discovery/v2.0/keys")
ssl_context: ssl.SSLContext = ssl.create_default_context(cafile=certifi.where())
token = jwt.PyJWKClient(jwks_url, ssl_context=ssl_context)

mTLS

相互 TLS (mTLS) 是一种协议,用于在客户端和服务器可以通信之前验证客户端和服务器的身份。它是一种行业标准,使用 X.509 证书和 TLS(传输层安全协议)加密协议来确保流量安全。

mTLS 可以在网络内部以及跨网络使用,从而促进零信任架构,作为深度防御方法的一部分,即使在发生网络 breaches 事件时也能提供网络安全(身份验证和加密)。

网络

当我们讨论安全性时,我们谈论的大部分内容是网络安全以及连接到互联网带来的威胁。如果不了解网络、应用程序服务器的运行方式、它们如何连接到互联网等等,我们就无法理解安全性。

网络层

在 1970 年代,国际标准化组织 (ISO) 开始研究用于联网的开放系统互连 (OSI) 模型。  在此框架下,七个层中的每一层都依赖于其下方的一层,但依赖于下方的一层,而不是直接依赖于整个堆栈。

每一层在其关注点上与更高或更低层中实现的关注点正交。  安全问题可能在所有层都出现,但在大多数情况下,具体威胁和要求可以分析并隔离到当前关注的单层。  许多具有熟悉名称的协议存在于每个 OSI 层。

快速总结一下,我们可以将这些层视为

  1. 物理层:RJ45 插头、100Base-T、蓝牙等。
  2. 数据链路层:以太网、802.11 等。
  3. 网络层:IP、IPSec 等。
  4. 传输层:TCP、UDP、QUIC 等。
  5. 会话层:套接字
  6. 表示层:MIME、TLS/SSL 等。
  7. 应用层,HTTP、HTTPS、FTP、SMTP、DNS、NTP 等。

Web 开发人员主要关注第 7 层的安全性,但了解使应用层成为可能的所有因素是值得的。

应用协议

网络通信使用协议(如 HTTP(超文本传输协议)),通过 TLS 进行保护,跨网络使用 TCP/IP(传输控制协议/互联网协议)、DHCP(动态主机配置协议)和 DNS(域名系统),通过 Kubernetes Pod 中运行的应用程序服务器进程中的套接字,在虚拟机上,在 VLAN(虚拟局域网)中,作为 SDN(软件定义网络)的一部分,在云中运行。这是一个常见的例子。

HTTP 是一种基于文本的协议,允许客户端机器(运行浏览器或可能其他服务)与服务器通信并交换信息。我们可以在 HTTP 之上构建 REST(具象状态传输)API,使用 JSON 或 XML 交换数据。 

这并不构成所有网络通信,例如,还有使用 UDP(用户数据报协议)而不是 TCP 进行通信;但通过 HTTP(通过 TLS 保护)公开 API 的 API 服务器是更常见的情况。 

每个阶段都涉及协议。Spolsky 的泄漏抽象定律告诉我们,为了在任何抽象级别工作,我们需要对下一层有一些了解。因为抽象会泄漏。当出现问题时,了解正在发生的事情的唯一方法是根据其实现来理解当前的抽象层,即它在下一层中正在做什么。

套接字

所有网络 [本质上] 都是通过套接字完成的,Berkeley 套接字起源于 1983 年发布的 BSD 操作系统 4.2。它们用于从客户端到服务器的通信,在服务器内部监听客户端连接,用于同一台机器上的进程间通信 (IPC),或任何进行网络通信的地方。

Python 通过 sockets 模块公开了套接字的低级接口。直接使用它并不常见,但其他库(如 Web 服务器和 Web 客户端(例如标准库中的 urllibhttp.server))都建立在它们之上。但是,套接字通信的基础知识非常简单,并且很容易直接使用套接字构建简单的服务器和客户端。

套接字可以是 TCP 或 UDP。TCP 速度较慢,但更可靠。它验证客户端是否收到了所有数据,这些数据被分成数据包进行通信,并按正确的顺序接收。UDP 不保证这一点,但用于广播或性能很重要的情况,并且沿途丢弃或重新排序几个数据包无关紧要。

数据包包含

  • 标头(包含)
    • 源 IP 地址:客户端的 IP 地址。
    • 目标 IP 地址:服务器的 IP 地址。
    • 源端口:客户端的套接字端口。
    • 目标端口:服务器的套接字端口(例如,HTTP 的端口 80)。
    • 数据包编号:以便可以按正确的顺序重新组装
    • 协议:对于防火墙基于允许的协议来允许/阻止流量很有用
  • 有效负载:正在传输的数据

为了进行通信,我们需要一个寻址系统。为此,我们使用 IPv4 或 IPv6。IPv4 使用 “点分四组” 数字来指定地址;“127.0.0.1” 是环回地址或主地址,也称为 “localhost”。IPv6 使用 128 位地址,使其具有更大的地址空间。在 IPv6 中,“::1” 是环回地址。

IPv6 地址写为八段十六进制数字,用冒号分隔。例如,2001:db8:3333:4444:5555:6666:7777:8888。

可以通过删除前导零或用两个冒号 (::) 替换连续的零段来缩短 IPv6 地址。例如,2001:0db8:0000:0000:0000:7a6e:0680:9668 可以缩短为 2001:db8::7a6e:680:9668。

数据包的路由是使用 RIP(路由信息协议)等协议完成的,这些协议很少与应用程序开发人员直接相关。

如果通信不在同一台机器上,它将通过 “网络接口” 进行。每个网络接口都在一个网络上,任何机器都可能具有多个网络接口。

除了 IP 地址之外,我们还需要端口号。端口允许计算机将单个物理网络连接用于许多传入和传出请求。

端口号的范围为 0 到 65,535。非临时端口是固定的,并且与服务或服务器关联(HTTPS 的端口 443,SSH 的端口 22,SMTP 的端口 25 等),而临时端口(49152 到 65535)是临时的,用于客户端与服务器通信。

这是 Python 中回显套接字服务器的代码。

import socket

import sys

# Create a TCP/IP socket

# AF_INET means use IPv4

# SOCK_STREAM specifies TCP instead of UDP

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Bind the socket to the port

server_address = ('localhost', 10000)

print('starting up on {} port {}'.format(*server_address))

sock.bind(server_address)

# Listen for incoming connections

sock.listen(1)

while True:

    # Wait for a connection

    print('waiting for a connection')

    connection, client_address = sock.accept()

    with connection:

        print('connection from', client_address)

        # Receive the data in small chunks and retransmit it

        while True:

            data = connection.recv(16)

            print('received {!r}'.format(data))

            if data:

                print('sending data back to the client')

                connection.sendall(data)

            else:

                print('no data from', client_address)

                break

此代码创建一个 TCP/IPv4 套接字,将其绑定到一个地址(端口 10000 上的 localhost)并侦听连接。“sock.accept” 的调用会阻塞,直到我们收到连接;当我们获得客户端连接时,我们可以发送和接收数据。

这都是同步、阻塞代码。异步编程使用非阻塞系统调用来访问套接字。这些低级调用作为环路事件对象的方法可用,但您几乎总是使用更高级别的抽象(由您的 Web 应用程序框架或客户端库提供)来访问它们。

这是客户端代码,用于连接到回显服务器

import socket

import sys

# Create a TCP/IP socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Connect the socket to the port where the server is listening

server_address = ('localhost', 10000)

print('connecting to {} port {}'.format(*server_address))

sock.connect(server_address)

with sock:

    # Send data

    message = b'This is the message.  It will be repeated.'

    print('sending {message!}')

    sock.sendall(message)

    # Look for the response

    amount_received = 0

    amount_expected = len(message)

    while amount_received < amount_expected:

        data = sock.recv(16)

        amount_received += len(data)

        print(f'received {data!r}')

客户端使用 sock.connect 而不是我们在服务器代码中使用的 sock.bind。 

更复杂的协议(如 HTTP)是分层在这样的套接字通信之上的。您可以使用简单的套接字连接工具(如 telnet)连接到服务器,并手动为 HTTP 协议提供文本,该协议基于客户端与服务器通信的请求响应周期。

要使用 TLS 进行安全套接字通信,我们可以使用 ssl 模块来 “包装套接字”

import socket, ssl

HOST = "www.agileabstractions.com"

PORT = 443

GET = f"GET / HTTP/1.1\r\nHost: {HOST}\r\n\r\n "

context = ssl.create_default_context()

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

s_sock = context.wrap_socket(s, server_hostname=HOST)

with s_sock:

    s_sock.connect((HOST, 443))

    s_sock.send(GET.encode())

    while True:

        data = s_sock.recv(2048)

        if not data:

            break

        print(data)

一个服务器套接字,使用 TLS 通过 TCP 和 IPv4 进行侦听,它需要访问证书链和私钥

context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)

context.load_cert_chain('/path/to/certchain.pem', '/path/to/private.key')

with socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) as sock:

    sock.bind(('127.0.0.1', 8443))

    sock.listen(5)

    with context.wrap_socket(sock, server_side=True) as ssock:

        conn, addr = ssock.accept()

        ...

DNS

域名系统是将 “www.agileabstractions.com” 等 Web 地址转换为 IP 地址(IPv4 或 IPv6)以启用套接字通信的方式。每个域都注册了指定域地址的 “名称服务器”。

名称服务器可以具有域的几种地址记录, 

DNS 服务器通过查找名称服务器并获取域的地址来将域名地址转换为 IP 地址;因此,要 “解析” 地址,套接字必须查询 DNS 服务器。 

我们可以使用 socket.getaddrinfo 函数从 Python 执行此操作

import socket

# Query for socket info - Criteria is IPv4, TCP

address_info = socket.getaddrinfo(

   host="example.com", port=80,

   family=socket.AF_INET, proto=socket.IPPROTO_TCP

)

# Print socket info

print(address_info)

DNS 依赖于在您的计算机上配置的 DNS 服务器地址(Linux 上的 /etc/resolv.conf)以及为域发布的正确 DNS 记录。

反向查找,从 IP 地址查找主机名,可以使用 socket.gethostbyaddr 完成

>>> import socket
>>> socket.gethostbyaddr("69.59.196.211")
('stackoverflow.com', ['211.196.59.69.in-addr.arpa'], ['69.59.196.211'])

请求-响应周期

REST API 通过 HTTP 公开,这意味着可以使用标准 HTTP 服务器框架来构建 API 服务器,并使用标准 HTTP 客户端库来编写 API 客户端。请求-响应周期是开发人员通常使用的抽象级别。在此之下是 HTTP,在此之下是套接字,在此之下是数据包和传输协议以及网络。

HTTP 是一种基于 TCP/IP 的文本协议,它基于 “请求 -> 响应” 模型。客户端发出请求,指定请求 “方法” 和资源作为路径,以及请求的任何参数 - 服务器以响应回复。 

请求和响应都包含标头(语言、用户代理、cookie、压缩、编码等在标头中指定)。一些请求方法和大多数响应都包含 “正文”,并且响应具有状态代码。

REST 的基础是,几种请求方法大致对应于具有数据库的 CRUD(创建/读取/更新/删除)应用程序的基本操作

  • POST:创建新资源(创建)
  • GET:读取资源的表示形式(读取)
  • PUT:替换数据(通过替换更新)
  • PATCH:修改资源(更新)
  • DELETE:删除资源(删除)

获取网页/资源的正常请求是 GET 请求。表单提交是 POST 请求。

请求作为文本发送,包括

  • 请求方法(和 http 版本)
  • 服务器上的位置(路径)
  • 标头
  • 可选数据(消息正文或参数)

GET 请求示例

GET /hello.html HTTP/1.1

User-Agent: Mozilla/4.0 (compatible; MSIE5.01; Windows NT)

Host: www.agileabstractions.com

Accept-Language: en-us

Accept-Encoding: gzip, deflate

Connection: Keep-Alive

单个服务器可以为多个域提供资源,因此主机域在请求中指定为 Host 标头。服务器配置将告知服务器哪些应用程序应处理特定域的请求。

如果请求未到达服务器,则客户端库可能会引发套接字连接错误。如果请求到达服务器,但存在不同类型的错误,例如 401 表示身份验证,404 表示资源丢失,500 表示服务器错误,501 表示超时等,则客户端将看到 HTTP 错误。(urllib 使用异常响应这些错误,requests 返回一个响应对象,其中包含错误状态代码。) 

如果请求到达服务器,我们将收到响应。响应包括

  • 状态代码 
  • 标头
  • 消息正文(数据)

返回 json 的响应示例

HTTP/1.1 200 OK

Cache-Control: no-cache

Server: libnhttpd

Date: Wed Jul 4 15:32:03 2022

Connection: Keep-Alive:

Content-Type: application/json;charset=UTF-8

Content-Length: 5964

{"rowset": {"osname": "NCOMS", "dbname": alerts", ...}}}

状态代码范围(理论上)从 100-599

        ◦ 100-199 信息性(罕见)

        ◦ 200-299 成功

        ◦ 300-399 重定向

        ◦ 400-499 客户端错误

        ◦ 500-599 服务器错误

常用状态代码

        ◦ 200 – 成功,OK

        ◦ 301 – 临时重定向

        ◦ 302 – 永久重定向

        ◦ 400 – 错误请求

        ◦ 401 – 未经身份验证

        ◦ 403 – 禁止(即使已通过身份验证)

        ◦ 404 – 资源丢失

        ◦ 500 – 内部服务器错误

        ◦ 501 – 超时错误

Cookie 是 http 规范的有用部分,可能包含将请求标识为 “会话” 一部分的数据。这允许与客户端相关的状态存储在服务器上。用于身份验证的 JWT 可以存储在 cookie 中。

Cookie 可以作为请求的一部分发送,如下所示

GET /sample_page.html HTTP/2.0

Host: www.example.org

Cookie: yummy_cookie=chocolate; tasty_cookie=strawberry

Cookie 可以作为响应的一部分返回(以及最大年龄或显式过期日期),如下所示

HTTP/2.0 200 OK

Content-Type: text/html

Set-Cookie: id=a3fWa; Expires=Thu, 31 Oct 2021 07:28:00 GMT;

Set-Cookie: id=a3fWa; Max-Age=2592000

没有 Max-Age 或 Expires 属性的 Cookie 称为 “会话 cookie”,并在当前会话结束时删除,尽管会话由浏览器定义,并且某些浏览器使用 “会话恢复”,允许会话 cookie 无限期地存在。 

在服务器上存储会话状态违反了 REST 的原则之一,即状态应仅由客户端存储,并且每个请求都是单独的事务。

基本身份验证

基本身份验证是最简单和最常见的身份验证方案之一,它内置于 HTTP 中(自 1996 年的 1.0 版本以来)。“Authorization” 标头作为请求的一部分发送,其中用户名和密码使用 base-64 编码(实际上以明文形式发送)。

使用 TLS,标头被加密,因此它可以用于为基本身份验证提供安全性。

身份验证标头字段的格式为 “Authorization: Basic <credentials>”,其中 <credentials> 是 ID 和密码的 Base64 编码,用单个冒号 : 连接。例如。 

Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==

对受身份验证保护的资源的未经身份验证的请求将引发 401 错误和 “WWW-Authenticate” 标头字段。如果用户已通过身份验证但无权访问请求的资源,则将返回 403 错误。

请求身份验证的服务器响应标头字段(带有错误 401)构造如下

WWW-Authenticate: Basic realm="User Visible Realm", charset="UTF-8"

这是一个使用 httpx 客户端库进行基本身份验证的示例

>>> import httpx

>>> auth = httpx.BasicAuth(username="finley", password="secret") 

>>> client = httpx.Client(auth=auth)

>>> response = client.get("https://httpbin.org/basic-auth/finley/secret")

>>> response

<Response [200 OK]>

摘要式身份验证是用于 http 的另一种身份验证方案,但由于它建立在 MD5 之上(使用哈希来避免以明文形式发送用户名/密码),因此由于 MD5 已被破解,因此被认为是过时的。

JSON Web 令牌 (JWT)(在 RFC 7519 中指定)以及 OAuth 2 等协议和身份提供程序,是一种更现代的身份验证方式。  JWT 依赖于其他基于 JSON 的标准:JSON Web 签名JSON Web 加密

使用共享密钥加密和解密 JWT 的示例(公钥和签名算法是另一种方法)

import jwt

>>> encoded_jwt = jwt.encode({"some": "payload"}, "secret", algorithm="HS256")

>>> jwt.decode(encoded_jwt, "secret", algorithms=["HS256"])

{'some': 'payload'}

WebSocket

某些服务可能使用 WebSocket,最初为浏览器实现,它允许服务器通过长时间存在的连接将数据推送到客户端。WebSocket 具有略有不同的 API 来处理它们,但基本原理是相同的。websocket 客户端通常发出一个请求,然后该请求会阻塞,直到服务器提供数据为止。Websocket 也用于流式传输数据。 

与所有套接字通信一样,安全性通常由 TLS 协议处理。

浏览器 (JavaScript) 不会将请求标头公开给 websocket API,这不幸意味着正常的身份验证协议无法与 websocket 一起使用。身份验证通常通过发送令牌作为第一条消息来完成。

关于在 Python Web 应用程序中使用 websocket 的最佳参考是 Armin Ronacher 的这篇文章

websockets 是一个库,构建在 asyncio 之上,但具有同步和异步接口,用于在 Python 中构建 WebSocket 服务器和客户端

网络接口、路由器和防火墙

当我们想到网络接口(NIC – 网络接口控制器)时,我们会想到带有以太网插口的物理卡。虽然大多数计算机都具有物理网络接口(或提供某种物理接口的 WiFi),但它们也可能具有虚拟网络接口,并且如果您的代码在云中的容器或虚拟机中运行,则所有网络接口都将是虚拟的,并且是 SDN(软件定义网络)的一部分。

为了使从一台计算机到另一台计算机(通过套接字)的连接工作,两个端点都必须可寻址。它们必须连接到可以相互访问的网络,并且都具有 IP 地址并知道彼此的端口。具有单个 NIC/IP 地址的计算机可以打开多个套接字进行通信,使用不同的端口。 

设备的 IP 地址属于 NIC,NIC(除非使用静态寻址)通常会从 DHCP(动态主机配置协议)获取其 IP 地址。因此,具有多个 NIC 的机器将为每个 NIC 拥有一个 IP 地址。 

在一个简单的网络中,DHCP 服务器将在路由器上运行。DHCP 服务器使用设备的 MAC(媒体访问控制)地址来分配 IP 地址,因此同一设备每次请求 IP 地址时都会获得相同的 IP 地址。

来自套接字的流量将通过 NIC,然后通过多个路由器和防火墙,直到到达其目的地。每个数据包都包含其源 IP 和目标 IP,路由器使用它们将流量路由到其目的地。仅将网络置于路由器之后就可以提供一定程度的安全性,因为路由器专用网络之外的任何设备都看不到其中的任何设备,路由器本身除外。 

路由器通常包含防火墙,防火墙将阻止除明确允许的流量之外的所有流量。用于应用程序部署的更复杂的网络将具有许多网络交换机和特定的防火墙。

局域网 (LAN) 由网络上的所有计算机组成,这些计算机可能位于路由器后面,路由器只有一个公共 IP 地址。本地网络上的所有计算机都将具有为专用地址保留的 IP 地址范围之一中的地址。

专用 IP 地址保留用于专用网络,并且在互联网上不可公开路由。互联网号码分配机构 (IANA) 保留了以下 IPv4 地址范围供私人使用

  • A 类:10.0.0.0–10.255.255.255
  • B 类:172.16.0.0–172.31.255.255
  • C 类:192.168.0.0–192.168.255.255 

专用 IP 地址不可路由,这意味着它们不会出现在互联网上。专用 IP 地址可以在不同的专用网络中重复使用。 

您的路由器可能具有地址 192.168.0.1(或类似地址),而您的计算机可能具有类似 19.168.0.15 的地址。并非所有专用网络都使用 192.168 专用地址空间。

网络使用网络地址转换 (NAT) 允许多个设备共享一个公共 IP 地址。这意味着专用网络上的设备无法直接接收互联网流量,但它们仍然可以通过网络的公共 IP 地址访问互联网。网络地址转换允许具有专用 IP 地址的计算机在公共互联网上通信。NAT 通常在路由器上完成。

一个复杂的真实世界场景可能具有多个连接的网络,并且如果网络存在于云中,则这些网络将在 SDN(软件定义网络)中使用 VLAN(虚拟 LAN)定义。(在 AWS 中,您的虚拟网络称为 VPC – 虚拟私有云)。

对于物理网络,所有网络流量对于网络上的任何人都是可见的。虚拟 LAN 将流量标记为仅属于单个 VLAN,并且 VLAN 上的计算机将仅 “看到” 标记为该 VLAN 的流量(尽管它仍然存在于物理层)。VLAN 在云中更有效,因为攻击者不太可能访问物理网络。

防火墙用于在受信任的内部网络和不受信任的外部网络(例如互联网)之间创建屏障。它们通过执行阻止未经授权的流量访问安全网络的策略来帮助预防网络攻击。 

防火墙可以根据各种标准配置为允许或拒绝流量,例如:源和目标 IP 地址、端口号和协议类型。

标准的 Linux 防火墙是出了名的难以理解的 iptables。通过 UFW (Uncomplicated Firewall) 界面配置 iptables 规则要容易得多。

一种为了安全而安排网络的方式是将所有暴露于公共互联网的服务器放在单个网络(子网)中,即 DMZ(隔离区)。具有多个 NIC 的机器(防火墙机器或网络交换机)可以充当网关,以便内部网络中的机器可以访问 DMZ,但外部互联网中的任何东西都无法看到私有网络。另一个防火墙将保护 DMZ 免受公共互联网的侵害。

子网是使用“子网掩码”定义的,它使用 CIDR(无类别域间路由)表示网络地址空间的一部分。网络内子网的另一个原因可能是为了某些机器才需要的高速流量骨干网。

虚拟专用网络 (VPN) 是一种网络架构(以及隧道协议等),用于虚拟扩展私有网络。它们通常是安全性的基本组成部分,允许安全地远程连接到网络。VPN 允许网络主机跨另一个[不受信任的]网络交换网络消息以访问私有内容,就好像它们是同一网络的一部分一样。

像 LXC/LXD/docker 这样的容器系统将在您的机器上创建一个虚拟 NIC,以便它们可以为容器进行软件定义的网络。所有容器都将在此网络中拥有一个私有地址。这可以使用“网络桥接”来完成,网络桥接是一种网络连接,允许多个设备相互通信,即使它们连接到不同的网络。常见的是看到私有地址范围 10.* 用于容器网络。

Kubernetes 中部署的基本单元是 Pod。每个 Pod 都有一个唯一的 IP 地址和一个私有网络命名空间,该命名空间由其中的所有容器共享。Pod 中的所有容器都可以在本地网络上相互看到。使用 Kubernetes 进行部署通过限制您可以使用的选项来简化网络设计。

运行和暴露应用服务器

应用程序服务器是一个接受套接字连接的进程。这些很少直接暴露于互联网,而是位于负载均衡器和服务器之后。服务器将接受来自负载均衡器(在多个应用程序服务器之间路由请求)或来自互联网的连接。TLS 终止(流量在 未加密 之后握手)通常由需要访问(通过配置)公钥和私钥的服务器完成。直接在应用程序服务器中进行 TLS 终止不太常见(零信任系统除外)。

然后,服务器会将请求路由到应用服务器,这可能意味着为每个请求启动一个进程,或者维护一个进程池,或者连接到可以处理多个并发请求的单个应用服务器(例如,使用线程、forking 或异步事件循环)。 

应用程序服务器通常作为容器中的进程运行,使用像 gunicorn  (对于 WSGI 应用程序,对于 ASGI 使用 uvicorn) 这样的工具,它使用 Unix forks(预先 fork 模型)来管理应用服务器进程和请求。

应用服务器通常与数据库服务器(连接到快速存储)一起部署(在 K8s 应用程序中的同一 Pod 内),可能在应用服务器前面有一个负载均衡器,以在应用服务器的多个实例之间分配流量。应用服务器的多个实例可用于提供高可用性 (HA)。您的部署基础设施和您的 DevOps 工具,基础设施即代码,都需要被理解和保护。

结论段落

确保安全的 Web 开发始于整体的、纵深防御的方法。这意味着将安全思维融入到开发过程的每个阶段,利用安全的库和工具,并实施分层保护。通过遵守这些基本原则,Python Web 开发人员可以构建能够抵御各种安全威胁的应用程序。

这是 系列 的最后一部分:“每个 Python Web 应用程序开发人员必须了解的关于安全性的绝对最低限度”。

作者

  • Michael Foord

    我是一名 Python 培训师和承包商。我专注于 Python 教学和系统的端到端自动化测试。我的热情在于代码的简洁明了、高效的轻量级流程以及精心设计的系统。作为 Python 核心开发人员,我编写了 unittest 的一部分,并创建了 mock 库,该库后来成为 unittest.mock。

    查看所有帖子

如果你喜欢这篇文章,你可能也喜欢这些