Skip to content

Commit

Permalink
feat: support gcp secret manager (#11436)
Browse files Browse the repository at this point in the history
  • Loading branch information
HuanXin-Chen authored Sep 22, 2024
1 parent a393320 commit 2fcfbd8
Show file tree
Hide file tree
Showing 8 changed files with 1,383 additions and 1 deletion.
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",
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


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

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


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)
})

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
| 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

0 comments on commit 2fcfbd8

Please sign in to comment.