Skip to content

Commit

Permalink
Kv group auth (#80)
Browse files Browse the repository at this point in the history
* Add `groups` attribute to secrets request, which are granted read-only policy on the KV
  • Loading branch information
suprjinx authored Nov 20, 2024
1 parent 184ab37 commit 91966d2
Show file tree
Hide file tree
Showing 38 changed files with 624 additions and 169 deletions.
1 change: 1 addition & 0 deletions .devcontainer/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ services:
volumes:
- ../cert:/vault/cert
environment:
VAULT_LOG_LEVEL: debug
VAULT_DEV_ROOT_TOKEN_ID: root_token
VAULT_LOCAL_CONFIG: >
{
Expand Down
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,7 @@ group :development, :test do

# Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/]
gem "rubocop-rails-omakase", require: false

# Mocking for tests
gem "mocha"
end
4 changes: 4 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ GEM
marcel (1.0.4)
mini_mime (1.1.5)
minitest (5.25.1)
mocha (2.5.0)
ruby2_keywords (>= 0.0.5)
msgpack (1.7.2)
net-http (0.4.1)
uri
Expand Down Expand Up @@ -243,6 +245,7 @@ GEM
rubocop-performance
rubocop-rails
ruby-progressbar (1.13.0)
ruby2_keywords (0.0.5)
securerandom (0.3.1)
sqlite3 (2.2.0-aarch64-linux-gnu)
sqlite3 (2.2.0-aarch64-linux-musl)
Expand Down Expand Up @@ -300,6 +303,7 @@ DEPENDENCIES
jbuilder
json_tagged_logger
jwt
mocha
ostruct
puma (>= 5.0)
rails (~> 7.2.2)
Expand Down
5 changes: 1 addition & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Some features of Astral:
0) Configure Astral-specific Certificate Authority and Key-Value stores in Vault
1) Authenticate requests for cerficates or secrets using a third party
trusted source (JWT with signing key, eg)
2) For certiciates:
2) For certificates:
a) Authorize the request using a Domain Ownership registry, where domain owner
or authorized groups must match the identity of the requesting client
b) When authorized, obtain a certificate for the common name
Expand Down Expand Up @@ -162,9 +162,6 @@ config/astral.yml).

The rails test's configure the OIDC initial user, so if the tests pass,
you can invoke the oidc login as follows:

To use SSL in production, provide the necessary environment (SSL_CERT, SSL_KEY) to
the container environment, and use the `bin/ssl.sh` startup command. Eg:
```
export VAULT_ADDR=http://127.0.0.1:8200; vault login -method=oidc
```
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/secrets_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,6 @@ def destroy
private

def params_permitted
params.require(:secret).permit(:path, data: {})
params.require(:secret).permit(:path, :groups, data: {})
end
end
4 changes: 2 additions & 2 deletions app/interactors/authorize_cert_request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ def call
domain = Domain.where(fqdn: fqdn).first
context.fail!(error: AuthError.new("Common or alt name not recognized")) unless domain
context.fail!(error: AuthError.new("No subject or group authorization")) unless
domain.users_array.include?(context.identity.subject) ||
(domain.group_delegation? && (domain.groups_array & context.identity.groups).any?)
domain.users.include?(context.identity.subject) ||
(domain.group_delegation? && (domain.groups & context.identity.groups).any?)
end
nil
ensure
Expand Down
2 changes: 1 addition & 1 deletion app/interactors/write_secret.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
class WriteSecret < ApplicationInteractor
def call
if secret = Services::KeyValue.write(context.identity, context.request.path, context.request.data)
if secret = Services::KeyValue.write(context.identity, context.request.groups_array, context.request.path, context.request.data)
context.secret = secret
else
context.fail!(message: "Failed to store secret")
Expand Down
4 changes: 2 additions & 2 deletions app/lib/clients/app_registry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ def convert(domain_info)
OpenStruct.new(
fqdn: domain_info["fullyQualifiedDomainName"],
group_delegation: domain_info["ownerDelegatedRequestsToTeam"],
groups: domain_info["autoApprovedGroups"],
users: domain_info["autoApprovedServiceAccounts"]
groups: domain_info["autoApprovedGroups"]&.split(","),
users: domain_info["autoApprovedServiceAccounts"]&.split(",")
)
end

Expand Down
4 changes: 2 additions & 2 deletions app/lib/clients/vault.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ class Vault
extend Clients::Vault::Certificate
extend Clients::Vault::KeyValue
extend Clients::Vault::Policy
extend Clients::Vault::Entity
extend Clients::Vault::EntityAlias
extend Clients::Vault::Identity
extend Clients::Vault::IdentityAlias
extend Clients::Vault::Oidc

