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

Add OIDC config primitives #49

Merged
merged 66 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
e7d31cb
rebased
Oct 7, 2024
593126b
class vars
Oct 8, 2024
ad3c237
cleanup
Oct 8, 2024
770bc42
cleanup
Oct 8, 2024
8e999f3
adding comments
Oct 8, 2024
3e9da0e
comments
Oct 8, 2024
616e979
fixed yml file
Oct 8, 2024
1903d11
cleanup
Oct 8, 2024
806fca2
cleanup client configure
Oct 8, 2024
84b1638
comments
Oct 8, 2024
9d55d26
cleanup
Oct 9, 2024
6915b0e
fix provider addr
Oct 9, 2024
9ff6d1f
separate out issuer config
Oct 9, 2024
97fdf62
added comments
Oct 9, 2024
20cb10d
cleanup readme
Oct 9, 2024
8f6d97a
cleanup
Oct 9, 2024
e496ef6
fix for prod
Oct 10, 2024
d33a69c
fixed issuer
Oct 10, 2024
14326f0
cleanup
Oct 10, 2024
4957347
comment
Oct 10, 2024
f3a4828
cleanup local names
Oct 10, 2024
3d3d1fb
cleanup local names
Oct 10, 2024
5c59530
review fixes
Oct 11, 2024
52e2420
application.rb fixes
Oct 11, 2024
0617b30
application.rb fixes
Oct 11, 2024
71a2259
cleanup configs
Oct 11, 2024
289dea6
added initial_user()
Oct 11, 2024
87a56aa
don't refresh the provider unnecessarily
Oct 11, 2024
66c9bb1
cleanup
Oct 11, 2024
0d6bf1c
rubocop
Oct 11, 2024
bb90ac4
memoized oidc_provider
Oct 11, 2024
a5dfa94
cleanup
Oct 11, 2024
3897835
removed unneeded reader policy
Oct 11, 2024
9a49426
moved provider to test dir
Oct 11, 2024
9bf5124
init
Oct 15, 2024
a733396
moved provider
Oct 15, 2024
2a90392
refactored init code
Oct 15, 2024
56b6f76
rake task
Oct 16, 2024
52122d0
rake task
Oct 16, 2024
2793425
added Config
Oct 16, 2024
609cbcf
rake task working
Oct 16, 2024
a43272f
moved oidc comment block
Oct 16, 2024
0772fdd
removed utils/oidc.rb
Oct 16, 2024
3402488
cleanup
Oct 16, 2024
a474cbd
added tests
Oct 16, 2024
4f7d5d3
fixed token
Oct 16, 2024
2aba8fb
provider tests
Oct 16, 2024
7456b02
cleanup
Oct 17, 2024
3774510
fix test comments
Oct 17, 2024
caeb603
commented rake task
Oct 17, 2024
ed18d74
fixed initial user
Oct 17, 2024
bbb3255
cleanup review comments
Oct 17, 2024
5c5f3c5
rubocop
Oct 17, 2024
ca256c8
Merge branch 'main' into addOidcProviderRebase
GeorgeJahad Oct 17, 2024
70329c3
Because the "VAULT_SSL_CERT" env var is set, added ssl
Oct 18, 2024
de43ef3
updated Brakeman
Oct 18, 2024
8f195ca
added oidc provider ssl
Oct 18, 2024
a87e951
fixed up provider certs
Oct 18, 2024
6b866af
fixed oidc_provider for ssl
Oct 20, 2024
7ae52ba
fix for oidc_provider/ssl
Oct 21, 2024
5e0eda0
fixed issuer path
Oct 21, 2024
350d286
add comment for oidcProvider tls
Oct 21, 2024
dc4ece1
fixed issuer
Oct 21, 2024
879b0db
fixed comment
Oct 21, 2024
2f35777
rubocop
Oct 21, 2024
909516e
fixed readme
Oct 21, 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
2 changes: 1 addition & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

// Use 'forwardPorts' to make a list of ports inside the container available locally.
// This can be used to network with other containers or the host.
"forwardPorts": [3000, 5432, 8200],
"forwardPorts": [3000, 5432, 8200, 8300],

// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "bundle install && rake db:setup",
Expand Down
9 changes: 9 additions & 0 deletions .devcontainer/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ services:
VAULT_DEV_ROOT_TOKEN_ID: root_token
VAULT_DEV_LISTEN_ADDRESS: 0.0.0.0:8200

oidc_provider:
image: hashicorp/vault:latest
restart: unless-stopped
ports:
- 8300:8300
environment:
VAULT_DEV_ROOT_TOKEN_ID: root_token
VAULT_DEV_LISTEN_ADDRESS: 0.0.0.0:8300

app_registry:
image: node:latest
restart: unless-stopped
Expand Down
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,43 @@ docker build -t astral:latest .
```
docker run -p 3000:3000 astral:latest
```
# Logging into vault with OIDC

The rails test's configure the OIDC provider, so if the tests pass,
you can invoke the oidc login as follows:
suprjinx marked this conversation as resolved.
Show resolved Hide resolved

