conjur-api 6.3.1 → 6.4.0.pre.849
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 +27 -9
- data/Jenkinsfile +123 -6
- data/Rakefile +28 -1
- data/VERSION +1 -1
- data/ci/get_gcp_token.sh +43 -0
- data/conjur-api.gemspec +4 -0
- data/docker-compose.yml +15 -0
- data/features/README.md +80 -0
- data/features/authn_azure.feature +15 -0
- data/features/authn_gcp.feature +19 -0
- data/features/authn_iam.feature +18 -0
- data/features/step_definitions/cloud_authn_steps.rb +182 -0
- data/lib/conjur/api/authn_azure.rb +145 -0
- data/lib/conjur/api/authn_gcp.rb +137 -0
- data/lib/conjur/api/authn_iam.rb +140 -0
- data/lib/conjur/api/router.rb +33 -2
- data/lib/conjur/api.rb +3 -0
- data/secrets.yml +13 -0
- data/spec/authn_azure_spec.rb +91 -0
- data/spec/authn_gcp_spec.rb +121 -0
- data/spec/authn_iam_spec.rb +103 -0
- data/test.sh +78 -0
- metadata +36 -1
|
@@ -0,0 +1,145 @@
|
|
|
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
|
|
@@ -0,0 +1,137 @@
|
|
|
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
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'uri'
|
|
5
|
+
|
|
6
|
+
module Conjur
|
|
7
|
+
class API
|
|
8
|
+
# Authenticator that uses AWS IAM credentials to obtain Conjur access tokens.
|
|
9
|
+
# Signs an AWS STS GetCallerIdentity request with SigV4, then POSTs the
|
|
10
|
+
# resulting headers as JSON to the Conjur authn-iam endpoint.
|
|
11
|
+
#
|
|
12
|
+
# Requires the +aws-sdk-core+ gem (not bundled with conjur-api).
|
|
13
|
+
# Add `gem 'aws-sdk-core'` to your Gemfile to use this authenticator.
|
|
14
|
+
class IAMAuthenticator
|
|
15
|
+
include TokenExpiration
|
|
16
|
+
|
|
17
|
+
DEFAULT_SERVICE_ID = 'prod'.freeze
|
|
18
|
+
DEFAULT_REGION = 'us-east-1'.freeze
|
|
19
|
+
|
|
20
|
+
STS_GLOBAL_URL = 'https://sts.amazonaws.com/?Action=GetCallerIdentity&Version=2011-06-15'.freeze
|
|
21
|
+
STS_REGIONAL_URL = 'https://sts.%<region>s.amazonaws.com/?Action=GetCallerIdentity&Version=2011-06-15'.freeze
|
|
22
|
+
STS_GLOBAL_SIGNING_REGION = 'us-east-1'.freeze
|
|
23
|
+
|
|
24
|
+
attr_reader :account, :service_id, :identity, :aws_region
|
|
25
|
+
|
|
26
|
+
def initialize(account, service_id, identity, aws_region = DEFAULT_REGION)
|
|
27
|
+
@account = account
|
|
28
|
+
@service_id = service_id
|
|
29
|
+
@identity = identity
|
|
30
|
+
@aws_region = aws_region
|
|
31
|
+
update_token_born
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def refresh_token
|
|
35
|
+
Conjur::API.authenticate_iam(identity, account: account,
|
|
36
|
+
service_id: service_id,
|
|
37
|
+
aws_region: aws_region).tap do
|
|
38
|
+
update_token_born
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class << self
|
|
44
|
+
# Create a {Conjur::API} instance authenticated via AWS IAM (authn-iam).
|
|
45
|
+
#
|
|
46
|
+
# AWS credentials are resolved from the default chain: environment variables
|
|
47
|
+
# (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN), then
|
|
48
|
+
# ~/.aws/credentials, then the EC2 instance profile.
|
|
49
|
+
#
|
|
50
|
+
# Requires the +aws-sdk-core+ gem.
|
|
51
|
+
#
|
|
52
|
+
# @param [String] identity Conjur host identity (e.g. "host/my-app/my-role")
|
|
53
|
+
# @param [String] service_id the authn-iam service ID (default: "prod")
|
|
54
|
+
# @param [String] account the Conjur organization account
|
|
55
|
+
# @param [String] aws_region AWS region for STS signing. Use "global" for the
|
|
56
|
+
# global STS endpoint (signed as us-east-1).
|
|
57
|
+
# @param [String, nil] remote_ip optional IP address recorded in the audit log
|
|
58
|
+
# @return [Conjur::API]
|
|
59
|
+
def new_from_iam(identity,
|
|
60
|
+
account: Conjur.configuration.account,
|
|
61
|
+
service_id: IAMAuthenticator::DEFAULT_SERVICE_ID,
|
|
62
|
+
aws_region: IAMAuthenticator::DEFAULT_REGION,
|
|
63
|
+
remote_ip: nil)
|
|
64
|
+
self.new.init_from_iam(identity, account: account, service_id: service_id,
|
|
65
|
+
aws_region: aws_region, remote_ip: remote_ip)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Authenticate via authn-iam and return a parsed Conjur access token.
|
|
69
|
+
#
|
|
70
|
+
# @param [String] identity Conjur host identity
|
|
71
|
+
# @param [String] service_id the authn-iam service ID
|
|
72
|
+
# @param [String] account the Conjur organization account
|
|
73
|
+
# @param [String] aws_region AWS region for STS signing
|
|
74
|
+
# @return [Hash] parsed access token
|
|
75
|
+
def authenticate_iam(identity,
|
|
76
|
+
account: Conjur.configuration.account,
|
|
77
|
+
service_id: IAMAuthenticator::DEFAULT_SERVICE_ID,
|
|
78
|
+
aws_region: IAMAuthenticator::DEFAULT_REGION)
|
|
79
|
+
raise ArgumentError, "identity is required" if identity.nil? || identity.empty?
|
|
80
|
+
raise ArgumentError, "service_id is required" if service_id.nil? || service_id.empty?
|
|
81
|
+
|
|
82
|
+
begin
|
|
83
|
+
require 'aws-sdk-core'
|
|
84
|
+
rescue LoadError
|
|
85
|
+
raise LoadError,
|
|
86
|
+
"aws-sdk-core is required for IAM authentication. " \
|
|
87
|
+
"Add `gem 'aws-sdk-core'` to your Gemfile."
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
if Conjur.log
|
|
91
|
+
Conjur.log << "Authenticating #{identity} to account #{account} via authn-iam/#{service_id}\n"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
signed_headers = build_iam_signed_headers(aws_region)
|
|
95
|
+
JSON.parse(url_for(:authn_iam_authenticate, account, service_id, identity)
|
|
96
|
+
.post(signed_headers.to_json, content_type: 'application/json'))
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def build_iam_signed_headers(aws_region)
|
|
102
|
+
signing_region = aws_region == 'global' ? IAMAuthenticator::STS_GLOBAL_SIGNING_REGION : aws_region
|
|
103
|
+
sts_url = aws_region == 'global' || aws_region == IAMAuthenticator::STS_GLOBAL_SIGNING_REGION \
|
|
104
|
+
? IAMAuthenticator::STS_GLOBAL_URL
|
|
105
|
+
: format(IAMAuthenticator::STS_REGIONAL_URL, region: aws_region)
|
|
106
|
+
|
|
107
|
+
provider = Aws::CredentialProviderChain.new.resolve
|
|
108
|
+
raise ArgumentError,
|
|
109
|
+
"No AWS credentials found. Configure an instance profile, " \
|
|
110
|
+
"environment variables (AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY), " \
|
|
111
|
+
"or ~/.aws/credentials." unless provider
|
|
112
|
+
|
|
113
|
+
creds = provider.credentials
|
|
114
|
+
signer = Aws::Sigv4::Signer.new(
|
|
115
|
+
service: 'sts',
|
|
116
|
+
region: signing_region,
|
|
117
|
+
access_key_id: creds.access_key_id,
|
|
118
|
+
secret_access_key: creds.secret_access_key,
|
|
119
|
+
session_token: creds.session_token
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
signed = signer.sign_request(http_method: 'GET', url: sts_url, headers: {}, body: '')
|
|
123
|
+
headers = signed.headers.dup
|
|
124
|
+
# Conjur expects these specific keys
|
|
125
|
+
headers['host'] = URI.parse(sts_url).host
|
|
126
|
+
headers.compact
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def init_from_iam(identity,
|
|
131
|
+
account: Conjur.configuration.account,
|
|
132
|
+
service_id: IAMAuthenticator::DEFAULT_SERVICE_ID,
|
|
133
|
+
aws_region: IAMAuthenticator::DEFAULT_REGION,
|
|
134
|
+
remote_ip: nil)
|
|
135
|
+
@remote_ip = remote_ip
|
|
136
|
+
@authenticator = IAMAuthenticator.new(account, service_id, identity, aws_region)
|
|
137
|
+
self
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
data/lib/conjur/api/router.rb
CHANGED
|
@@ -42,6 +42,33 @@ module Conjur
|
|
|
42
42
|
)[fully_escape account][fully_escape username]['authenticate']
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
+
# POST /authn-azure/{service_id}/{account}/{login}/authenticate
|
|
46
|
+
def authn_azure_authenticate account, service_id, identity
|
|
47
|
+
RestClient::Resource.new(
|
|
48
|
+
Conjur.configuration.core_url,
|
|
49
|
+
Conjur.configuration.rest_client_options
|
|
50
|
+
)['authn-azure'][fully_escape service_id][fully_escape account][fully_escape identity]['authenticate']
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# POST /authn-iam/{service_id}/{account}/{login}/authenticate
|
|
54
|
+
def authn_iam_authenticate account, service_id, identity
|
|
55
|
+
RestClient::Resource.new(
|
|
56
|
+
Conjur.configuration.core_url,
|
|
57
|
+
Conjur.configuration.rest_client_options
|
|
58
|
+
)['authn-iam'][fully_escape service_id][fully_escape account][fully_escape identity]['authenticate']
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# POST /authn-gcp/{account}/authenticate
|
|
62
|
+
# authn-gcp is a serviceless authenticator: Conjur derives the host from
|
|
63
|
+
# the GCP identity token's claims, so neither a service id nor the login
|
|
64
|
+
# appears in the path.
|
|
65
|
+
def authn_gcp_authenticate account
|
|
66
|
+
RestClient::Resource.new(
|
|
67
|
+
Conjur.configuration.core_url,
|
|
68
|
+
Conjur.configuration.rest_client_options
|
|
69
|
+
)['authn-gcp'][fully_escape account]['authenticate']
|
|
70
|
+
end
|
|
71
|
+
|
|
45
72
|
# Builds the RestClient::Resource for an authn-cert authentication request.
|
|
46
73
|
# cert_options must include :ssl_client_cert and :ssl_client_key so that
|
|
47
74
|
# the client certificate is presented during the TLS handshake.
|
|
@@ -62,10 +89,14 @@ module Conjur
|
|
|
62
89
|
end
|
|
63
90
|
|
|
64
91
|
def authenticator account, authenticator, service_id, credentials
|
|
65
|
-
RestClient::Resource.new(
|
|
92
|
+
resource = RestClient::Resource.new(
|
|
66
93
|
Conjur.configuration.core_url,
|
|
67
94
|
Conjur.configuration.create_rest_client_options(credentials)
|
|
68
|
-
)[fully_escape authenticator]
|
|
95
|
+
)[fully_escape authenticator]
|
|
96
|
+
# Serviceless authenticators (e.g. authn-gcp) have no service id segment:
|
|
97
|
+
# the path is authn-<type>/<account>.
|
|
98
|
+
resource = resource[fully_escape service_id] unless service_id.nil? || service_id.empty?
|
|
99
|
+
resource[fully_escape account]
|
|
69
100
|
end
|
|
70
101
|
|
|
71
102
|
def authenticators
|
data/lib/conjur/api.rb
CHANGED
|
@@ -36,6 +36,9 @@ require 'conjur/log_source'
|
|
|
36
36
|
require 'conjur/has_attributes'
|
|
37
37
|
require 'conjur/api/authenticators'
|
|
38
38
|
require 'conjur/api/authn'
|
|
39
|
+
require 'conjur/api/authn_azure'
|
|
40
|
+
require 'conjur/api/authn_iam'
|
|
41
|
+
require 'conjur/api/authn_gcp'
|
|
39
42
|
require 'conjur/api/authn_cert'
|
|
40
43
|
require 'conjur/api/roles'
|
|
41
44
|
require 'conjur/api/resources'
|
data/secrets.yml
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Summon secrets for CI. `summon -e <env> ./test.sh` resolves the !var
|
|
2
|
+
# references below from the CI Conjur vault and exports them into the
|
|
3
|
+
# environment for the wrapped command.
|
|
4
|
+
#
|
|
5
|
+
# The azure environment supplies the values the authn-azure integration
|
|
6
|
+
# feature needs (see features/authn_azure.feature and features/README.md).
|
|
7
|
+
# Mirrors the layout used by conjur-api-java / conjur-api-dotnet.
|
|
8
|
+
azure:
|
|
9
|
+
RUN_AZURE_TESTS: "true"
|
|
10
|
+
AZURE_SUBSCRIPTION_ID: !var ci/azure/subscription-id
|
|
11
|
+
AZURE_RESOURCE_GROUP: !var ci/azure/authn-test/resource-group
|
|
12
|
+
USER_ASSIGNED_IDENTITY: !var ci/azure/authn-test/user-assigned-id
|
|
13
|
+
USER_ASSIGNED_IDENTITY_CLIENT_ID: !var ci/azure/authn-test/user-assigned-id-client-id
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'cgi'
|
|
5
|
+
require 'conjur/api/router'
|
|
6
|
+
|
|
7
|
+
describe Conjur::API, api: :dummy do
|
|
8
|
+
let(:service_id) { 'my-azure-service' }
|
|
9
|
+
let(:identity) { 'host/my-app/azure-host' }
|
|
10
|
+
let(:azure_jwt) { 'eyJhbGciOiJSUzI1NiJ9.azure-payload.sig' }
|
|
11
|
+
let(:raw_token) { { 'protected' => 'p', 'payload' => 'pl', 'signature' => 's' } }
|
|
12
|
+
|
|
13
|
+
describe '.new_from_azure' do
|
|
14
|
+
it 'returns an API instance with an AzureAuthenticator' do
|
|
15
|
+
allow(Conjur::API).to receive(:authenticate_azure).and_return(raw_token)
|
|
16
|
+
api_instance = Conjur::API.new_from_azure(service_id, identity, account: account)
|
|
17
|
+
expect(api_instance).to be_a(Conjur::API)
|
|
18
|
+
expect(api_instance.authenticator).to be_a(Conjur::API::AzureAuthenticator)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
describe '.authenticate_azure' do
|
|
23
|
+
let(:resource) { double('resource') }
|
|
24
|
+
|
|
25
|
+
before do
|
|
26
|
+
allow(Conjur::API).to receive(:url_for)
|
|
27
|
+
.with(:authn_azure_authenticate, account, service_id, identity)
|
|
28
|
+
.and_return(resource)
|
|
29
|
+
allow(resource).to receive(:post)
|
|
30
|
+
.with("jwt=#{CGI.escape(azure_jwt)}", content_type: 'application/x-www-form-urlencoded')
|
|
31
|
+
.and_return(raw_token.to_json)
|
|
32
|
+
allow(Conjur::API).to receive(:fetch_azure_jwt).and_return(azure_jwt)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'returns a parsed token' do
|
|
36
|
+
token = Conjur::API.authenticate_azure(service_id, identity, account: account)
|
|
37
|
+
expect(token).to eq(raw_token)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'raises ArgumentError when service_id is empty' do
|
|
41
|
+
expect { Conjur::API.authenticate_azure('', identity, account: account) }
|
|
42
|
+
.to raise_error(ArgumentError, /service_id is required/)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it 'raises ArgumentError when identity is empty' do
|
|
46
|
+
expect { Conjur::API.authenticate_azure(service_id, '', account: account) }
|
|
47
|
+
.to raise_error(ArgumentError, /identity is required/)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
describe 'AzureAuthenticator#refresh_token' do
|
|
52
|
+
subject(:authenticator) do
|
|
53
|
+
Conjur::API::AzureAuthenticator.new(account, service_id, identity)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it 'delegates to authenticate_azure with stored credentials' do
|
|
57
|
+
expect(Conjur::API).to receive(:authenticate_azure)
|
|
58
|
+
.with(service_id, identity,
|
|
59
|
+
account: account,
|
|
60
|
+
resource_uri: Conjur::API::AzureAuthenticator::DEFAULT_RESOURCE,
|
|
61
|
+
client_id: nil,
|
|
62
|
+
imds_base_url: Conjur::API::AzureAuthenticator::DEFAULT_IMDS_BASE,
|
|
63
|
+
imds_api_version: Conjur::API::AzureAuthenticator::DEFAULT_IMDS_VER)
|
|
64
|
+
.and_return(raw_token)
|
|
65
|
+
expect(authenticator.refresh_token).to eq(raw_token)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
describe Conjur::API::Router do
|
|
71
|
+
let(:account) { 'myaccount' }
|
|
72
|
+
let(:service_id) { 'my-service' }
|
|
73
|
+
let(:identity) { 'host/my-app/azure-host' }
|
|
74
|
+
|
|
75
|
+
before do
|
|
76
|
+
allow(Conjur.configuration).to receive(:core_url).and_return('https://conjur.example.com')
|
|
77
|
+
allow(Conjur.configuration).to receive(:rest_client_options).and_return({})
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
describe '#authn_azure_authenticate' do
|
|
81
|
+
subject { described_class.authn_azure_authenticate(account, service_id, identity) }
|
|
82
|
+
|
|
83
|
+
it 'returns a RestClient::Resource' do
|
|
84
|
+
expect(subject).to be_a(RestClient::Resource)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it 'builds the correct URL' do
|
|
88
|
+
expect(subject.url).to end_with('/authn-azure/my-service/myaccount/host%2Fmy-app%2Fazure-host/authenticate')
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|