class_attribute :token
Expand Down
2 changes: 1 addition & 1 deletion app/lib/clients/vault/certificate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ def issue_cert(identity, cert_issue_request)
opts = cert_issue_request.attributes
# Generate the TLS certificate using the intermediate CA
tls_cert = client.logical.write(cert_path, opts)
assign_policy(identity, GENERIC_CERT_POLICY_NAME)
assign_entity_policy(identity, GENERIC_CERT_POLICY_NAME)
OpenStruct.new tls_cert.data
end

Expand Down
29 changes: 0 additions & 29 deletions app/lib/clients/vault/entity.rb

This file was deleted.

47 changes: 0 additions & 47 deletions app/lib/clients/vault/entity_alias.rb

This file was deleted.

72 changes: 72 additions & 0 deletions app/lib/clients/vault/identity.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
module Clients
class Vault
module Identity
def put_entity(name, policies)
write_identity(path: "identity/entity",
name: name,
policies: policies,
extra_params: [ :metadata, :disabled ])
end

def put_group(name, policies)
write_identity(path: "identity/group",
name: name,
policies: policies,
extra_params: [ :metadata, :type, :member_group_ids, :member_entity_ids ],
defaults: { type: "external" })
end

def read_entity(name)
client.logical.read("identity/entity/name/#{name}")
end

def delete_entity(name)
client.logical.delete("identity/entity/name/#{name}")
end

def get_entity_data(name)
get_identity_data("identity/entity/name/#{name}")
end

def read_group(name)
client.logical.read("identity/group/name/#{name}")
end

def get_group_data(name)
get_identity_data("identity/group/name/#{name}")
end

private

def write_identity(path:, name:, policies:, defaults: {}, extra_params: [], merge_policies: true)
full_path = "#{path}/name/#{name}"
Domain.with_advisory_lock(full_path) do
identity = client.logical.read(full_path)
policies = (policies || []) + (identity&.data&.fetch(:policies) || []) if merge_policies
params = defaults.
merge({
name: name,
policies: policies.uniq
}).
merge((identity&.data || {}).
slice(*extra_params)).
compact
# cannot supply member ids for external group
if params[:type] == "external"
params.delete(:member_entity_ids)
end
client.logical.write(path, params)
end
end

def get_identity_data(path)
identity = client.logical.read(path)
if identity
[ identity.data[:policies], identity.data[:metadata] ]
else
[ [], {} ]
end
end
end
end
end
80 changes: 80 additions & 0 deletions app/lib/clients/vault/identity_alias.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
module Clients
class Vault
module IdentityAlias
def put_entity_alias(entity_name, alias_name, auth_path)
write_identity_alias("entity", entity_name, alias_name, auth_path)
end

def put_group_alias(group_name, alias_name, auth_path)
write_identity_alias("group", group_name, alias_name, auth_path)
end

def read_entity_alias(entity_name, alias_name, auth_path)
read_identity_alias("entity", entity_name, alias_name, auth_path)
end

def read_group_alias(group_name, alias_name, auth_path)
read_identity_alias("group", group_name, alias_name, auth_path)
end

def delete_entity_alias(entity_name, alias_name, auth_path)
identity = client.logical.read("identity/entity/name/#{entity_name}")
if identity.nil?
raise "no such #{type} #{identity_name}"
end
id = find_identity_alias_id(identity, alias_name, auth_path)
if id.nil?
raise "no such alias #{alias_name}"
end
client.logical.delete("identity/entity-alias/id/#{id}")
end

private

def find_identity_alias_id(identity, alias_name, auth_path)
aliases = identity.data[:aliases] || [ identity.data[:alias] ]
a = find_alias(aliases, alias_name, auth_path)
a&.fetch(:id)
end

def find_alias(aliases, name, auth_path)
aliases&.find { |a| a[:name] == name && a[:mount_path] == "auth/#{auth_path}/" }
end

def read_identity_alias(type, identity_name, alias_name, auth_path)
identity = client.logical.read("identity/#{type}/name/#{identity_name}")
if identity.nil?
raise "no such #{type} #{identity_name}"
end
id = find_identity_alias_id(identity, alias_name, auth_path)
if id.nil?
raise "no such alias #{alias_name}"
end
client.logical.read("identity/#{type}-alias/id/#{id}")
end

def write_identity_alias(type, identity_name, alias_name, auth_path)
auth_sym = "#{auth_path}/".to_sym
accessor = client.logical.read("/sys/auth")
accessor = accessor.data[auth_sym][:accessor]

identity = client.logical.read("identity/#{type}/name/#{identity_name}")
if identity.nil?
raise "no such #{type} #{identity_name}"
end
aliases = (identity.data[:aliases] || [ identity.data[:alias] ])
identity_alias = find_alias(aliases, alias_name, auth_path)
# only create alias when not existant
unless identity_alias
client.logical.write("identity/#{type}-alias",
{
name: alias_name,
mount_accessor: accessor,
canonical_id: identity.data[:id]
}
)
end
end
end
end
end
Loading

0 comments on commit 91966d2

Please sign in to comment.