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

Merged
merged 28 commits into from
Sep 22, 2024
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
28 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
49c6a6f
Merge remote-tracking branch 'upstream/master' into feat-gcp-secret
HuanXin-Chen Sep 17, 2024
ad3af95
style(gcp): remove sanity
HuanXin-Chen Sep 18, 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
202 changes: 202 additions & 0 deletions apisix/secret/gcp.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
--
-- 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.utils.google-cloud-oauth")

local str_sub = core.string.sub
local str_find = core.string.find
local decode_base64 = ngx.decode_base64

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

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"
},
scope = {
type = "array",
shreemaan-abhishek marked this conversation as resolved.
Show resolved Hide resolved
items = {
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" }
},
ssl_verify = {
type = "boolean",
default = true
},
auth_file = { type = "string" },
},
oneOf = {
{ required = { "auth_config" } },
{ required = { "auth_file" } },
},
}

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

local config = {
auth_config = {
client_email = config_tab.client_email,
private_key = config_tab.private_key,
project_id = config_tab.project_id
}
}

local ok, err = core.schema.check(schema, config)
if not ok then
return nil, "config parse failure, file: " .. conf.auth_file .. ", err: " .. err
end

return config_tab
end


local function get_secret(oauth, secrets_id)
local httpc = http.new()

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

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

local res, err = httpc: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

return decode_base64(payload.data)
end

membphis marked this conversation as resolved.
Show resolved Hide resolved

local function make_request_to_gcp(conf, secrets_id)
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", google_oauth.new, auth_config, conf.ssl_verify)
shreemaan-abhishek marked this conversation as resolved.
Show resolved Hide resolved
if not oauth then
return nil, "failed to create oauth object, " .. err
end

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

return secret
end


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

local idx = str_find(key, '/')

local main_key = idx and str_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 str_sub(key, idx + 1)

core.log.info("main: ", main_key, sub_key and ", sub: " .. sub_key or "")

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
membphis marked this conversation as resolved.
Show resolved Hide resolved

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, err: " .. err
end

return data[sub_key]
end


membphis marked this conversation as resolved.
Show resolved Hide resolved
return _M
130 changes: 130 additions & 0 deletions apisix/utils/google-cloud-oauth.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
--
-- 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.
--

local core = require("apisix.core")
local type = type
local setmetatable = setmetatable

local ngx_update_time = ngx.update_time
local ngx_time = ngx.time
local ngx_encode_args = ngx.encode_args

local http = require("resty.http")
local jwt = require("resty.jwt")


local function get_timestamp()
ngx_update_time()
return ngx_time()
end


local _M = {}


function _M.generate_access_token(self)
if not self.access_token or get_timestamp() > self.access_token_expire_time - 60 then
self:refresh_access_token()
end
return self.access_token
end


function _M.refresh_access_token(self)
local http_new = http.new()
local res, err = http_new:request_uri(self.token_uri, {
ssl_verify = self.ssl_verify,
method = "POST",
body = ngx_encode_args({
grant_type = "urn:ietf:params:oauth:grant-type:jwt-bearer",
assertion = self:generate_jwt_token()
}),
headers = {
["Content-Type"] = "application/x-www-form-urlencoded",
},
})

if not res then
core.log.error("failed to refresh google oauth access token, ", err)
return
end

if res.status ~= 200 then
core.log.error("failed to refresh google oauth access token: ", res.body)
return
end

res, err = core.json.decode(res.body)
if not res then
core.log.error("failed to parse google oauth response data: ", err)
return
end

self.access_token = res.access_token
self.access_token_type = res.token_type
self.access_token_expire_time = get_timestamp() + res.expires_in
end


function _M.generate_jwt_token(self)
local payload = core.json.encode({
iss = self.client_email,
aud = self.token_uri,
scope = self.scope,
iat = get_timestamp(),
exp = get_timestamp() + (60 * 60)
bzp2010 marked this conversation as resolved.
Show resolved Hide resolved
})

local jwt_token = jwt:sign(self.private_key, {
header = { alg = "RS256", typ = "JWT" },
payload = payload,
})

return jwt_token
end


function _M.new(config, ssl_verify)
local oauth = {
client_email = config.client_email,
private_key = config.private_key,
project_id = config.project_id,
token_uri = config.token_uri or "https://oauth2.googleapis.com/token",
auth_uri = config.auth_uri or "https://accounts.google.com/o/oauth2/auth",
entries_uri = config.entries_uri,
access_token = nil,
access_token_type = nil,
access_token_expire_time = 0,
}

oauth.ssl_verify = ssl_verify

if config.scope then
if type(config.scope) == "string" then
oauth.scope = config.scope
end

if type(config.scope) == "table" then
oauth.scope = core.table.concat(config.scope, " ")
end
end

return setmetatable(oauth, { __index = _M })
end


return _M
54 changes: 54 additions & 0 deletions docs/en/latest/terminology/secret.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ APISIX currently supports storing secrets in the following ways:
- [Environment Variables](#use-environment-variables-to-manage-secrets)
- [HashiCorp Vault](#use-hashicorp-vault-to-manage-secrets)
- [AWS Secrets Manager](#use-aws-secrets-manager-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 @@ -293,3 +294,56 @@ curl -i http://127.0.0.1:9080/your_route -H 'apikey: value'
```

This will verify whether the `key-auth` plugin is correctly using the key from AWS Secrets Manager.

## 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).

### 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
HuanXin-Chen marked this conversation as resolved.
Show resolved Hide resolved

| 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.scope | 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",
"scope": ["https://www.googleapis.com/auth/cloud-platform"]
}
}'

```
Loading
Loading