conjur-api 6.3.1.pre.848 → 6.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -25
- data/Jenkinsfile +6 -123
- data/Rakefile +1 -28
- data/VERSION +1 -1
- data/conjur-api.gemspec +0 -4
- data/docker-compose.yml +0 -15
- data/lib/conjur/api/router.rb +2 -33
- data/lib/conjur/api.rb +0 -3
- data/test.sh +0 -78
- metadata +1 -36
- data/ci/get_gcp_token.sh +0 -43
- data/features/README.md +0 -80
- data/features/authn_azure.feature +0 -15
- data/features/authn_gcp.feature +0 -19
- data/features/authn_iam.feature +0 -18
- data/features/step_definitions/cloud_authn_steps.rb +0 -182
- data/lib/conjur/api/authn_azure.rb +0 -145
- data/lib/conjur/api/authn_gcp.rb +0 -137
- data/lib/conjur/api/authn_iam.rb +0 -140
- data/secrets.yml +0 -13
- data/spec/authn_azure_spec.rb +0 -91
- data/spec/authn_gcp_spec.rb +0 -121
- data/spec/authn_iam_spec.rb +0 -103
data/features/README.md
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
# Cucumber integration features
|
|
2
|
-
|
|
3
|
-
The default suite (run by `rake jenkins_cucumber` via `test.sh`) exercises the
|
|
4
|
-
API against a local Conjur brought up by `docker-compose`.
|
|
5
|
-
|
|
6
|
-
## Cloud authenticator features
|
|
7
|
-
|
|
8
|
-
`authn_iam.feature`, `authn_azure.feature`, and `authn_gcp.feature` are **live
|
|
9
|
-
integration tests** for the AWS IAM / Azure / GCP authenticators. They are
|
|
10
|
-
tagged `@authn_iam` / `@authn_azure` / `@authn_gcp` and are **excluded from the
|
|
11
|
-
default run** (see `Rakefile`), because they only pass on the matching cloud
|
|
12
|
-
provider's infrastructure.
|
|
13
|
-
|
|
14
|
-
### How they run in CI
|
|
15
|
-
|
|
16
|
-
They are gated behind Jenkins boolean params `RUN_AWS_TESTS` / `RUN_AZURE_TESTS`
|
|
17
|
-
/ `RUN_GCP_TESTS` (default `false`; enabled by the daily cron). When a flag is
|
|
18
|
-
set, `test.sh` runs only that authenticator's feature against a real Conjur
|
|
19
|
-
(with `CONJUR_AUTHENTICATORS` including the cloud authenticators) plus the real
|
|
20
|
-
cloud metadata/STS service:
|
|
21
|
-
|
|
22
|
-
- **AWS IAM** — runs on the standard `ExecutorV2` agent, which is itself in AWS
|
|
23
|
-
and carries an instance profile. The stage derives the caller identity with
|
|
24
|
-
`aws sts get-caller-identity` and passes `AWS_ACCOUNT_ID` / `AWS_ROLE_NAME`;
|
|
25
|
-
the Conjur host id is `<account-id>/<role-name>` (authn-iam matches the host
|
|
26
|
-
to the signing role). `new_from_iam` signs a real STS `GetCallerIdentity`
|
|
27
|
-
request.
|
|
28
|
-
- **Azure** — runs on an `AzureExecutorV2` agent. `summon -e azure` injects
|
|
29
|
-
`AZURE_SUBSCRIPTION_ID`, `AZURE_RESOURCE_GROUP`, and
|
|
30
|
-
`USER_ASSIGNED_IDENTITY_CLIENT_ID`. `new_from_azure` fetches a JWT from the
|
|
31
|
-
real Azure IMDS (`169.254.169.254`); the feature derives the `provider-uri`
|
|
32
|
-
from that token's `iss` claim and annotates the host with the
|
|
33
|
-
subscription-id / resource-group restrictions.
|
|
34
|
-
- **GCP** — GCP agents lack a Docker runtime, so the flow is split:
|
|
35
|
-
`ci/get_gcp_token.sh` fetches a real identity token on a `GcpExecutorV2`
|
|
36
|
-
agent, Jenkins stashes it, and the test runs on the AWS agent with the token
|
|
37
|
-
passed via `GCP_ID_TOKEN` (and `GCP_PROJECT_ID` for the host's
|
|
38
|
-
`authn-gcp/project-id` annotation). `new_from_gcp` exchanges that pre-fetched
|
|
39
|
-
token via the serviceless `authn-gcp` endpoint, bypassing the metadata service.
|
|
40
|
-
|
|
41
|
-
`test.sh` forwards the cloud env vars into the tester container and clears
|
|
42
|
-
`HTTP(S)_PROXY` / sets `NO_PROXY` so the link-local metadata endpoints are
|
|
43
|
-
reachable directly.
|
|
44
|
-
|
|
45
|
-
### Running one locally
|
|
46
|
-
|
|
47
|
-
You must be on (or have credentials for) the relevant cloud. For example, on an
|
|
48
|
-
AWS host with an appropriate instance profile / credentials:
|
|
49
|
-
|
|
50
|
-
```sh
|
|
51
|
-
RUN_AWS_TESTS=true ./test.sh
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
For GCP, fetch a token first (on a GCP host), then point the test at it:
|
|
55
|
-
|
|
56
|
-
```sh
|
|
57
|
-
./ci/get_gcp_token.sh data/test/gcp-apps/test-app cucumber gcp-ctx
|
|
58
|
-
GCP_ID_TOKEN="$(cat gcp-ctx/token)" RUN_GCP_TESTS=true ./test.sh
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
### CI infrastructure these depend on
|
|
62
|
-
|
|
63
|
-
The cloud stages authenticate against real provider infrastructure, so they
|
|
64
|
-
rely on the following being in place (all currently provisioned in the
|
|
65
|
-
Conjur-Enterprise CI):
|
|
66
|
-
|
|
67
|
-
1. `AzureExecutorV2` / `GcpExecutorV2` InfraPool agent types, plus the standard
|
|
68
|
-
AWS `ExecutorV2` agent (used for AWS IAM and to run the GCP test).
|
|
69
|
-
2. The `!var` references in `secrets.yml` (`ci/azure/...`) resolvable from the
|
|
70
|
-
CI Conjur vault, so `summon -e azure` can supply the `AZURE_*` /
|
|
71
|
-
`USER_ASSIGNED_IDENTITY_CLIENT_ID` values.
|
|
72
|
-
3. Cloud identities matching how each feature builds its Conjur host:
|
|
73
|
-
- **AWS**: the executor's instance-profile role (host id `<account>/<role>`,
|
|
74
|
-
derived at runtime).
|
|
75
|
-
- **Azure**: the managed identity in the given subscription / resource-group.
|
|
76
|
-
- **GCP**: the `GcpExecutorV2` agent's service account, mapped to
|
|
77
|
-
`data/test/gcp-apps/test-app` via the `authn-gcp/project-id` annotation.
|
|
78
|
-
|
|
79
|
-
If any of these change, a stage may fail with an opaque `401`; `test.sh` dumps
|
|
80
|
-
the Conjur server log on cloud-test failure so the precise reason is visible.
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
@authn_azure
|
|
2
|
-
Feature: Authenticate to Conjur using Azure Managed Identity (authn-azure)
|
|
3
|
-
|
|
4
|
-
This scenario is a live integration test: it exercises new_from_azure against
|
|
5
|
-
the real Azure Instance Metadata Service (IMDS). It is tagged @authn_azure and
|
|
6
|
-
is only run in CI on an Azure executor agent, gated by RUN_AZURE_TESTS, which
|
|
7
|
-
also supplies AZURE_SUBSCRIPTION_ID / AZURE_RESOURCE_GROUP / the user-assigned
|
|
8
|
-
identity client id. See Jenkinsfile and features/README.md.
|
|
9
|
-
|
|
10
|
-
Scenario: Authenticate with a system-assigned identity and retrieve a secret
|
|
11
|
-
Given I enable the authn-azure authenticator "prod" for host "test/azure-apps/test-app" using Azure environment config
|
|
12
|
-
And the secret "test/azure-apps/db-password" has value "s3cr3t"
|
|
13
|
-
When I authenticate via authn-azure as "host/test/azure-apps/test-app" with service id "prod"
|
|
14
|
-
Then I receive a Conjur access token
|
|
15
|
-
And I can retrieve the secret "test/azure-apps/db-password" with value "s3cr3t"
|
data/features/authn_gcp.feature
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
@authn_gcp
|
|
2
|
-
Feature: Authenticate to Conjur using GCP workload identity (authn-gcp)
|
|
3
|
-
|
|
4
|
-
This scenario is a live integration test. GCP executor agents typically lack a
|
|
5
|
-
Docker runtime, so the flow is split: ci/get_gcp_token.sh fetches a real GCP
|
|
6
|
-
identity token on the GCP agent, the pipeline hands it to this test via
|
|
7
|
-
GCP_ID_TOKEN, and new_from_gcp exchanges that pre-fetched token with Conjur
|
|
8
|
-
(bypassing the metadata service). Tagged @authn_gcp, gated by RUN_GCP_TESTS.
|
|
9
|
-
See Jenkinsfile and features/README.md.
|
|
10
|
-
|
|
11
|
-
authn-gcp is a serviceless authenticator: the host is identified by its
|
|
12
|
-
authn-gcp/project-id annotation (set from GCP_PROJECT_ID), not a service id.
|
|
13
|
-
|
|
14
|
-
Scenario: Authenticate with a pre-fetched GCP identity token and retrieve a secret
|
|
15
|
-
Given I enable the authn-gcp authenticator for host "data/test/gcp-apps/test-app"
|
|
16
|
-
And the secret "data/test/gcp-apps/db-password" has value "s3cr3t"
|
|
17
|
-
When I authenticate via authn-gcp as "host/data/test/gcp-apps/test-app" using the pre-fetched token
|
|
18
|
-
Then I receive a Conjur access token
|
|
19
|
-
And I can retrieve the secret "data/test/gcp-apps/db-password" with value "s3cr3t"
|
data/features/authn_iam.feature
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
@authn_iam
|
|
2
|
-
Feature: Authenticate to Conjur using AWS IAM (authn-iam)
|
|
3
|
-
|
|
4
|
-
This scenario is a live integration test: it exercises new_from_iam against
|
|
5
|
-
the real AWS STS service using the credentials of the host it runs on. It is
|
|
6
|
-
tagged @authn_iam and is only run in CI on an AWS executor agent, gated by
|
|
7
|
-
RUN_AWS_TESTS. See Jenkinsfile and features/README.md.
|
|
8
|
-
|
|
9
|
-
authn-iam identifies the Conjur host by the AWS caller identity, so the host
|
|
10
|
-
id is "<AWS_ACCOUNT_ID>/<AWS_ROLE_NAME>" (derived on the agent from
|
|
11
|
-
`aws sts get-caller-identity` and passed in via env).
|
|
12
|
-
|
|
13
|
-
Scenario: Authenticate and retrieve a secret via authn-iam
|
|
14
|
-
Given I enable the authn-iam authenticator "prod" for the AWS caller identity host
|
|
15
|
-
And the secret "test/iam-apps/db-password" has value "s3cr3t"
|
|
16
|
-
When I authenticate via authn-iam as the AWS caller identity host with service id "prod"
|
|
17
|
-
Then I receive a Conjur access token
|
|
18
|
-
And I can retrieve the secret "test/iam-apps/db-password" with value "s3cr3t"
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
require 'base64'
|
|
2
|
-
require 'json'
|
|
3
|
-
|
|
4
|
-
# Step definitions for the cloud authenticator integration features
|
|
5
|
-
# (authn-iam, authn-azure, authn-gcp).
|
|
6
|
-
#
|
|
7
|
-
# These steps drive real Conjur (brought up by docker-compose) against the
|
|
8
|
-
# cloud provider's real metadata/STS services. They are only meaningful when
|
|
9
|
-
# run on the matching cloud agent, so the features carry @authn_iam /
|
|
10
|
-
# @authn_azure / @authn_gcp tags and are gated by RUN_AWS_TESTS /
|
|
11
|
-
# RUN_AZURE_TESTS / RUN_GCP_TESTS in the pipeline.
|
|
12
|
-
|
|
13
|
-
# --- authn-iam -------------------------------------------------------------
|
|
14
|
-
|
|
15
|
-
# authn-iam matches the Conjur host to the AWS caller identity, so the host id
|
|
16
|
-
# must be "<account-id>/<role-name>" (supplied via env by the AWS CI stage).
|
|
17
|
-
def aws_caller_identity_host_id
|
|
18
|
-
account = ENV.fetch('AWS_ACCOUNT_ID')
|
|
19
|
-
role = ENV.fetch('AWS_ROLE_NAME')
|
|
20
|
-
"#{account}/#{role}"
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
Given(/^I enable the authn-iam authenticator "([^"]+)" for the AWS caller identity host$/) do |service_id|
|
|
24
|
-
host_id = aws_caller_identity_host_id
|
|
25
|
-
$conjur.load_policy 'root', <<-POLICY
|
|
26
|
-
- !policy
|
|
27
|
-
id: conjur/authn-iam/#{service_id}
|
|
28
|
-
body:
|
|
29
|
-
- !webservice
|
|
30
|
-
- !group apps
|
|
31
|
-
- !permit
|
|
32
|
-
role: !group apps
|
|
33
|
-
privilege: [ read, authenticate ]
|
|
34
|
-
resource: !webservice
|
|
35
|
-
|
|
36
|
-
- !host
|
|
37
|
-
id: #{host_id}
|
|
38
|
-
annotations:
|
|
39
|
-
authn-iam/#{service_id}: true
|
|
40
|
-
|
|
41
|
-
- !grant
|
|
42
|
-
role: !group conjur/authn-iam/#{service_id}/apps
|
|
43
|
-
member: !host #{host_id}
|
|
44
|
-
POLICY
|
|
45
|
-
|
|
46
|
-
@cloud_host_id = host_id
|
|
47
|
-
$conjur.authenticator_enable('authn-iam', service_id)
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
When(/^I authenticate via authn-iam as the AWS caller identity host with service id "([^"]+)"$/) do |service_id|
|
|
51
|
-
identity = "host/#{aws_caller_identity_host_id}"
|
|
52
|
-
@cloud_api = Conjur::API.new_from_iam(identity, service_id: service_id)
|
|
53
|
-
@result = @cloud_api.token
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
# --- authn-azure -----------------------------------------------------------
|
|
57
|
-
|
|
58
|
-
# The authn-azure provider-uri must equal the token's actual issuer
|
|
59
|
-
# (https://sts.windows.net/<tenant>/). Derive it from a real IMDS token's `iss`
|
|
60
|
-
# claim rather than hardcoding, matching conjur-api-java's resolveProviderUriFromImds.
|
|
61
|
-
def azure_provider_uri
|
|
62
|
-
jwt = Conjur::API.send(:fetch_azure_jwt,
|
|
63
|
-
resource_uri: Conjur::API::AzureAuthenticator::DEFAULT_RESOURCE,
|
|
64
|
-
client_id: (ENV['USER_ASSIGNED_IDENTITY_CLIENT_ID'] unless ENV['USER_ASSIGNED_IDENTITY_CLIENT_ID'].to_s.empty?),
|
|
65
|
-
imds_base_url: Conjur::API::AzureAuthenticator::DEFAULT_IMDS_BASE,
|
|
66
|
-
imds_api_version: Conjur::API::AzureAuthenticator::DEFAULT_IMDS_VER)
|
|
67
|
-
payload = JSON.parse(Base64.decode64(jwt.split('.')[1]))
|
|
68
|
-
issuer = payload.fetch('iss')
|
|
69
|
-
issuer.end_with?('/') ? issuer : "#{issuer}/"
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
Given(/^I enable the authn-azure authenticator "([^"]+)" for host "([^"]+)" using Azure environment config$/) do |service_id, host_id|
|
|
73
|
-
subscription = ENV.fetch('AZURE_SUBSCRIPTION_ID')
|
|
74
|
-
resource_group = ENV.fetch('AZURE_RESOURCE_GROUP')
|
|
75
|
-
provider_uri = azure_provider_uri
|
|
76
|
-
|
|
77
|
-
$conjur.load_policy 'root', <<-POLICY
|
|
78
|
-
- !policy
|
|
79
|
-
id: conjur/authn-azure/#{service_id}
|
|
80
|
-
body:
|
|
81
|
-
- !webservice
|
|
82
|
-
- !variable provider-uri
|
|
83
|
-
- !group apps
|
|
84
|
-
- !permit
|
|
85
|
-
role: !group apps
|
|
86
|
-
privilege: [ read, authenticate ]
|
|
87
|
-
resource: !webservice
|
|
88
|
-
|
|
89
|
-
- !host
|
|
90
|
-
id: #{host_id}
|
|
91
|
-
annotations:
|
|
92
|
-
authn-azure/subscription-id: "#{subscription}"
|
|
93
|
-
authn-azure/resource-group: "#{resource_group}"
|
|
94
|
-
|
|
95
|
-
- !grant
|
|
96
|
-
role: !group conjur/authn-azure/#{service_id}/apps
|
|
97
|
-
member: !host #{host_id}
|
|
98
|
-
POLICY
|
|
99
|
-
|
|
100
|
-
@cloud_host_id = host_id
|
|
101
|
-
$conjur.resource("cucumber:variable:conjur/authn-azure/#{service_id}/provider-uri")
|
|
102
|
-
.add_value(provider_uri)
|
|
103
|
-
$conjur.authenticator_enable('authn-azure', service_id)
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
When(/^I authenticate via authn-azure as "([^"]+)" with service id "([^"]+)"(?: and client id "([^"]*)")?$/) do |identity, service_id, client_id|
|
|
107
|
-
# new_from_azure takes service_id and identity positionally; client_id is a keyword.
|
|
108
|
-
opts = {}
|
|
109
|
-
opts[:client_id] = client_id if client_id && !client_id.empty?
|
|
110
|
-
@cloud_api = Conjur::API.new_from_azure(service_id, identity, **opts)
|
|
111
|
-
@result = @cloud_api.token
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
# --- authn-gcp -------------------------------------------------------------
|
|
115
|
-
|
|
116
|
-
Given(/^I enable the authn-gcp authenticator for host "([^"]+)"$/) do |host_id|
|
|
117
|
-
# authn-gcp is serviceless. The host is matched to the GCP identity token via
|
|
118
|
-
# its authn-gcp/project-id annotation, so the project id must be the one the
|
|
119
|
-
# token was issued for (supplied by ci/get_gcp_token.sh as GCP_PROJECT_ID).
|
|
120
|
-
project_id = ENV.fetch('GCP_PROJECT_ID')
|
|
121
|
-
|
|
122
|
-
$conjur.load_policy 'root', <<-POLICY
|
|
123
|
-
- !policy
|
|
124
|
-
id: conjur/authn-gcp
|
|
125
|
-
body:
|
|
126
|
-
- !webservice
|
|
127
|
-
- !group apps
|
|
128
|
-
- !permit
|
|
129
|
-
role: !group apps
|
|
130
|
-
privilege: [ read, authenticate ]
|
|
131
|
-
resource: !webservice
|
|
132
|
-
|
|
133
|
-
- !host
|
|
134
|
-
id: #{host_id}
|
|
135
|
-
annotations:
|
|
136
|
-
authn-gcp/project-id: "#{project_id}"
|
|
137
|
-
|
|
138
|
-
- !grant
|
|
139
|
-
role: !group conjur/authn-gcp/apps
|
|
140
|
-
member: !host #{host_id}
|
|
141
|
-
POLICY
|
|
142
|
-
|
|
143
|
-
@cloud_host_id = host_id
|
|
144
|
-
$conjur.authenticator_enable('authn-gcp', '')
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
When(/^I authenticate via authn-gcp as "([^"]+)" using the pre-fetched token$/) do |identity|
|
|
148
|
-
jwt = ENV['GCP_ID_TOKEN']
|
|
149
|
-
raise 'GCP_ID_TOKEN is not set; run ci/get_gcp_token.sh on a GCP agent first' if jwt.nil? || jwt.empty?
|
|
150
|
-
|
|
151
|
-
@cloud_api = Conjur::API.new_from_gcp(identity, jwt: jwt)
|
|
152
|
-
@result = @cloud_api.token
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
# --- shared assertions -----------------------------------------------------
|
|
156
|
-
|
|
157
|
-
Then(/^I receive a Conjur access token$/) do
|
|
158
|
-
# Conjur access tokens are returned parsed into a Hash of the JWT parts.
|
|
159
|
-
expect(@result).to be_a(Hash)
|
|
160
|
-
expect(@result).to include('protected', 'payload', 'signature')
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
Given(/^the secret "([^"]+)" has value "([^"]+)"$/) do |var_id, value|
|
|
164
|
-
# Declare the variable and grant the cloud host permission to read it, then
|
|
165
|
-
# set the value. @cloud_host_id is captured by the "enable ... authenticator"
|
|
166
|
-
# step that always runs first.
|
|
167
|
-
raise 'no cloud host configured; run the "enable authenticator" step first' unless @cloud_host_id
|
|
168
|
-
|
|
169
|
-
$conjur.load_policy 'root', <<-POLICY
|
|
170
|
-
- !variable #{var_id}
|
|
171
|
-
- !permit
|
|
172
|
-
role: !host #{@cloud_host_id}
|
|
173
|
-
privilege: [ read, execute ]
|
|
174
|
-
resource: !variable #{var_id}
|
|
175
|
-
POLICY
|
|
176
|
-
|
|
177
|
-
$conjur.resource("cucumber:variable:#{var_id}").add_value(value)
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
Then(/^I can retrieve the secret "([^"]+)" with value "([^"]+)"$/) do |var_id, value|
|
|
181
|
-
expect(@cloud_api.resource("cucumber:variable:#{var_id}").value).to eq(value)
|
|
182
|
-
end
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'net/http'
|
|
4
|
-
require 'json'
|
|
5
|
-
require 'uri'
|
|
6
|
-
require 'cgi'
|
|
7
|
-
|
|
8
|
-
module Conjur
|
|
9
|
-
class API
|
|
10
|
-
# Authenticator that uses an Azure Managed Identity JWT to obtain Conjur access tokens.
|
|
11
|
-
# Fetches a JWT from the Azure Instance Metadata Service (IMDS), then POSTs it
|
|
12
|
-
# to the Conjur authn-azure endpoint.
|
|
13
|
-
class AzureAuthenticator
|
|
14
|
-
include TokenExpiration
|
|
15
|
-
|
|
16
|
-
IMDS_TOKEN_PATH = '/metadata/identity/oauth2/token'.freeze
|
|
17
|
-
DEFAULT_RESOURCE = 'https://management.azure.com/'.freeze
|
|
18
|
-
DEFAULT_IMDS_BASE = 'http://169.254.169.254'.freeze
|
|
19
|
-
DEFAULT_IMDS_VER = '2018-02-01'.freeze
|
|
20
|
-
|
|
21
|
-
attr_reader :account, :service_id, :identity, :resource_uri,
|
|
22
|
-
:client_id, :imds_base_url, :imds_api_version
|
|
23
|
-
|
|
24
|
-
def initialize(account, service_id, identity,
|
|
25
|
-
resource_uri: DEFAULT_RESOURCE,
|
|
26
|
-
client_id: nil,
|
|
27
|
-
imds_base_url: DEFAULT_IMDS_BASE,
|
|
28
|
-
imds_api_version: DEFAULT_IMDS_VER)
|
|
29
|
-
@account = account
|
|
30
|
-
@service_id = service_id
|
|
31
|
-
@identity = identity
|
|
32
|
-
@resource_uri = resource_uri
|
|
33
|
-
@client_id = client_id
|
|
34
|
-
@imds_base_url = imds_base_url
|
|
35
|
-
@imds_api_version = imds_api_version
|
|
36
|
-
update_token_born
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def refresh_token
|
|
40
|
-
Conjur::API.authenticate_azure(service_id, identity,
|
|
41
|
-
account: account,
|
|
42
|
-
resource_uri: resource_uri,
|
|
43
|
-
client_id: client_id,
|
|
44
|
-
imds_base_url: imds_base_url,
|
|
45
|
-
imds_api_version: imds_api_version).tap do
|
|
46
|
-
update_token_born
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
class << self
|
|
52
|
-
# Create a {Conjur::API} instance authenticated via Azure Managed Identity (authn-azure).
|
|
53
|
-
#
|
|
54
|
-
# @param [String] service_id the authn-azure service ID configured in Conjur
|
|
55
|
-
# @param [String] identity Conjur host identity (e.g. "host/my-app/azure-host")
|
|
56
|
-
# @param [String] account the Conjur organization account
|
|
57
|
-
# @param [String] resource_uri Azure resource URI for the IMDS token request
|
|
58
|
-
# @param [String, nil] client_id optional client ID for a user-assigned managed identity
|
|
59
|
-
# @param [String] imds_base_url override for the IMDS base URL (useful in tests)
|
|
60
|
-
# @param [String] imds_api_version override for the IMDS API version
|
|
61
|
-
# @param [String, nil] remote_ip optional IP address recorded in the audit log
|
|
62
|
-
# @return [Conjur::API]
|
|
63
|
-
def new_from_azure(service_id, identity,
|
|
64
|
-
account: Conjur.configuration.account,
|
|
65
|
-
resource_uri: AzureAuthenticator::DEFAULT_RESOURCE,
|
|
66
|
-
client_id: nil,
|
|
67
|
-
imds_base_url: AzureAuthenticator::DEFAULT_IMDS_BASE,
|
|
68
|
-
imds_api_version: AzureAuthenticator::DEFAULT_IMDS_VER,
|
|
69
|
-
remote_ip: nil)
|
|
70
|
-
self.new.init_from_azure(service_id, identity,
|
|
71
|
-
account: account,
|
|
72
|
-
resource_uri: resource_uri,
|
|
73
|
-
client_id: client_id,
|
|
74
|
-
imds_base_url: imds_base_url,
|
|
75
|
-
imds_api_version: imds_api_version,
|
|
76
|
-
remote_ip: remote_ip)
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
# Authenticate via authn-azure and return a parsed Conjur access token.
|
|
80
|
-
#
|
|
81
|
-
# @param [String] service_id the authn-azure service ID
|
|
82
|
-
# @param [String] identity Conjur host identity
|
|
83
|
-
# @param [String] account the Conjur organization account
|
|
84
|
-
# @return [Hash] parsed access token
|
|
85
|
-
def authenticate_azure(service_id, identity,
|
|
86
|
-
account: Conjur.configuration.account,
|
|
87
|
-
resource_uri: AzureAuthenticator::DEFAULT_RESOURCE,
|
|
88
|
-
client_id: nil,
|
|
89
|
-
imds_base_url: AzureAuthenticator::DEFAULT_IMDS_BASE,
|
|
90
|
-
imds_api_version: AzureAuthenticator::DEFAULT_IMDS_VER)
|
|
91
|
-
raise ArgumentError, "service_id is required" if service_id.nil? || service_id.empty?
|
|
92
|
-
raise ArgumentError, "identity is required" if identity.nil? || identity.empty?
|
|
93
|
-
|
|
94
|
-
if Conjur.log
|
|
95
|
-
Conjur.log << "Authenticating #{identity} to account #{account} via authn-azure/#{service_id}\n"
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
jwt = fetch_azure_jwt(resource_uri: resource_uri, client_id: client_id,
|
|
99
|
-
imds_base_url: imds_base_url, imds_api_version: imds_api_version)
|
|
100
|
-
JSON.parse(url_for(:authn_azure_authenticate, account, service_id, identity)
|
|
101
|
-
.post("jwt=#{CGI.escape(jwt)}", content_type: 'application/x-www-form-urlencoded'))
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
private
|
|
105
|
-
|
|
106
|
-
# Fetches an Azure Managed Identity JWT from IMDS.
|
|
107
|
-
# Uses a direct Net::HTTP connection (proxy addr = nil) because the IMDS
|
|
108
|
-
# link-local address 169.254.169.254 must never be routed through a proxy.
|
|
109
|
-
def fetch_azure_jwt(resource_uri:, client_id:, imds_base_url:, imds_api_version:)
|
|
110
|
-
query = "api-version=#{CGI.escape(imds_api_version)}&resource=#{CGI.escape(resource_uri)}"
|
|
111
|
-
query += "&client_id=#{CGI.escape(client_id)}" if client_id
|
|
112
|
-
|
|
113
|
-
uri = URI("#{imds_base_url}#{AzureAuthenticator::IMDS_TOKEN_PATH}?#{query}")
|
|
114
|
-
http = Net::HTTP.new(uri.host, uri.port, nil) # nil = no proxy
|
|
115
|
-
response = http.get(uri.request_uri, 'Metadata' => 'true')
|
|
116
|
-
|
|
117
|
-
unless response.is_a?(Net::HTTPSuccess)
|
|
118
|
-
raise "Azure IMDS token request failed: #{response.code} #{response.message}. Body: #{response.body}"
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
body = JSON.parse(response.body)
|
|
122
|
-
token = body['access_token']
|
|
123
|
-
raise "Azure IMDS returned an empty access token" if token.nil? || token.empty?
|
|
124
|
-
|
|
125
|
-
token
|
|
126
|
-
end
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
def init_from_azure(service_id, identity,
|
|
130
|
-
account: Conjur.configuration.account,
|
|
131
|
-
resource_uri: AzureAuthenticator::DEFAULT_RESOURCE,
|
|
132
|
-
client_id: nil,
|
|
133
|
-
imds_base_url: AzureAuthenticator::DEFAULT_IMDS_BASE,
|
|
134
|
-
imds_api_version: AzureAuthenticator::DEFAULT_IMDS_VER,
|
|
135
|
-
remote_ip: nil)
|
|
136
|
-
@remote_ip = remote_ip
|
|
137
|
-
@authenticator = AzureAuthenticator.new(account, service_id, identity,
|
|
138
|
-
resource_uri: resource_uri,
|
|
139
|
-
client_id: client_id,
|
|
140
|
-
imds_base_url: imds_base_url,
|
|
141
|
-
imds_api_version: imds_api_version)
|
|
142
|
-
self
|
|
143
|
-
end
|
|
144
|
-
end
|
|
145
|
-
end
|
data/lib/conjur/api/authn_gcp.rb
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'net/http'
|
|
4
|
-
require 'uri'
|
|
5
|
-
require 'cgi'
|
|
6
|
-
require 'json'
|
|
7
|
-
|
|
8
|
-
module Conjur
|
|
9
|
-
class API
|
|
10
|
-
# Authenticator that uses a GCP identity token to obtain Conjur access tokens.
|
|
11
|
-
# Fetches an identity JWT from the GCP Instance Metadata Service, then POSTs it
|
|
12
|
-
# to the Conjur authn-gcp endpoint.
|
|
13
|
-
#
|
|
14
|
-
# authn-gcp is a serviceless authenticator: there is no service ID, and Conjur
|
|
15
|
-
# identifies the calling host from the token's claims (matched against the
|
|
16
|
-
# host's authn-gcp/project-id annotation), so the host id is not sent in the
|
|
17
|
-
# authenticate URL.
|
|
18
|
-
#
|
|
19
|
-
# An explicit JWT may be supplied to bypass the metadata service fetch —
|
|
20
|
-
# useful in CI environments where the token is pre-fetched on a GCP agent.
|
|
21
|
-
class GCPAuthenticator
|
|
22
|
-
include TokenExpiration
|
|
23
|
-
|
|
24
|
-
DEFAULT_METADATA_URL = 'http://metadata.google.internal/computeMetadata/v1' \
|
|
25
|
-
'/instance/service-accounts/default/identity'.freeze
|
|
26
|
-
|
|
27
|
-
attr_reader :account, :identity, :jwt, :gcp_identity_url
|
|
28
|
-
|
|
29
|
-
def initialize(account, identity, jwt: nil,
|
|
30
|
-
gcp_identity_url: DEFAULT_METADATA_URL)
|
|
31
|
-
@account = account
|
|
32
|
-
@identity = identity
|
|
33
|
-
@jwt = jwt
|
|
34
|
-
@gcp_identity_url = gcp_identity_url
|
|
35
|
-
update_token_born
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def refresh_token
|
|
39
|
-
Conjur::API.authenticate_gcp(identity, account: account,
|
|
40
|
-
jwt: jwt,
|
|
41
|
-
gcp_identity_url: gcp_identity_url).tap do
|
|
42
|
-
update_token_born
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
class << self
|
|
48
|
-
# Create a {Conjur::API} instance authenticated via GCP workload identity (authn-gcp).
|
|
49
|
-
#
|
|
50
|
-
# @param [String] identity Conjur host identity (e.g. "host/data/test/gcp-apps/test-app").
|
|
51
|
-
# Used to build the metadata-service audience; Conjur itself derives the
|
|
52
|
-
# host from the token, so it is not sent in the authenticate request.
|
|
53
|
-
# @param [String] account the Conjur organization account
|
|
54
|
-
# @param [String, nil] jwt pre-obtained GCP identity token; when supplied the metadata
|
|
55
|
-
# service is not called. Useful in tests and CI where the token is pre-fetched.
|
|
56
|
-
# @param [String] gcp_identity_url override for the GCP metadata identity URL (for tests)
|
|
57
|
-
# @param [String, nil] remote_ip optional IP address recorded in the audit log
|
|
58
|
-
# @return [Conjur::API]
|
|
59
|
-
def new_from_gcp(identity,
|
|
60
|
-
account: Conjur.configuration.account,
|
|
61
|
-
jwt: nil,
|
|
62
|
-
gcp_identity_url: GCPAuthenticator::DEFAULT_METADATA_URL,
|
|
63
|
-
remote_ip: nil)
|
|
64
|
-
self.new.init_from_gcp(identity, account: account,
|
|
65
|
-
jwt: jwt, gcp_identity_url: gcp_identity_url,
|
|
66
|
-
remote_ip: remote_ip)
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
# Authenticate via authn-gcp and return a parsed Conjur access token.
|
|
70
|
-
#
|
|
71
|
-
# @param [String] identity Conjur host identity (used to build the audience)
|
|
72
|
-
# @param [String] account the Conjur organization account
|
|
73
|
-
# @param [String, nil] jwt pre-obtained GCP identity token
|
|
74
|
-
# @param [String] gcp_identity_url override for the GCP metadata identity URL
|
|
75
|
-
# @return [Hash] parsed access token
|
|
76
|
-
def authenticate_gcp(identity,
|
|
77
|
-
account: Conjur.configuration.account,
|
|
78
|
-
jwt: nil,
|
|
79
|
-
gcp_identity_url: GCPAuthenticator::DEFAULT_METADATA_URL)
|
|
80
|
-
raise ArgumentError, "identity is required" if identity.nil? || identity.empty?
|
|
81
|
-
|
|
82
|
-
if Conjur.log
|
|
83
|
-
Conjur.log << "Authenticating #{identity} to account #{account} via authn-gcp\n"
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
token = jwt || fetch_gcp_jwt(account, identity, gcp_identity_url: gcp_identity_url)
|
|
87
|
-
JSON.parse(url_for(:authn_gcp_authenticate, account)
|
|
88
|
-
.post("jwt=#{CGI.escape(token)}", content_type: 'application/x-www-form-urlencoded'))
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
private
|
|
92
|
-
|
|
93
|
-
# Fetches a GCP identity token from the instance metadata service.
|
|
94
|
-
# Uses a direct Net::HTTP connection (proxy addr = nil) because the metadata
|
|
95
|
-
# link-local address must never be routed through a proxy.
|
|
96
|
-
#
|
|
97
|
-
# The requested audience is "conjur/{account}/host/{identity}", matching what
|
|
98
|
-
# the authn-gcp authenticator expects.
|
|
99
|
-
def fetch_gcp_jwt(account, identity, gcp_identity_url:)
|
|
100
|
-
aud = gcp_audience(account, identity)
|
|
101
|
-
url = "#{gcp_identity_url}?#{URI.encode_www_form(audience: aud, format: 'full')}"
|
|
102
|
-
|
|
103
|
-
uri = URI(url)
|
|
104
|
-
http = Net::HTTP.new(uri.host, uri.port, nil) # nil = no proxy
|
|
105
|
-
response = http.get(uri.request_uri, 'Metadata-Flavor' => 'Google')
|
|
106
|
-
|
|
107
|
-
unless response.is_a?(Net::HTTPSuccess)
|
|
108
|
-
raise "GCP metadata token request failed: #{response.code} #{response.message}. Body: #{response.body}"
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
token = response.body&.strip
|
|
112
|
-
raise "GCP metadata service returned an empty token" if token.nil? || token.empty?
|
|
113
|
-
|
|
114
|
-
token
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
# Builds the authn-gcp audience for a Conjur host identity, e.g.
|
|
118
|
-
# "conjur/myorg/host/data/app". The "host/" prefix is optional in the
|
|
119
|
-
# supplied identity and is normalized away before being re-applied.
|
|
120
|
-
def gcp_audience(account, identity)
|
|
121
|
-
normalized = identity.start_with?('host/') ? identity['host/'.length..] : identity
|
|
122
|
-
"conjur/#{account}/host/#{normalized}"
|
|
123
|
-
end
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
def init_from_gcp(identity,
|
|
127
|
-
account: Conjur.configuration.account,
|
|
128
|
-
jwt: nil,
|
|
129
|
-
gcp_identity_url: GCPAuthenticator::DEFAULT_METADATA_URL,
|
|
130
|
-
remote_ip: nil)
|
|
131
|
-
@remote_ip = remote_ip
|
|
132
|
-
@authenticator = GCPAuthenticator.new(account, identity,
|
|
133
|
-
jwt: jwt, gcp_identity_url: gcp_identity_url)
|
|
134
|
-
self
|
|
135
|
-
end
|
|
136
|
-
end
|
|
137
|
-
end
|