【微服务架构】使用Spring Cloud Config和Vault进行外部配置
让您的应用程序提取其配置和凭据
你开始一个新项目。一开始,它主要是原型;你尝试了一些想法并且还没有修复,所以在你的应用程序配置方面你非常务实。一些属性文件存储在源代码旁边 - 至少您没有使用硬编码的URL和凭据!首次将应用程序部署到开发或演示环境时,只需复制和修改属性文件即可。原型变成了生产代码,但配置仍然是以临时方式管理的。这对你来说听起来很熟悉吗?无论如何,这是我在一年多前的一个当前项目中发现自己的情况。
在不同的环境中维护配置文件的单独副本可能永远不是最好的方法,即使我们曾经部署到少数长时间运行的雪花服务器上也是如此。鉴于我们越来越多地学习利用云产品,因此经常为测试创建短暂的应用程序环境,甚至将我们的生产系统部署为凤凰服务器,我们需要做得更好。
这篇文章描述了我们如何使用Spring Cloud Config和Spring Cloud Vault在我们的一个基于Spring Boot的项目中解决这个问题,以及我们如何定制这些库以满足我们的需求。特别是,该帖子着眼于外部化配置的动机,并在描述(a)我们为实现Spring Cloud Config而实施的扩展之前,对Spring Cloud Vault,Hashicorp Vault和Spring Cloud Vault进行了(非常)高级概述。客户端从Vault获取必要的HTTP基本身份验证凭据,以及(b)我们如何使应用程序从Vault读取所有TLS(客户端或服务器)密钥材料。您可以分别在公共演示项目demo-authorized-spring-config-server和demo-spring-boot-tls-material-from-vault中的GitHub上找到相关代码。
- 外化配置
- Spring服务
- Spring Cloud Config
- 配置服务器
- 配置客户端
- Spring Cloud Config
- Hashicorp Vault
- Spring Cloud Vault
- Vault Authentication
- Haufe的Spring Cloud Vault扩展
- 授权的配置服务器
- PKI密钥和信任库集成
- 来自Vault的Trustore配置
- 来自Vault的TLS客户端配置
外化配置
当然,配置挑战并不新鲜,因此我们可以看到经过测试的概念。例如,十二因素应用程序的宗旨是应用程序将通过环境独家配置:几乎每个操作系统和部署模型都支持环境变量,无论您使用哪种语言或框架,它们都可以轻松访问,并且您可以定义它们而不对您的部署工件进行任何修改。
虽然我接受了12因素应用程序方法的情绪,并同意声称部署到新环境(可能在不同的平台上)不应该要求对部署工件进行任何修改,但我不相信结论由十二因素应用程序绘制:不应将这些配置组合在一起。
原因很简单:严格遵循十二因素应用指南可能确实可以保证您始终可以更改应用程序的任何配置方面。但它只是将配置管理的负担放在部署者身上,它无助于解决这一挑战。根据我的经验,它还将使开发人员只公开他们希望从一个环境变为另一个环境的少数参数;所有其他配置将最终融入部署工件中。
最后但并非最不重要的是,通过环境传递敏感的秘密并不适合我:如果你可以保证应用程序运行的VM根本不共享可能没问题 - 在这种情况下,攻击者很可能能够无论如何,读取环境将能够从您的应用程序中提取秘密。但是,如果您将不同的应用程序部署到同一个VM(或针对不同租户的多个应用程序实例),那么我不相信容器化解决方案提供的进程隔离,以使一个容器的环境变量与确定的攻击者保持对另一个容器的访问权限。同一台机器。
如果传递一组配置属性的名称(或者,更具技术性地说,标识符),则可以避免上述问题。唯一的前提条件是您必须随时可以自由地重新组织这些属性集。
确实,这引入了对服务的依赖,以某种方式可以解析这样的配置集标识符;但是,与依赖于您的应用程序必须可以访问的数据库服务相比,这并没有太大的不同。只要此配置服务具有定义良好的API,您就可以在必要时轻松进行模拟。
Spring 服务
回到我刚才提到的项目:它在文档数据库前面使用一组Spring Boot服务。这些服务被打包到部署到小型集群中的Docker映像中。 (到目前为止,Docker Swarm模式对我们来说效果很好。但是如果像Azure管理的Kubernetes服务(AKS)这样的产品让我们的生活变得更简单,我将不排除在未来的某个地方切换到Kubernetes。)
Spring框架引入了带有配置文件和属性源的环境抽象概念。 Spring Boot构建于此环境抽象之上,开箱即用,提供了许多选项,可以将配置属性传递给应用程序:“传统”属性文件,YAML配置文件,JVM系统属性,命令行参数,OS环境变量和等等。最后,所有适用的属性源(根据活动配置文件)都组织在由环境抽象管理的列表中。当应用程序需要特定属性的值时,Spring会遍历此列表并使用定义相关属性的第一个源中的值。由于列表中属性源的顺序,更具体的源可以覆盖“默认”配置。如果属性值引用了某个其他属性,则再次从列表的头部开始查找。当然,可以向环境添加自定义属性源。
Spring Boot配置概念对我们来说效果很好 - 我们可以定义合理的默认配置,配置属性(例如,如果我们激活安全连接,所有TLS设置)可以轻松地分组到特定于配置文件的配置中,我们可以轻松为新的部署目标定义其他配置文件。我们只错过了两个关键功能:配置文件不应嵌入到docker镜像中,并且不得在配置文件中保留数据库用户凭据或私钥等秘密。
Spring Cloud Config
在Dave Syer和Josh Long的SpringDeveloper演示文稿的录制中,我回忆起Spring Cloud Config至少解决了前者的问题。
配置服务器
Spring Cloud Config的服务器部分是一个“普通”Spring Boot Web应用程序,它以简单的JSON结构提供属性源列表。 最好以示例的方式显示:
以下JSON对象是本地配置服务器对URL http:// localhost:9400 / demo / plain-actuator-access,integration-db的GET请求的响应。 第一个URL路径段指定应用程序名称 - 配置服务器实例可以为多个应用程序提供配置。 第二个路径段指定以逗号分隔的应用程序配置文件列表。 Config Server还支持第三个路径段(此处未使用),其标签可用于例如请求特定版本的配置。
{ "name": "demo", "profiles": [ "plain-actuator-access,integration-db" ], "label": null, "version": null, "state": null, "propertySources": [ { "name": "file:///Users/ludwigc/Java/JUG/authenticated-config-server/vault-config-client-demo/configurations/demo-integration-db.yml", "source": { "demo.db.host": "integrationtest.demo.contenthub.haufe.io" } }, { "name": "file:///Users/ludwigc/Java/JUG/authenticated-config-server/vault-config-client-demo/configurations/demo-plain-actuator-access.yml", "source": { "management.security.enabled": false } }, { "name": "file:///Users/ludwigc/Java/JUG/authenticated-config-server/vault-config-client-demo/configurations/demo.yml", "source": { "demo.db.host": "localhost", "demo.db.database": "demo", "demo.db.url": "jdbc:postgresql://${demo.db.host}/${demo.db.database}", "demo.db.user": "${vault.demo.db.user}", "demo.db.password": "${vault.demo.db.passord}", "management.security.enabled": true } }, { "name": "file:///Users/ludwigc/Java/JUG/authenticated-config-server/vault-config-client-demo/configurations/application.yml", "source": { "endpoints.shutdown.enabled": true } } ] }
响应的重要部分是数组propertySources:如果演示应用程序是使用指定的配置文件和类路径中的相应配置文件启动的,则此数组中的每个对象都表示将按此顺序加载到Spring环境中的属性源。
请注意,可以在多个源中定义相同的属性,其中属性源的顺序决定“赢家”。此外,Spring递归地解析占位符(使用$ {...}语法)。仅给出此属性源列表,属性demo.db.url因此解析为jdbc:postgresql://integrationtest.demo.contenthub.haufe.io/demo。
由于这个特定的配置服务实例是从我的IDE中运行的,并且我希望它提供反映我的本地工作空间内容的配置,因此我使用本机配置文件启动了配置服务器 - 使用此配置文件,服务器将配置的文件系统文件夹视为只读配置源。
除非服务器用于本地开发,否则更常见的是让服务器从Git存储库中提取配置。更准确地说,配置服务器指向本地Git存储库或配置为在启动时克隆远程存储库。当客户端请求配置时,服务器从远程存储库中提取所有相关更新,并检出客户端请求中指定为标签的分支或版本。 (如果URL中省略了label,则配置服务器默认为主分支的HEAD。)然后,配置服务器才会读取配置文件。
旁注:Config Server依赖Eclipse的JGit库来实现所有Git功能。不幸的是,JGit仅支持ssh-rsa密钥,并且不了解known_hosts文件中的散列条目。如果已经存在远程Git主机的哈希条目,则很容易导致错误。由于JGit抛出的异常中的错误消息根本不清楚,因此很难诊断出这个问题 - 当我第一次尝试配置服务器时,它花了我几个小时。
如果有疑问,您可以使用ssh-keygen -F bitbucket.org查看您的known_hosts文件是否包含bitbucket.org的密钥。如果对于任何返回的键,第一个字段不以纯文本显示相关的主机名,则需要从known_hosts文件中删除键并添加非散列的ssh-rsa键:
$ ssh-keygen -R bitbucket.org
$ ssh-keyscan -t rsa bitbucket.org >>〜/ .ssh / known_hosts
配置客户端
Spring Cloud Config客户端库从配置服务器获取相关配置属性,并将它们作为属性源插入到您的应用程序环境中。仍然支持所有其他配置选项,环境将只有其他来源。对于您的应用程序代码,属性来自哪里没有任何区别。
当然,配置客户端也需要配置 - 至少,它需要配置服务器的地址。 Spring Boot的引导阶段进入图片:在Spring Boot应用程序开始构建其应用程序上下文(应用程序的Spring bean的所有注入和自动装配)之前,它会创建一个引导上下文,其中包含以后用于的所有组件构建应用程序上下文。其中,引导上下文确定如何设置应用程序上下文的环境。
引导上下文的环境使用每个Spring引导应用程序可用的相同源;唯一的区别是配置文件名为bootstrap.yml和bootstrap-profilename.yml(如果您更喜欢使用传统属性文件,则为bootstrap.properties和bootstrap-profilename.properties)。
如上所述,配置服务器可以在底层存储库中的分支之间切换。属性spring.cloud.config.label控制客户端应用程序请求的版本或分支。我们将gradle-git-properties插件添加到我们项目的Gradle构建中,从而构建应用程序的分支在运行时被称为属性git.branch。通过在bootstrap.yml中设置spring.cloud.config.label = $ {git.branch},我们使应用程序从与源分支匹配的分支中获取配置。如果要测试添加或更改配置属性的功能分支,这非常方便。
回顾十二因素应用程序的配置方法,我们可以在应用程序的bootstrap.yml中使用一些默认的配置客户端属性(适用于本地开发),并在部署时使用环境覆盖,例如,配置服务器端点变量。配置服务器地址不敏感,只有很少的属性在部署之间变化,因为环境变量方法可以很好地管理。
作为应用程序运行状况检查的一部分,配置客户端还会定期从服务器重新加载配置。大多数情况下,环境只在应用程序上下文初始化期间读取,因此重新加载不会产生太大影响 - 除非您使用@RefreshScope注释bean。这似乎是即时更改的一个很好的功能,但我还没有第一手的经验。
Hashicorp Vault
使用Spring Cloud Config,我们提供存储在Git存储库中的配置 - 在我们的示例中,在实际的代码存储库中。到目前为止,您应该知道不应该在Git存储库中存储明确的秘密 - 至少,必须使用与存储库分开存储的密钥正确加密机密。 (几年前,一些开发人员在亚马逊收取未经授权的用户可能使用的资源时收取的费用很难,因为在公共存储库中找到了AWS凭据。)
很少有开发人员是密码学专家,因此加密正确很难。并且您不希望保留大量其他代码来拦截您的配置属性,并在应用程序使用它们之前对其进行解密。最后,您最好使用像Hashicorp Vault这样的专用秘密商店解决方案。
Hashicorp Vault是一种安全访问秘密的工具,可以在休息时处理秘密的加密,可以在审计日志中记录任何秘密访问,并附带精心设计的访问控制概念。它还支持动态机密,例如即时创建的数据库密码,一旦相应的Vault令牌过期,它将自动被删除。与Vault实例的所有交互都通过其(通常是TLS安全的)REST接口进行。
我们在通用秘密商店之上使用的功能之一是Vault的PKI后端,可以颁发X.509证书;它本质上提供私人CA. (CA的注册机构事实上是由Vault的角色和访问控制实现实现的。)对于客户和合作伙伴(包括Haufe内的其他项目)访问的端点,我们当然配置由“众所周知的”CA之一颁发的TLS证书;感谢Let's Encrypt,现在获取您的客户可能信任的证书已经不再麻烦了。但在我们的应用程序中,我们需要通过TLS和客户端身份验证来保护一些内部通信路径。我们的加密不会发出客户端证书,因此通过Vault的REST接口可访问的私有CA使操作更加简单。
Spring Cloud Vault
与Spring Cloud Config类似,Spring Cloud Vault客户端将其他属性源插入Spring Boot应用程序的环境中。但是,这些属性是存储在Vault中的机密。
您甚至可以拥有特定于配置文件的机密:Vault将机密存储为JSON对象;您可以通过类似于文件系统路径的分层路径名来解决这些JSON对象,从相应的秘密插件的安装名称开始。
haufe-lexware-blog-chludwig ludwigc$ vault read -format=json local-secrets/ch-integrationtests/CHinteg { "request_id": "0ca07792-c7da-625b-0dd7-57b794fb9856", "lease_id": "", "lease_duration": 604800, "renewable": false, "data": { "vault.content.apiKey": "*********", "vault.content.clientId": "*********", "vault.content.clientSecret": "*********", "vault.ingest.apiKey": "*********", "vault.ingest.clientId": "*********", "vault.ingest.clientSecret": "*********" }, "warnings": null }
当我在路径local-secrets / ch-integrationtests / CHinteg查询对象时,您会看到我从本地Vault实例获得的响应(我用星号标记的实际机密值)。 实际的秘密存储在数据对象中。 鉴于Spring Boot应用程序ch-integration在配置文件CHinteg中运行,Spring Cloud Vault客户端将从Vault位置获取秘密local-secrets / ch-integrationtests / CHinteg,local-secrets / ch-integrationtests,local-secrets / defaultContext / CHinteg 和local-secrets / defaultContext。 defaultContext是应用程序的bootstrap.yml中指定的名称; 它意味着保存多个应用程序共享的秘密。
如果在应用程序中同时激活Spring Cloud Config和Vault,下图显示了数据流:
Vault 验证
Spring Cloud Vault支持与服务身份验证相关的Vault身份验证方法;其中,支持AppRole和Token身份验证。 Spring Cloud Vault客户端还负责令牌续订。但是,我们如何将必要的凭据传递给我们的应用程序实例?毕竟,Vault凭据至少与Vault中可访问的最敏感秘密一样有价值。
在项目中,我们有两种类型的应用程序:
1.批量作业,例如,每天运行一次,并且分别仅作为单个实例启动。
在这里,Vault与AppRole身份验证一起使用的一次性秘密ID非常匹配:部署管道从Vault首次使用后请求新的secret-id。这个secret-id通过一个环境变量传递给应用程序。应用程序将立即将此秘密ID替换为Vault令牌 - 之后,secret-id无用。因此,潜在的攻击者只有非常小的时间窗口才能窃取和使用secret-id。
即使攻击者成功,也不会被忽视:如果攻击者使用了secret-id,应用程序的登录尝试将失败。必须将其记录为安全事件,以便立即做出响应。
2. 部署到Docker Swarm集群中的长时间运行的服务。
某些服务被复制,因此在部署后多个应用程序实例需要登录到Vault - 一次性secret-id是不够的。 n-time secret-id(其中n是服务实例的数量)不会削减它,因为集群可以随时自由重启服务实例,以便将实例移动到另一个工作节点。因此,我们必须将一个秘密ID传递给不能进入攻击者手中的服务实例。
幸运的是,Docker Swarm Mode可以与服务实例共享秘密。 (Kubernetes具有类似的功能。)首先,部署管道要求Vault提供新的机密ID,并将其作为集群中的密钥存储。 Docker通过相互认证的TLS连接将密钥发送到集群管理器,并在集群的Raft存储中加密静态的秘密:
$ vault write -f –format=json auth/approle/role/myapprole/secret-id |\ jq -r '.secret_id' |\ docker secret create myapprole_secretid -
其次,部署管道请求swarm启动我们的服务,并使上面创建的秘密可用于服务实例。该秘密将映射到服务实例的容器文件系统,但此挂载仅保留在内存中:
$ docker service create --name="myapp" \ --secret="myapprole_secretid" myapp:alpine
请注意,secret-id的值永远不会出现在命令行或环境变量中。
Haufe的Spring Cloud Vault扩展
在我们使用Spring Cloud Config和Vault时,我们遇到了开箱即用的其他要求。幸运的是,它们提供了足够的定制钩子。
授权的配置服务器
我们拆分了配置属性:敏感属性存储在Vault中,其余属性在我们的Git存储库中管理,并通过Spring Config Server提供给应用程序实例。因此配置服务器不会泄露秘密。
然而,配置服务器提供了对应用程序内部结构的更多洞察,而不是我们想要自由地分发给潜在的攻击者。这不是“默默无闻”的尝试,但不必要的信息暴露(CWE-200)仍然可以帮助攻击者进行攻击。如果可以通过Internet访问配置服务器,或者通过Intranet访问公司的大多数公司,那么我们需要一些客户端身份验证。
基于TLS的HTTP基本身份验证应该足以达到此目的(假设密码足够强)。这也很容易实现 - 我们所要做的就是将Spring Security添加到Spring Config Server。配置服务器中的Spring Cloud Vault客户端从Vault获取凭据,因此Spring Security配置可以设置内存中的用户存储 - 这是一般的Spring Boot服务开发。
在客户端,情况更棘手:使用这种安全配置服务器的服务实例需要访问凭据作为其配置客户端引导程序设置的一部分。凭据是秘密,保存在Vault中。但是,Spring Cloud Vault提取的属性仅在“常规”应用程序上下文环境中可见,它们在引导环境中不可用!
我仔细阅读了文档并使用调试器逐步完成了Spring Boot应用程序初始化代码。我曾希望我能告诉Spring Boot(例如,通过添加@Order注释)来首先从Vault加载秘密,然后才开始初始化Spring Cloud Config客户端 - 无济于事。无论我尝试了什么,Config Client都从未看到Vault客户端读取的属性。
当我再次浏览Spring Cloud Config文档时,我终于有了一个想法:Spring Cloud Config客户端支持服务发现。 Spring Cloud附带了例如Eureka或Consul的实现,并且在其典型用例中,客户端将通过发现来学习配置服务器的地址。但与大多数Spring功能一样,很容易提供发现客户端的自定义实现;并且发现客户端界面支持基本身份验证凭据。以下显示了剥离文档,日志记录和大多数错误处理的发现客户端实现的核心:
public class VaultBasedDiscoveryClient implements DiscoveryClient { public static final String CONFIG_SERVICE_ID = "configserver"; public static final String URI_PROPERTY_NAME = "spring.cloud.config.uri"; public static final String USERNAME_PROPERTY_NAME = "spring.cloud.config.username"; public static final String PASSWORD_PROPERTY_NAME = "spring.cloud.config.password"; public static final String CONFIG_PATH_PROPERTY_NAME = "spring.cloud.config.configPath"; private final ConfigClientProperties configClientProperties; private final PropertySourceLocator vaultPropertySourceLocator; private final Environment environment; private final Supplier<List<ServiceInstance>> memoizedConfigServiceListSupplier; public VaultBasedDiscoveryClient(ConfigClientProperties configClientProperties, PropertySourceLocator vaultPropertySourceLocator, Environment environment) { this.configClientProperties = configClientProperties; this.vaultPropertySourceLocator = vaultPropertySourceLocator; this.environment = environment; } @Override public String description() { return "Vault-based Discovery Client"; } @Override @Deprecated public ServiceInstance getLocalServiceInstance() { return null; } @Override public List<ServiceInstance> getInstances(String serviceId) { VaultBasedConfigServiceInstance serviceInstance = null; if (CONFIG_SERVICE_ID.equals(serviceId)) { serviceInstance = createServiceInstance(); } return serviceInstance != null ? Collections.singletonList(serviceInstance) : Collections.emptyList(); } @Override public List<String> getServices() { return Collections.singletonList(CONFIG_SERVICE_ID); } private VaultBasedConfigServiceInstance createServiceInstance() { PropertySource<?> vaultPropertySource = vaultPropertySourceLocator.locate(environment); URI uri = getUri(vaultPropertySource); if (uri == null) { return null; } String userInfo = uri.getUserInfo(); String username = getUsername(vaultPropertySource, userInfo); String password = getPassword(vaultPropertySource, userInfo); String configPath = getVaultProperty(CONFIG_PATH_PROPERTY_NAME, vaultPropertySource, null); return new VaultBasedConfigServiceInstance(CONFIG_SERVICE_ID, uri, username, password, configPath); } private URI getUri(PropertySource<?> vaultPropertySource) { String uriString = getVaultProperty(URI_PROPERTY_NAME, vaultPropertySource, configClientProperties.getUri()); try { return StringUtils.isNotBlank(uriString) ? new URI(uriString) : null; } catch (URISyntaxException e) { return null; } } private String getUsername(PropertySource<?> vaultPropertySource, String userInfo) { String defaultUsername = configClientProperties.getUsername(); if (userInfo != null) { String[] userInfoParts = userInfo.split(":", 2); defaultUsername = userInfoParts[0]; } return getVaultProperty(USERNAME_PROPERTY_NAME, vaultPropertySource, defaultUsername); } private String getPassword(PropertySource<?> vaultPropertySource, String userInfo) { String defaultPassphrase = configClientProperties.getPassword(); if (userInfo != null) { String[] userInfoParts = userInfo.split(":", 2); if (userInfoParts.length > 1) { defaultPassphrase = userInfoParts[1]; } } return getVaultProperty(PASSWORD_PROPERTY_NAME, vaultPropertySource, defaultPassphrase); } private String getVaultProperty(String propertyName, PropertySource<?> vaultPropertySource, String defaultValue) { Object property = vaultPropertySource.getProperty(propertyName); if (property != null) { return property.toString(); } return defaultValue; } }
如果使用服务标识“configserver”调用VaultBasedDiscoveryClient#getInstances(String serviceId),则它会将请求委派给VaultBasedDiscoveryClient#createServiceInstance(),然后从VaultPropertySourceLocator提供的PropertySource读取所需的属性。 后一类是Spring Cloud Vault的一部分,可以访问从Vault检索的属性。
用户名和密码也在ConfigClientProperties的实例中查找,ConfigClientProperties是Spring Cloud Config客户端的类型安全配置类。 这样,如果Vault中未指定属性,我们仍然可以使用配置客户端的“标准”配置作为后备。
您可能已经注意到VaultBasedDiscoveryClient上没有任何Spring注释 - 它不是@Component或类似的。 将SpringBasedDiscoveryClient实例创建为Spring bean则是配置类VaultBasedDiscoveryClientAutoConfiguration的任务:
@Configuration @ConditionalOnExpression("${haufe.cloud.config.vaultDiscovery.enabled:true} and ${spring.cloud.vault.enabled:true}") @ConditionalOnMissingBean(VaultBasedDiscoveryClient.class) @AutoConfigureAfter(RefreshAutoConfiguration.class) @Import(VaultBootstrapConfiguration.class) public class VaultBasedDiscoveryClientAutoConfiguration { @Resource(name = "vaultPropertySourceLocator") private PropertySourceLocator vaultPropertySourceLocator; @Bean DiscoveryClient discoveryClient(@Autowired ConfigClientProperties configClientProperties, @Autowired Environment environment) { return new VaultBasedDiscoveryClient(configClientProperties, vaultPropertySourceLocator, environment); } }
这个类是Spring Boot部分的一个例子我不是那么热衷于:对我来说,至少,对注释的这种蔑视总是让人很难理解它们的净效应。
无论如何,让我们试一试:
- @Configuration将此类声明为应用程序上下文中Spring bean的贡献者,具体取决于以下注释中的条件。
- 注释@ConditionalOnExpression要求两个属性haufe.cloud.config.vaultDiscovery.enabl…,否则将忽略此配置类。
- 由于@ConditionalOnMissingBean批注,仅当没有通过其他方式实例化VaultBasedDiscoveryClient bean时,才会使用此配置类。
- @AutoConfigureAfter(RefreshAutoConfiguration.class)告诉Spring,只有在Spring的日志记录设置等完成后才应用此配置。
- @Import(VaultBootstrapConfiguration.class)从Spring Cloud Vault的自动配置中加载bean定义 - 特别是名为vaultPropertySourceLocator的bean。
通过类路径上的VaultBasedDiscoveryClientAutoConfiguration(并启用),配置客户端将引用VaultBasedDiscoveryClient以获取其自己的配置,并从Vault加载所需的属性(包括配置服务器凭据)。
PKI密钥和信任库集成
TLS密钥材料的管理通常是令人厌烦的任务。像openssl或Java的keytool这样的工具使用起来不是很直观,很少有开发人员熟悉所有选项。不幸的是,您的应用程序仍然按预期工作的事实并不意味着您的安全性足够强大。因此,更重要的是我们自动处理TLS密钥和信任存储,以避免由于手动操作而导致的未检测错误。
在许多情况下,您可以利用像Let's Encrypt这样的服务 - 以完全自动化的方式 - 发布大多数客户信任的证书。但您仍需要以安全的方式为您的应用程序提供密钥材料。如果您的服务不公开,那么您有时最好使用私有证书颁发机构颁发的证书。特别是,如果您依赖相互身份验证的TLS来保护服务之间的连接,则通常会出现这种情况。
传统上,我们曾经将带有必要密钥材料的密钥(和/或信任)存储文件放入文件系统中,应用程序可以从中加载它们。当然可以将这些密钥库作为通用机密存储在库中,并在应用程序启动之前将它们下载 - 例如,作为Docker入口点脚本的一部分 - 下载到Docker tmpfs mount中的文件夹中。这会将密钥和证书分发给应用程序实例,但您仍需要准备密钥库才能将其上载到Vault中。
Mark Paluch在他的示例代码中演示了如何使Spring Boot应用程序的嵌入式Web服务器直接从Vault加载TLS密钥(即,不触及文件系统)。如果您将Vault的PKI后端用作私有CA,则服务甚至可以即时请求新证书。
实际上,示例代码侧重于后一部分。但它会在通用秘密存储中缓存已颁发的证书,并在可能的情况下从那里加载它们。这可以很容易地适应我们将从外部CA获取的密钥和证书上载到Vault的情况。
我们将服务代码扩展为从库中加载信任库。诚然,信托商店不需要保密;但是,您必须保证信任存储的完整性,否则攻击者可能会使您的应用程序接受由攻击者颁发的证书或为攻击者颁发的证书。此外,我们还添加了配置TLS客户端的功能。
来自Vault的Trustore配置
作为Spring Vault的一部分实现的CertificateBundles包括私钥的Base64字符串表示,证书,颁发者证书以及证书的序列号。 Mark Paluch的示例代码包括EmbeddedServletContainerCustomizer,它使嵌入式Web容器使用Vault中的CertificateBundleread内容进行TLS服务器身份验证。不幸的是,它始终注入一个仅包含服务器证书的颁发者证书的信任库。由于CA颁发服务器证书或客户端证书(但不是两者)非常常见,因此此信任库设置不符合我们的要求。
因此,我们决定从Vault加载受信任的证书。我们扩展的TrustedCertificates类非常简单 - 它包含信任库条目的列表,信任库条目又包含证书的Base64字符串表示以及Java用来引用证书的证书别名。我们添加到CertificateUtils的readTrustedCertificates(VaultOperations,String)方法从Vault读取这样的TrustedCertificates对象。由Spring Vault实现的VaultOperations #read(String,Class <T>)处理所有“繁重的工作”。
TrustedCertificates#createTrustStore()从这些条目构建内存中的Java KeyStore。为方便用户,实现剥离PEM证书开始和结束标记,并从证书表示中删除所有空格。这使得可以以PEM或Base64编码的DER格式将证书存储在Vault中。
通过这些扩展,我们可以在VaultPkiConfiguration中修改EmbeddedServletContainerCustomizer的实现,以便它注入从Vault获取的信任库,或者 - 如果没有配置此信任库,则为JVM的默认信任库的副本。
来自Vault的TLS客户端配置
到目前为止,我们讨论了Spring Boot应用程序的嵌入式servlet容器的配置。通常,您的应用程序还充当后端服务的HTTP客户端,并且客户端还需要自定义TLS配置 - 也许该服务使用来自私有CA的证书,JVM默认不信任该私有CA,或者您的客户端必须使用X.509客户端证书。
Spring @Configuration类ServiceClientTLSConfig创建一个TLSClientKeyMaterial Spring bean,它捆绑私钥材料(即密钥库,密钥库密码和密钥密码)以及信任材料(即信任库和信任库密码)。如果spring.cloud.vault.enabled和haufe.client.ssl.vault.enabled都为真且如果Spring上下文中有可用的VaultOperations bean(即Spring Vault可用且已激活),则TLSClientKeyMaterial将初始化为从Vault通用秘密后端读取的值。用于在Vault中存储密钥材料的数据格式与服务器密钥材料的数据格式相同。
在某些情况下(如针对后端服务的集成实例的本地开发),在文件系统上使用密钥和信任存储可能仍然更方便。 ServiceClientTLSConfig适应这种情况;只需将haufe.client.ssl.vault.enabled设置为false,然后将从文件系统初始化TLSClientKeyMaterial。这是由Spring Boot的bean创建方法的@ConditionalOnXYZ注释控制的:ServiceClientTLSConfig #tlsClientKeyMaterialFromVault(ServiceClientTLSProperties,VaultOperations)仅在满足上述所有条件时才会被调用。在所有其他情况下,ServiceClientTLSConfig#tlsClientKeyMaterialFromFilesystem(ServiceClientTLSProperties)从文件系统加载密钥材料。 (乍一看,使用@ConditionalOnXYZ可能看起来有点过分;但是,如果我们想在关闭Vault支持的情况下运行应用程序,它会避免不满意的Spring bean依赖关系,因为那时不会有VaultOperations bean。)
当然,如何使HTTP客户端使用密钥材料取决于您的HTTP客户端实现。演示存储库包含模块demo-service-frontend中的示例:组件ClientHttpRequestFactoryConfigurer使用Spring注入构造函数的TLCLientKeyMaterial bean来设置SSLContext,而SSLContext又用于构造HttpComponentsClientHttpRequestFactory。我希望其他客户端实现的设置看起来类似。
原文:http://work.haufegroup.io/spring-cloud-config-and-vault/
本文:https://pub.intelligentx.net/externalized-configuration-spring-cloud-config-and-vault
讨论:请加入知识星球或者小红圈【首席架构师圈】
- 158 次浏览