Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support gcp secret manager #11436

Open
wants to merge 26 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
0e5538c
feat: support gcp secret manager
HuanXin-Chen Jul 25, 2024
8eabd06
docs(secret): integrating gcp usage introduction
HuanXin-Chen Jul 25, 2024
16a815b
style(gcp): fix some style about gcp secret
HuanXin-Chen Jul 25, 2024
95ee16a
style(gcp): fix the success.json style
HuanXin-Chen Jul 25, 2024
1205deb
style(gcp): fix the secret docs
HuanXin-Chen Jul 26, 2024
11acef1
fix(secret): fix some gcp logic
HuanXin-Chen Jul 27, 2024
a213043
fix(secret): gcp code and test
HuanXin-Chen Jul 29, 2024
4ea2590
feat(secret): support ther gcp string value
HuanXin-Chen Aug 3, 2024
6fd6389
feat(secret): return decode err
HuanXin-Chen Aug 4, 2024
3e780e7
cli(common): add the expact
HuanXin-Chen Aug 8, 2024
113a96c
cli(common): remove the expact
HuanXin-Chen Aug 8, 2024
1d017a7
feat(secret): put the oauth into utils
HuanXin-Chen Aug 9, 2024
eeb5712
merge(): remote-tracking branch 'upstream/master' into feat-gcp-secret
HuanXin-Chen Aug 9, 2024
9464092
fix(secret): fix the test1
HuanXin-Chen Aug 13, 2024
c8ced2f
feat(secret): using serverless to test and fix some style
HuanXin-Chen Aug 25, 2024
7174810
fix(secret): resolved the docs conflicts
HuanXin-Chen Sep 1, 2024
626654f
style(secret): _M.get and test case
HuanXin-Chen Sep 1, 2024
749aa95
Merge branch 'master' into feat-gcp-secret
HuanXin-Chen Sep 1, 2024
873bd12
fix(secret): just code style
HuanXin-Chen Sep 5, 2024
23bb722
fix(secret): scope should not be used in the plural
HuanXin-Chen Sep 6, 2024
02a1910
docs(secret): fix the example
HuanXin-Chen Sep 6, 2024
d7e5676
Merge branch 'feat-gcp-secret' of https://github.com/HuanXin-Chen/api…
HuanXin-Chen Sep 6, 2024
5fdadbd
style(secret): fix the lint problem
HuanXin-Chen Sep 6, 2024
0f53faa
fix(utils): remove the default entries
HuanXin-Chen Sep 6, 2024
cdcd661
style(secret): remove some tips
HuanXin-Chen Sep 11, 2024
a4cc432
style(secret): code style
HuanXin-Chen Sep 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
210 changes: 210 additions & 0 deletions apisix/secret/gcp.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
--
-- Licensed to the Apache Software Foundation (ASF) under one or more
-- contributor license agreements. See the NOTICE file distributed with
-- this work for additional information regarding copyright ownership.
-- The ASF licenses this file to You under the Apache License, Version 2.0
-- (the "License"); you may not use this file except in compliance with
-- the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--

--- GCP Tools.
local core = require("apisix.core")
local http = require("resty.http")
local google_oauth = require("apisix.plugins.google-cloud-logging.oauth")
bzp2010 marked this conversation as resolved.
Show resolved Hide resolved

local sub = core.string.sub
bzp2010 marked this conversation as resolved.
Show resolved Hide resolved
local find = core.string.find
bzp2010 marked this conversation as resolved.
Show resolved Hide resolved
local type = type
local decode_base64 = ngx.decode_base64

local lrucache = core.lrucache.new({ttl = 300, count= 16})

local schema = {
type = "object",
properties = {
auth_config = {
type = "object",
properties = {
client_email = { type = "string" },
private_key = { type = "string" },
project_id = { type = "string" },
token_uri = {
type = "string",
default = "https://oauth2.googleapis.com/token"
},
scopes = {
bzp2010 marked this conversation as resolved.
Show resolved Hide resolved
type = "string",
default = "https://www.googleapis.com/auth/cloud-platform"
},
entries_uri = {
type = "string",
default = "https://secretmanager.googleapis.com/v1/"
},
},
required = { "client_email", "private_key", "project_id"}
bzp2010 marked this conversation as resolved.
Show resolved Hide resolved
},
ssl_verify = {
type = "boolean",
default = true
},
auth_file = { type = "string" },
},
oneOf = {
{ required = { "auth_config" } },
{ required = { "auth_file" } },
},
encrypt_fields = {"auth_config.private_key"},
bzp2010 marked this conversation as resolved.
Show resolved Hide resolved
}