```
export VAULT_ADDR=http://127.0.0.1:8200; vault login -method=oidc
```

You should do this on your host machine, not in docker. This will
allow a browser window to open on your host. When it does, select
"username" option with user test/test. (That is the username/pw
configured by the rails tests.)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

configured at start up

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


When that succeeds, you should see something like the following in the cli:
```
Success! You are now authenticated
.
identity_policies ["[email protected]"]
.
.
```

Note that "identity_policies" includes "[email protected]", which is the
policy we created for this user.

To make this work smoothly with the browser, you should add the
following to the /etc/hosts file on your host:

```
127.0.0.1 oidc_provider
```

Finally, if you run "rails test" a second time, it will recreate the
GeorgeJahad marked this conversation as resolved.
Show resolved Hide resolved
provider settings, so you will need to clear the browser's
"oidc_provider" cookie. Otherwise you will see this error:

```
* Vault login failed. Expired or missing OAuth state.
```
1 change: 1 addition & 0 deletions app/lib/clients/vault.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ class Vault
extend Clients::Vault::Policy
extend Clients::Vault::Entity
extend Clients::Vault::EntityAlias
extend Clients::Vault::Oidc

class_attribute :token

Expand Down
151 changes: 151 additions & 0 deletions app/lib/clients/vault/oidc.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
=begin

The purpose of this module is to assign a policy to an OIDC user, by
mapping that user's email address to a policy we create.
It works as follows:

It creates an OIDC provider and user. That user has a
username/password/email addr, that can be accessed with OIDC auth.

It creates an OIDC client which connects to that provider. When a
user tries to auth, the client connects to the provider, which opens
up a browser window allowing the user to enter his username/password.

On success, the provider returns an OIDC token, which includes the
user's email addr.

The client has been configured to map that email address to an entity
in vault, which has the policy which we want the user to have.

So the mapping goes from the email address on the provider, to the
policy in vault. The email addr may not be the best mapping to use.
Some other piece of user info may ultimately be better used to map the
user to the policy. But we don't yet have a good understanding of
GeorgeJahad marked this conversation as resolved.
Show resolved Hide resolved
different OIDC provider configurations, so this should be good enough
for now

Note that this provider is only meant to be used in our dev/test
environment to excercise the client. In a prod env, a real OIDC
provider is configured in.
GeorgeJahad marked this conversation as resolved.
Show resolved Hide resolved

=end
module Clients
class Vault
module Oidc
def configure_oidc_provider
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we should separate the "configure as provider" and "configure as client" as distinct module files? just for conceptual clarity

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or adding "as" in the method names -- in this case we are not configuring Vault's oidc provider but setting up vault as a provider; in the other methods we are configuring Vault's oidc provider ("_as_client")?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we should separate the "configure as provider" and "configure as client" as distinct module files? just for conceptual clarity

I think that is a good idea. I'll separate out the modules

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

create_provider_webapp
create_provider_with_email_scope
create_entity_for_initial_user
create_userpass_for_initial_user
map_userpass_to_entity
end

def configure_oidc_client(issuer, client_id, client_secret)
create_client_config(issuer, client_id, client_secret)
create_default_policy_for_role
create_default_role(client_id)
end

def configure_oidc_user(name, email, policy)
client.sys.put_policy(email, policy)
put_entity(name, email)
put_entity_alias(name, email, "oidc")
end

private
suprjinx marked this conversation as resolved.
Show resolved Hide resolved
cattr_accessor :client_id
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

conventionally i think these go at the top of the module definition

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

cattr_accessor :client_secret
WEBAPP_NAME = "identity/oidc/client/astral"

def oidc_provider
GeorgeJahad marked this conversation as resolved.
Show resolved Hide resolved
::Vault::Client.new(
address: "http://oidc_provider:8300",
Copy link
Contributor

@dave-gantenbein dave-gantenbein Oct 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this static string what you want here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed to config param in astral.yml

token: token
)
end

def create_provider_webapp
oidc_provider.logical.write(
WEBAPP_NAME,
redirect_uris: get_redirect_uris,
assignments: "allow_all")
app = oidc_provider.logical.read(WEBAPP_NAME)
@@client_id = app.data[:client_id]
@@client_secret = app.data[:client_secret]
end

def create_provider_with_email_scope
oidc_provider.logical.write("identity/oidc/scope/email",
template: '{"email": {{identity.entity.metadata.email}}}')
oidc_provider.logical.write("identity/oidc/provider/astral",
issuer: "http://oidc_provider:8300",
allowed_client_ids: @@client_id,
scopes_supported: "email")
end

def create_entity_for_initial_user
oidc_provider.logical.write("identity/entity",
policies: "default",
name: Config[:initial_user][:name],
GeorgeJahad marked this conversation as resolved.
Show resolved Hide resolved
metadata: "email=#{Config[:initial_user][:email]}",
disabled: false)
end

def create_userpass_for_initial_user
oidc_provider.logical.delete("/sys/auth/userpass")
oidc_provider.logical.write("/sys/auth/userpass", type: "userpass")
oidc_provider.logical.write("/auth/userpass/users/#{Config[:initial_user][:name]}",
password: Config[:initial_user][:password])
end