local _M = {
schema = schema
}

local function fetch_oauth_conf(conf)
if conf.auth_config then
return conf.auth_config
end

local file_content, err = core.io.get_file(conf.auth_file)
if not file_content then
return nil, "failed to read configuration, file: " .. conf.auth_file .. ", err: " .. err
end

local config_tab, err = core.json.decode(file_content)
if not config_tab then
return nil, "config parse failure, data: " .. file_content .. ", err: " .. err
end

if not config_tab.client_email or
not config_tab.private_key or
not config_tab.project_id or
not config_tab.token_uri then
return nil, "configuration is undefined, file: " .. conf.auth_file
end

return config_tab
end

local function create_oauth_object(auth_config, ssl_verify)
bzp2010 marked this conversation as resolved.
Show resolved Hide resolved
return google_oauth:new(auth_config, ssl_verify)
end

local function get_secret(oauth, secrets_id)
local http_new = http.new()
shreemaan-abhishek marked this conversation as resolved.
Show resolved Hide resolved

local access_token = oauth:generate_access_token()
if not access_token then
return nil, "failed to get google oauth token"
end

local entries_uri
if oauth.entries_uri == "http://127.0.0.1:1980/google/secret/" then
entries_uri = oauth.entries_uri .. oauth.project_id .. "/" .. secrets_id

else
entries_uri = oauth.entries_uri .. "projects/" .. oauth.project_id
.. "/secrets/" .. secrets_id .. "/versions/latest:access"
end

local res, err = http_new:request_uri(entries_uri, {
ssl_verify = oauth.ssl_verify,
method = "GET",
headers = {
["Content-Type"] = "application/json",
["Authorization"] = (oauth.access_token_type or "Bearer") .. " " .. access_token,
},
})

if not res then
return nil, err
end

if res.status ~= 200 then
return nil, res.body
end

local body, err = core.json.decode(res.body)
if not body then
return nil, "failed to parse response data, " .. err
end

local payload = body.payload
if not payload then
return nil, "invalid payload"
end

local secret_encoded = payload.data
if type(secret_encoded) ~= "string" then
return nil, "invalid secret string"
end

local secret = decode_base64(secret_encoded)
return secret
end

membphis marked this conversation as resolved.
Show resolved Hide resolved
local function make_request_to_gcp(conf, key)
local auth_config, err = fetch_oauth_conf(conf)
if not auth_config then
return nil, err
end

local lru_key = auth_config.client_email .. "#" .. auth_config.project_id

local oauth, err = lrucache(lru_key, "gcp", create_oauth_object, auth_config, conf.ssl_verify)
if not oauth then
return nil, "failed to create oauth object, " .. err
end

local secret, err = get_secret(oauth, key)
if not secret then
return nil, err
end

return secret
end

local function get(conf, key)
core.log.info("fetching data from gcp for key: ", key)

local idx = find(key, '/')

local main_key = idx and sub(key, 1, idx - 1) or key
if main_key == "" then
return nil, "can't find main key, key: " .. key
end

local sub_key = idx and sub(key, idx + 1) or nil
if not sub_key then
core.log.info("main: ", main_key)
else
core.log.info("main: ", main_key, " sub: ", sub_key)
end

local res, err = make_request_to_gcp(conf, main_key)
if not res then
return nil, "failed to retrtive data from gcp secret manager: " .. err
end
Comment on lines +184 to +187
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can remove make_

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is consistent with any of the remaining implementations, both hc vault and aws. aws follows the style of vault, so I think it's fine to keep it the same.

If you think it's critical, you can naturally change all three secret providers at once after that PR merge, but unfortunately I don't see the need to do that necessarily.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, I also think there's no need to modify it.


if not sub_key then
return res
end

local data, err = core.json.decode(res)
if not data then
return nil, "failed to decode result, res: " .. res .. ", err: " .. err
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to remove res, it is dangerous to contain res.body in error message

end

return data[sub_key]
end

_M.get = get

return _M
58 changes: 56 additions & 2 deletions docs/en/latest/terminology/secret.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ Its working principle is shown in the figure:
APISIX currently supports storing secrets in the following ways:

- [Environment Variables](#use-environment-variables-to-manage-secrets)
- [HashiCorp Vault](#use-vault-to-manage-secrets)
- [HashiCorp Vault](#use-hashicorp-vault-to-manage-secrets)
- [GCP Secrets Manager](#use-gcp-secrets-manager-to-manage-secrets)

You can use APISIX Secret functions by specifying format variables in the consumer configuration of the following plugins, such as `key-auth`.

Expand Down Expand Up @@ -133,7 +134,7 @@ Using HashiCorp Vault to manage secrets means that you can store secrets informa
$secret://$manager/$id/$secret_name/$key
```

- manager: secrets management service, could be the HashiCorp Vault, AWS, etc.
- manager: secrets management service, could be the HashiCorp Vault, AWS, GCP etc.
- id: APISIX Secrets resource ID, which needs to be consistent with the one specified when adding the APISIX Secrets resource
- secret_name: the secret name in the secrets management service
- key: the key corresponding to the secret in the secrets management service
Expand Down Expand Up @@ -190,3 +191,56 @@ curl http://127.0.0.1:9180/apisix/admin/consumers \
```

Through the above two steps, when the user request hits the `key-auth` plugin, the real value of the key in the Vault will be obtained through the APISIX Secret component.

## Use GCP Secrets Manager to manage secrets

Using the GCP Secrets Manager to manage secrets means you can store the secret information in the GCP service, and reference it using a specific format of variables when configuring plugins. APISIX currently supports integration with the GCP Secrets Manager, and the supported authentication method is [OAuth 2.0](https://developers.google.com/identity/protocols/oauth2?hl=zh-cn).

### Reference Format

```
$secret://$manager/$id/$secret_name/$key
```

The reference format is the same as before:

- manager: secrets management service, could be the HashiCorp Vault, AWS, GCP etc.
- id: APISIX Secrets resource ID, which needs to be consistent with the one specified when adding the APISIX Secrets resource
- secret_name: the secret name in the secrets management service
- key: get the value of a property when the value of the secret is a JSON string

### Required Parameters
Copy link
Member

@kayx23 kayx23 Sep 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This portion of the doc perhaps should also be added to Admin API doc given the current structure of the apache/apisix doc since that is where the vault parameters are also documented: https://apisix.apache.org/docs/apisix/admin-api/#request-body-parameters-11

cc @pottekkat for inputs as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I can do it in the new PR, because I didn't add the AWS part before.
#11417

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That works as well :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I have completed:#11569


| Name | Required | Default | Description |
|-------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| auth_config | True | | Either `auth_config` or `auth_file` must be provided. |
| auth_config.client_email | True | | Email address of the Google Cloud service account. |
| auth_config.private_key | True | | Private key of the Google Cloud service account. |
| auth_config.project_id | True | | Project ID in the Google Cloud service account. |
| auth_config.token_uri | False | https://oauth2.googleapis.com/token | Token URI of the Google Cloud service account. |
| auth_config.entries_uri | False | https://secretmanager.googleapis.com/v1/ | The API access endpoint for the Google Secrets Manager. |
| auth_config.scopes | False | https://www.googleapis.com/auth/cloud-platform | Access scopes of the Google Cloud service account. See [OAuth 2.0 Scopes for Google APIs](https://developers.google.com/identity/protocols/oauth2/scopes) |
| auth_file | True | | Path to the Google Cloud service account authentication JSON file. Either `auth_config` or `auth_file` must be provided. |
| ssl_verify | False | true | When set to `true`, enables SSL verification as mentioned in [OpenResty docs](https://github.com/openresty/lua-nginx-module#tcpsocksslhandshake). |

You need to configure the corresponding authentication parameters, or specify the authentication file through auth_file, where the content of auth_file is in JSON format.

### Example

Here is a correct configuration example:

```
curl http://127.0.0.1:9180/apisix/admin/secrets/gcp/1 \
-H "X-API-KEY: $admin_key" -X PUT -d '
{
"auth_config" : {
"client_email": "email@apisix.iam.gserviceaccount.com",
"private_key": "private_key",
"project_id": "apisix-project",
"token_uri": "https://oauth2.googleapis.com/token",
"entries_uri": "https://secretmanager.googleapis.com/v1/",
"scopes": "https://www.googleapis.com/auth/cloud-platform"
}
}'

```
56 changes: 55 additions & 1 deletion docs/zh/latest/terminology/secret.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ APISIX 目前支持通过以下方式存储密钥:

- [环境变量](#使用环境变量管理密钥)
- [HashiCorp Vault](#使用-vault-管理密钥)
- [GCP Secrets Manager](#使用-gcp-secrets-manager-管理密钥)

你可以在以下插件的 consumer 配置中通过指定格式的变量来使用 APISIX Secret 功能,比如 `key-auth` 插件。

Expand Down Expand Up @@ -134,7 +135,7 @@ curl http://127.0.0.1:9180/apisix/admin/consumers \
$secret://$manager/$id/$secret_name/$key
```

- manager: 密钥管理服务,可以是 Vault、AWS 等
- manager: 密钥管理服务,可以是 Vault、AWS、GCP
- APISIX Secret 资源 ID,需要与添加 APISIX Secret 资源时指定的 ID 保持一致
- secret_name: 密钥管理服务中的密钥名称
- key:密钥管理服务中密钥对应的 key
Expand Down Expand Up @@ -191,3 +192,56 @@ curl http://127.0.0.1:9180/apisix/admin/consumers \
```

通过上面两步操作,当用户请求命中 `key-auth` 插件时,会通过 APISIX Secret 组件获取到 key 在 Vault 中的真实值。

## 使用 GCP Secrets Manager 管理密钥

使用 GCP Secret Manager 来管理密钥意味着你可以将密钥信息保存在 GCP 服务中,在配置插件时通过特定格式的变量来引用。APISIX 目前支持对接 GCP Secret Manager, 所支持的验证方式是[OAuth 2.0](https://developers.google.com/identity/protocols/oauth2?hl=zh-cn)。

### 引用方式

```
$secret://$manager/$id/$secret_name/$key
```

引用方式和之前保持一致:

- manager: 密钥管理服务,可以是 Vault、AWS\GCP 等
- APISIX Secret 资源 ID,需要与添加 APISIX Secret 资源时指定的 ID 保持一致
- secret_name: 密钥管理服务中的密钥名称
- key:当密钥的值是 JSON 字符串时,获取某个属性的值

### 必要参数

| 名称 | 必选项 | 默认值 | 描述 |
| ----------------------- | -------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------- |
| auth_config | 是 | | `auth_config` 和 `auth_file` 必须配置一个。 |
| auth_config.client_email | 是 | | 谷歌服务帐号的 email 参数。 |
| auth_config.private_key | 是 | | 谷歌服务帐号的私钥参数。 |
| auth_config.project_id | 是 | | 谷歌服务帐号的项目 ID。 |
| auth_config.token_uri | 否 | https://oauth2.googleapis.com/token | 请求谷歌服务帐户的令牌的 URI。 |
| auth_config.entries_uri | 否 | https://secretmanager.googleapis.com/v1/ | 谷歌密钥服务访问端点 API。 |
| auth_config.scopes | 否 | https://www.googleapis.com/auth/cloud-platform | 谷歌服务账号的访问范围,可参考 [OAuth 2.0 Scopes for Google APIs](https://developers.google.com/identity/protocols/oauth2/scopes)|
| auth_file | 是 | | `auth_config` 和 `auth_file` 必须配置一个。 |
| ssl_verify | 否 | true | 当设置为 `true` 时,启用 `SSL` 验证。 |

你需要配置相应的认证参数,或者通过 auth_file 来指定认证文件,其中 auth_file 的内容为认证参数的 json 格式。

### 示例

以下一种正确的配置实例:

```
curl http://127.0.0.1:9180/apisix/admin/secrets/gcp/1 \
-H "X-API-KEY: $admin_key" -X PUT -d '
{
"auth_config" : {
"client_email": "email@apisix.iam.gserviceaccount.com",
"private_key": "private_key",
"project_id": "apisix-project",
"token_uri": "https://oauth2.googleapis.com/token",
"entries_uri": "https://secretmanager.googleapis.com/v1/",
"scopes": "https://www.googleapis.com/auth/cloud-platform"
}
}'

```
Loading
Loading