def map_userpass_to_entity
entity = oidc_provider.logical.read(
"identity/entity/name/#{Config[:initial_user][:name]}")
entity_id = entity.data[:id]
auth_list = oidc_provider.logical.read("/sys/auth")
accessor = auth_list.data[:"userpass/"][:accessor]
oidc_provider.logical.write("identity/entity-alias",
name: Config[:initial_user][:name],
canonical_id: entity_id,
mount_accessor: accessor)
end

def create_client_config(issuer, client_id, client_secret)
client.logical.delete("/sys/auth/oidc")
client.logical.write("/sys/auth/oidc", type: "oidc")
client.logical.write("auth/oidc/config",
oidc_discovery_url: issuer,
oidc_client_id: client_id,
oidc_client_secret: client_secret,
default_role: "reader")
end

def create_default_policy_for_role
policy = <<-EOH
path "sys" {
policy = "read"
}
EOH
client.sys.put_policy("reader", policy)
end

def get_redirect_uris
# use localhost:8250, per: https://developer.hashicorp.com/vault/docs/auth/jwt#redirect-uris
redirect_uris = <<-EOH
http://localhost:8250/oidc/callback,
#{Config[:vault_addr]}/ui/vault/auth/oidc/oidc/callback,
EOH
end

def create_default_role(client_id)
client.logical.write(
"auth/oidc/role/reader",
bound_audiences: client_id,
allowed_redirect_uris: get_redirect_uris,
user_claim: "email",
oidc_scopes: "email",
token_policies: "reader")
end
end
end
end
15 changes: 15 additions & 0 deletions app/lib/clients/vault/policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,21 @@ def create_astral_policy
path "#{kv_mount}/data/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
path "identity/entity" {
capabilities = ["create", "read", "update", "delete", "list"]
}
path "identity/entity/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
path "identity/entity-alias" {
capabilities = ["create", "read", "update", "delete", "list"]
}
path "/sys/auth" {
capabilities = ["read"]
}
path "/sys/policy/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
HCL

client.sys.put_policy("astral_policy", policy)
Expand Down
9 changes: 9 additions & 0 deletions config/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,15 @@ class Application < Rails::Application
Clients::Vault.token = Config[:vault_token]
Clients::Vault.configure_kv
Clients::Vault.configure_pki
issuer = config.astral.oidc_provider[:issuer]
client_id = config.astral.oidc_provider[:client_id]
client_secret = config.astral.oidc_provider[:client_secret]
if config.astral.configure_oidc_provider?
GeorgeJahad marked this conversation as resolved.
Show resolved Hide resolved
Clients::Vault.configure_oidc_provider
client_id = ::Clients::Vault::Oidc.client_id
client_secret = ::Clients::Vault::Oidc.client_secret
end
Clients::Vault.configure_oidc_client(issuer, client_id, client_secret) unless client_id.nil?
Clients::Vault.rotate_token
end
end
Expand Down
19 changes: 19 additions & 0 deletions config/astral.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,28 @@ shared:
audit_log_file: <%= "#{Rails.root.join('log')}/astral-audit.log" %>

test:
configure_oidc_provider?: true
oidc_provider:
issuer: "http://oidc_provider:8300/v1/identity/oidc/provider/astral"
initial_user:
name: "test"
password: "test"
email: "[email protected]"
cert_ttl: <%= 24.hours.in_seconds %>

development:
configure_oidc_provider?: true
oidc_provider:
issuer: "http://oidc_provider:8300/v1/identity/oidc/provider/astral"
initial_user:
name: "test"
password: "test"
email: "[email protected]"

production:
vault_create_root: false
configure_oidc_provider?: false
oidc_provider:
suprjinx marked this conversation as resolved.
Show resolved Hide resolved
issuer:
client_id:
client_secret:
30 changes: 30 additions & 0 deletions test/lib/clients/oidc_test.rb
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice that the test is so easy, after all the hard setup!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So to be clear, the real test is to use "vault login -method=oidc" and then interact with the browser, as I describe in the readme, but I didn't want to force the browser interaction on the rails test.

Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
require "test_helper"

# NOTE: these tests excercise the OIDC config but can't really verify a
# successful OIDC login. (Because that requires browser interaction.)
# See the readme for how to use oidc login with the browser.

class OIDCTest < ActiveSupport::TestCase
setup do
@client = Clients::Vault
end

test "#configure_oidc_user" do
GeorgeJahad marked this conversation as resolved.
Show resolved Hide resolved
@client.configure_oidc_user(Config[:initial_user][:name],
Config[:initial_user][:email], get_test_policy)
entity = @client.read_entity(Config[:initial_user][:name])
assert_equal Config[:initial_user][:email], entity.data[:policies][0]
aliases = entity.data[:aliases]
assert aliases.find { |a| a[:name] == Config[:initial_user][:email] }
end

private

def get_test_policy
policy = <<-EOH
path "sys" {
policy = "read"
}
EOH
end
end