diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 32d22d1..578f913 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -17,10 +17,10 @@ // 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", + "postCreateCommand": "bundle install && rake oidc_provider:configure && rake db:setup", // Configure tool-specific properties. // "customizations": {}, diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 2b58180..e73e950 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -48,6 +48,34 @@ services: "max_lease_ttl": "720h" } + oidc_provider: + image: hashicorp/vault:latest + restart: unless-stopped + ports: + - 8300:8300 + - 9443:9443 + volumes: + - ../cert:/vault/cert + environment: + VAULT_DEV_ROOT_TOKEN_ID: root_token + VAULT_DEV_LISTEN_ADDRESS: 0.0.0.0:8300 + VAULT_LOCAL_CONFIG: > + { + "listener": [ + { + "tcp": { + "address": "0.0.0.0:9443", + "tls_disable": "0", + "tls_cert_file": "/vault/cert/oidc_provider.pem", + "tls_key_file": "/vault/cert/oidc_provider.key" + } + } + ], + "default_lease_ttl": "168h", + "max_lease_ttl": "720h" + } + + app_registry: image: node:latest restart: unless-stopped diff --git a/Gemfile.lock b/Gemfile.lock index ac74dd8..0f925c8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -79,7 +79,7 @@ GEM bigdecimal (3.1.8) bootsnap (1.18.4) msgpack (~> 1.2) - brakeman (6.2.1) + brakeman (6.2.2) racc builder (3.3.0) concurrent-ruby (1.3.4) diff --git a/README.md b/README.md index 00ca829..04fe05a 100644 --- a/README.md +++ b/README.md @@ -87,3 +87,76 @@ rake configure:ssl To use SSL in the devcontainer, edit `.devcontainer/docker-compose.yml` so that the `app` service has `VAULT_ADDRESS` of `https://vault:8443`. + +## OIDC configuration +The OIDC modules allow the assignment of a policy to an OIDC user, by +mapping that user's email address to a policy we create. They work as +follows: + +OidcProvider.new.configure creates an OIDC provider +and user on a separate dedicated Vault instance. The user created has +a username/password/email address, that can be accessed with OIDC auth +from the principal Vault instance. + +Clients::Vault::configure_as_oidc_client creates an OIDC +client on our Vault instance. It connects to that provider just +created. When a user tries to auth, the client connects to the +provider, which opens up a browser window allowing the user to enter +their username/password. + +On success, the provider returns an OIDC token, which includes the +user's email address. + +The OIDC 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. + +Note that this provider is mainly meant to be used in our dev/test +environment to excercise the client. In a prod env, a real OIDC +provider would probably be used instead, (by configuring it in +config/astral.yml). + +# Logging into Vault with OIDC + +The rails test's configure the OIDC initial user, so if the tests pass, +you can invoke the oidc login as follows: + +``` + 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 at startup.) + +When that succeeds, you should see something like the following in the cli: +``` +Success! You are now authenticated +. +identity_policies ["test@example.com"] +. +. +``` + +Note that "identity_policies" includes "test@example.com", 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 restart the docker Vault container, it will recreate +the 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. +``` + + diff --git a/app/lib/clients/vault.rb b/app/lib/clients/vault.rb index 10ec7d9..5a68e85 100644 --- a/app/lib/clients/vault.rb +++ b/app/lib/clients/vault.rb @@ -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 diff --git a/app/lib/clients/vault/oidc.rb b/app/lib/clients/vault/oidc.rb new file mode 100644 index 0000000..ac1dd36 --- /dev/null +++ b/app/lib/clients/vault/oidc.rb @@ -0,0 +1,50 @@ +module Clients + class Vault + module Oidc + def configure_as_oidc_client(issuer, client_id, client_secret) + if client_id.nil? || !oidc_auth_data.nil? + return + end + create_client_config(issuer, client_id, client_secret) + 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 + + def get_oidc_client_config + client.logical.read("auth/oidc/config") + end + + private + + def create_client_config(issuer, client_id, client_secret) + client.logical.write("/sys/auth/oidc", type: "oidc") + client.logical.write("auth/oidc/config", + oidc_discovery_url: issuer, + oidc_discovery_ca_pem: File.read(Config[:oidc_provider_ssl_cert]), + oidc_client_id: client_id, + oidc_client_secret: client_secret, + default_role: "default") + end + + def create_default_role(client_id) + client.logical.write( + "auth/oidc/role/default", + bound_audiences: client_id, + allowed_redirect_uris: Config[:oidc_redirect_uris], + user_claim: "email", + oidc_scopes: "email", + token_policies: "default") + end + + def oidc_auth_data + auth_list = client.logical.read("/sys/auth") + auth_list.data[:"oidc/"] + end + end + end +end diff --git a/app/lib/clients/vault/policy.rb b/app/lib/clients/vault/policy.rb index 0d670c7..d2b12d0 100644 --- a/app/lib/clients/vault/policy.rb +++ b/app/lib/clients/vault/policy.rb @@ -20,6 +20,24 @@ 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 "auth/oidc/config" { + capabilities = ["read"] + } + path "/sys/policy/*" { + capabilities = ["create", "read", "update", "delete", "list"] + } HCL client.sys.put_policy("astral_policy", policy) diff --git a/app/lib/utils/oidc_provider.rb b/app/lib/utils/oidc_provider.rb new file mode 100644 index 0000000..9c8c539 --- /dev/null +++ b/app/lib/utils/oidc_provider.rb @@ -0,0 +1,93 @@ +class OidcProvider + attr_reader :client_id + attr_reader :client_secret + attr_reader :vault_client + + def configure + provider = vault_client.logical.read("identity/oidc/provider/astral") + if provider.nil? + create_provider_webapp + create_provider_with_email_scope + create_entity_for_initial_user + create_userpass_for_initial_user + map_userpass_to_entity + else + get_client_info + end + end + + + def get_client_info + app = vault_client.logical.read(WEBAPP_NAME) + @client_id = app.data[:client_id] + @client_secret = app.data[:client_secret] + [ @client_id, @client_secret ] + end + + def get_info + vault_client.logical.read("identity/oidc/provider/astral") + end + + def self.get_configured_issuer + Config[:oidc_provider_addr] + Config[:oidc_issuer_path] + end + + private + WEBAPP_NAME = "identity/oidc/client/astral" + + def vault_client + @vault_client ||= + ::Vault::Client.new( + address: Config[:oidc_provider_addr], + token: Config[:vault_token], + ssl_ca_cert: Config[:oidc_provider_ssl_cert], + ssl_pem_file: Config[:oidc_provider_ssl_client_cert], + ssl_key_file: Config[:oidc_provider_ssl_client_key] + ) + end + + def create_provider_webapp + vault_client.logical.write( + WEBAPP_NAME, + redirect_uris: Config[:oidc_redirect_uris], + assignments: "allow_all") + get_client_info + end + + def create_provider_with_email_scope + vault_client.logical.write("identity/oidc/scope/email", + template: '{"email": {{identity.entity.metadata.email}}}') + vault_client.logical.write("identity/oidc/provider/astral", + issuer: Config[:oidc_provider_addr], + allowed_client_ids: @client_id, + scopes_supported: "email") + vault_client.logical.read("identity/oidc/provider/astral") + end + + def create_entity_for_initial_user + vault_client.logical.write("identity/entity", + policies: "default", + name: Config[:initial_user_name], + metadata: "email=#{Config[:initial_user_email]}", + disabled: false) + end + + def create_userpass_for_initial_user + vault_client.logical.delete("/sys/auth/userpass") + vault_client.logical.write("/sys/auth/userpass", type: "userpass") + vault_client.logical.write("/auth/userpass/users/#{Config[:initial_user_name]}", + password: Config[:initial_user_password]) + end + + def map_userpass_to_entity + entity = vault_client.logical.read( + "identity/entity/name/#{Config[:initial_user_name]}") + entity_id = entity.data[:id] + auth_list = vault_client.logical.read("/sys/auth") + accessor = auth_list.data[:"userpass/"][:accessor] + vault_client.logical.write("identity/entity-alias", + name: Config[:initial_user_name], + canonical_id: entity_id, + mount_accessor: accessor) + end +end diff --git a/cert/oidc_provider.csr b/cert/oidc_provider.csr new file mode 100644 index 0000000..4386d8c --- /dev/null +++ b/cert/oidc_provider.csr @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIEyjCCArICAQAwWjELMAkGA1UEBhMCVVMxDzANBgNVBAgMBkRlbmlhbDEUMBIG +A1UEBwwLU3ByaW5nZmllbGQxDDAKBgNVBAoMA0RpczEWMBQGA1UEAwwNb2lkY19w +cm92aWRlcjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL9vn3CPEeeX +hw+Ti+MF5jFTjVYjNXD05lPx8p2CapGBTU1qQTQBgtHzz6X/DfBZ6g+m/uDqtFIj +W4VrJ0SAT8Mn7ift4lPZB7cJJDiJ1rXYgkeISk1v9xchcpnPChS8WpsK37MS8sZX +1asuCQFWkRxCOIOcIHTGjYZ/CAupFUIR24AoA/ubtpegB/oiaVS8aRg+jq5o2t2g +X9/x25ZM+TXm9+Ofg43y7oc431qbLzih83EmG2Fxgku+4Swb2s6vHUCCggqn8Rz6 +b5sGl3SUxxXlw4+Eh2lXiOkI38VAXE9z4OpD08p+iaWGqyFRmw3798yAMCO9WUer +ty+PxFZ1G68GAeNmREE+xE1/fMvVQw+uQxQKJqUCfEHCujAQUWHzG/TDEytkzR4H +zPyOnDXLHX1GNBdRvp+hVCxdmXR4K7NMNNWuOs3g6s30BWA27Wic67AwDibLfUqu +Cq/2ifv2iim/lVhBF7gpaKsJxfsqAHggerEPVpIGQS3E7lTbVh+y5lQAGpzT517+ +JXCxh4qVyw7SWd/U0qvp0GogdcJDU4JUSMKSuAK/uvpLr+MxNxHWZZ1q4uxJvslj +7LAttKQi7V9ctk0jzgSyQNoY9xBPkQFvmO+7XNkJg11bWIxW94W37CNeyglFpbh0 +c2S7tQfVI14r/aQwewfBt16uDxPq/gNzAgMBAAGgKzApBgkqhkiG9w0BCQ4xHDAa +MBgGA1UdEQQRMA+CDW9pZGNfcHJvdmlkZXIwDQYJKoZIhvcNAQELBQADggIBAAH7 +kCGnejVLi30rOHlU/QjyaHBJYQsBf1vrhqrM7xNB+u7uXaZ6A13L6dVrgvosKNOs +PYeABtmD9viIwfQoXhqiaUIsx/1hnReOJVlBaLyG9cOJB1tsZ7udWCVEbfg7GmoW +RDX7+P/sb1MV2raXbp3EWT/uU6Ro8VIxyMyivRlk7ghEEJHsGyH8RvNZi00eZkw4 +n10smbMQKlDxDXAsrGp2ez9KXso84xvx3NqZXc/uO5SQcIfT4haWSsNBVavtYXd1 +0eQ6KoJ8ptImgPFmPo0c1m4Hg5hdlY5FTGE8vp4Zj1J2sbfxqA0vsqPQ6mNOLsRh +AOtED9rDeh/prTMp3agbkF/Janwt5O69JgbH4iJMLc6PwYs3/MOlcceIlmHMt7dI +MFtbTN0PyBQfVN3D417/UCFcY8LoIQLfyquKay5tQSByxdqK9g+0bfeMXjDHIo8u +xNlTrxQwzgzoNqNMYk4vGEELvwRLPBxcXF1jaQoXGZiH6pVXg5jqqDNSOiqQr/WV +Ox466J47DxGJBfbRMQqfQoaLTYWTVyhZSSYrTi1nwvXd2VaC7YIQVRwy6M4gEAG6 +W3DWkO9d1stGBL+PJlAgQ77vsAkKjINolcQYfigV+4Wt+4DTl5lUIW8hxM/C+DmN +uI9e5QlUbt850qrTOuNW8BOXbaPmBL4R0iDV9kzK +-----END CERTIFICATE REQUEST----- diff --git a/cert/oidc_provider.key b/cert/oidc_provider.key new file mode 100644 index 0000000..c35a8f5 --- /dev/null +++ b/cert/oidc_provider.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQC/b59wjxHnl4cP +k4vjBeYxU41WIzVw9OZT8fKdgmqRgU1NakE0AYLR88+l/w3wWeoPpv7g6rRSI1uF +aydEgE/DJ+4n7eJT2Qe3CSQ4ida12IJHiEpNb/cXIXKZzwoUvFqbCt+zEvLGV9Wr +LgkBVpEcQjiDnCB0xo2GfwgLqRVCEduAKAP7m7aXoAf6ImlUvGkYPo6uaNrdoF/f +8duWTPk15vfjn4ON8u6HON9amy84ofNxJhthcYJLvuEsG9rOrx1AgoIKp/Ec+m+b +Bpd0lMcV5cOPhIdpV4jpCN/FQFxPc+DqQ9PKfomlhqshUZsN+/fMgDAjvVlHq7cv +j8RWdRuvBgHjZkRBPsRNf3zL1UMPrkMUCialAnxBwrowEFFh8xv0wxMrZM0eB8z8 +jpw1yx19RjQXUb6foVQsXZl0eCuzTDTVrjrN4OrN9AVgNu1onOuwMA4my31Krgqv +9on79oopv5VYQRe4KWirCcX7KgB4IHqxD1aSBkEtxO5U21YfsuZUABqc0+de/iVw +sYeKlcsO0lnf1NKr6dBqIHXCQ1OCVEjCkrgCv7r6S6/jMTcR1mWdauLsSb7JY+yw +LbSkIu1fXLZNI84EskDaGPcQT5EBb5jvu1zZCYNdW1iMVveFt+wjXsoJRaW4dHNk +u7UH1SNeK/2kMHsHwbderg8T6v4DcwIDAQABAoICAA6iZ9vg9AtyR/7m1qDKSKio +rHtTQbia4Ci2rEdiOudYrSIn50gkfW2zZ8JW1yfyl7QOnhlvl81Xqp1ubZgM/wv6 +N3iR9OVYCAD0D/LKhsFsBbmWL6fv0UHRasNbUnf3Vi3YDPXRkwGaoVjusf2KMpmo +bk2RV+HVc+g+Oc06ZcehOdh4NqW7Z5/7ueBjVQ4HQTl7PskSdvjOU8X45UJ+K+b0 ++ypJfXMSiS2JKXnxtxBrQQL7WMiANue3dds7XeTC+kd+MpbB8+q1Mmb1gAqHfRit +cd+8z+U7rdmytfiMTQI954nBGaW3OqqOuvJXHLVa97yIaCWzSenytJRMN0Qsu6rp +v7LDjTiMMfX2yGVhzx4epEwCs1AydIU2Rz+ldN7J0GOdoiajpN6+CmJfrlIUML7C +pRhLBzv4n7nj10UQRgDQa0LhkCwiAYv6NuRs/Oj3z1VqvsnDTXRA58WgVhzpW79o +AS3Pe0HiBo1bnbG+iPFTLp9OqkkHXiHznOx09VvkzP/cRRxvHhNakF1kizjb0B9D +kGUehj7ooeRcUdfxW/+vGzksqcaUKNr0LXykRIswd0BrSxvav81BLWa5Q+iLfhsf +RVAitgVZV/MVwGKgNlqZWWq/jvjkCnaato0pwnz5L9TrS4ROkpExzce2ho8OlfsB +sTBK9SPARortucRBFq5RAoIBAQDenMrJJ+xTt5+3mlNdaIf+wA+fhT+aQWKKnkdU +jmFrGLKIR9oGGfomSl+/pocZ1/vUsUqQumGqxACZg/CodPdU9x/Dg6weV4ho8Boe +4wuepAgbgrrNkM7EY8g7PHvJ/f2BUGPerkQhlblnbKwpqGAjRK6nf9zJTGq0w6hX +wHnf4xNsjdM0wbz0jIhCkaIff39Kb9sMvR03yzJhYDJeV3i19WbObtaDTHUx/OhN +L0vjU1+L8xvp12tSE7NghfCSLNqCuE8BMdpe8YBxbShGHcNT1P3yuTMIA0EcfsyS +ZGt+wD0KW1XfZzYyJVvgdkmPPDwCueZ0p7UGXWC3et4reXANAoIBAQDcJc+VCbof +JK/JZjE6NELrlN4/ITJsYiHPVcF0TPQhc0tS7J6j6Ui6G3narTzsMj6JVHUQWDId +ngsV4iqeMlXZzgvsZhs13ki5OyfZogYXGRhJeLGvRe2JLZJkjU8lqrCzj1zLahlR +R8jzpI6oUY9zC5K+AqrvIDrZcoOKIm4EbgNL3HbU4DfS1bdvcKzubVUXT99u5l87 +VJKwj+B3IJH+Pa27wUqXaiu6iFKWTRbwZEJxGcH46H3Ncfq5ONQYIMF8Qa0x7m4C +6vjj9p+MuLG09K/RVxS78C+aQbA33nw+2nHE7XGUstG/i/1lF9P7c5IvLpZhg3jh +dJqDXnTCVOF/AoIBACmHUoVIP1w2y0LzNU4drBEoP0HhF8ZtIDb/5AqwLRhPmS63 +SMMOoY33Hfmg1V9K3Z4GHQT24DV9Lyd0Z7blayacm35b2AYpCjeZYyYT8Rz9OM35 +C2RB+XDFtJroY6eBDIPNPXRTSj6Bsf7LVSrIUHcD3xk8TzH2YYmrXoJQU+wiboM3 +ygjzg46TkO/qPzZlEJgQWer8dLDt5U4pJfxXkRtQ1ob4QmhoIQzcQ3HyutODwDVZ +ewGawFileDcvhXufhnwQahys8jH4F6ARXwwFjWTcPSvExPJfuQvZ08wTk8InlruR +4mk01fVw5rzvFDX2ZLOVfpqQlsLDNMHF2CCAHRkCggEBAJfUYAEK71lxROdi2oqo +5opxZoIllBAZ8fV70Gs0c57qVEXBuXuUdbsdHgJKPl0sHeM64229VfsFN+IE1J28 +W2dW6vRES3nwbzmI6ef4Dbk6bnylh+45DSTx0CgXKUHyIeIE+tuztfhPyPD4Sgce +ERAoKDFxx5AaK1wy1TOcoUHe2dklGLOiW/3Ftbhe/nWF5Ayq4o8qxP03S+AHqqie +RvIQedSm75nT+IqyYDsWub5bd1Nnj1bqiXD6rg+2eNiXJrpa1Wc9aJQMmFEok0tL +SgxiQSvgogUoFryNl0pA4HG82IAXvqDuXyh7FOz27RVb1LNDryVbti/P6oy1xVMN +lWMCggEAEvT0AhjlbXrE9EKrfg27qMgL/gkwjCeDjnAIZXVCur8C+MBUTN/eRDZY +uDZU4ZWkE0Y04RPTNMlpVcBkHpXp2pn5pf0OlSWdoBVlI68SeV5Cr3kLO2kT+zf4 +wULf7DStzl2DJAjaa4+ORX/duo//+I0jIRUk1SFvkNryexz518QMHrSnYhXetdZZ +SRtY9qwB16JOqi4Kyer1X2DyE0Qr+i7rDw7XCeJ/YRA/M8JCk4rUi82Vjd2Kv6cV +RXkLGL2k2nEiK88Xxg+bYOp2Dwy5r7vxcRrbr6G9FkcU9UbxKRoK8dkdmEsjvlp5 +cXlsC3wuADNQnvoMNW1ZvKkhEubiwQ== +-----END PRIVATE KEY----- diff --git a/cert/oidc_provider.pem b/cert/oidc_provider.pem new file mode 100644 index 0000000..1ab4bb7 --- /dev/null +++ b/cert/oidc_provider.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFXjCCA0agAwIBAgIUdMon1qlq3LN/qjQdwTOXVPmUs0QwDQYJKoZIhvcNAQEL +BQAwWjELMAkGA1UEBhMCVVMxDzANBgNVBAgMBkRlbmlhbDEUMBIGA1UEBwwLU3By +aW5nZmllbGQxDDAKBgNVBAoMA0RpczEWMBQGA1UEAwwNb2lkY19wcm92aWRlcjAe +Fw0yNDEwMjExODE4MDBaFw0yNTEwMjExODE4MDBaMFoxCzAJBgNVBAYTAlVTMQ8w +DQYDVQQIDAZEZW5pYWwxFDASBgNVBAcMC1NwcmluZ2ZpZWxkMQwwCgYDVQQKDANE +aXMxFjAUBgNVBAMMDW9pZGNfcHJvdmlkZXIwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQC/b59wjxHnl4cPk4vjBeYxU41WIzVw9OZT8fKdgmqRgU1NakE0 +AYLR88+l/w3wWeoPpv7g6rRSI1uFaydEgE/DJ+4n7eJT2Qe3CSQ4ida12IJHiEpN +b/cXIXKZzwoUvFqbCt+zEvLGV9WrLgkBVpEcQjiDnCB0xo2GfwgLqRVCEduAKAP7 +m7aXoAf6ImlUvGkYPo6uaNrdoF/f8duWTPk15vfjn4ON8u6HON9amy84ofNxJhth +cYJLvuEsG9rOrx1AgoIKp/Ec+m+bBpd0lMcV5cOPhIdpV4jpCN/FQFxPc+DqQ9PK +fomlhqshUZsN+/fMgDAjvVlHq7cvj8RWdRuvBgHjZkRBPsRNf3zL1UMPrkMUCial +AnxBwrowEFFh8xv0wxMrZM0eB8z8jpw1yx19RjQXUb6foVQsXZl0eCuzTDTVrjrN +4OrN9AVgNu1onOuwMA4my31Krgqv9on79oopv5VYQRe4KWirCcX7KgB4IHqxD1aS +BkEtxO5U21YfsuZUABqc0+de/iVwsYeKlcsO0lnf1NKr6dBqIHXCQ1OCVEjCkrgC +v7r6S6/jMTcR1mWdauLsSb7JY+ywLbSkIu1fXLZNI84EskDaGPcQT5EBb5jvu1zZ +CYNdW1iMVveFt+wjXsoJRaW4dHNku7UH1SNeK/2kMHsHwbderg8T6v4DcwIDAQAB +oxwwGjAYBgNVHREEETAPgg1vaWRjX3Byb3ZpZGVyMA0GCSqGSIb3DQEBCwUAA4IC +AQCaQ4gL4hgiS8gLx4HOOonkPYwHY7VPK+6lElCiONEIfkgUacjn3TqV+iOiUY3l +JxBSEKFNiQc3Ci4XaS0hah2D0e49LlCRDJf2hMILmNF+D8dLF35Q6YLnppNnFOV0 +p77jUoPJTjmW7/beotwshR8TStVJLPm3Kq6vOcUicgcHtamBEwaAC0vzLsgTpWFE +/81ZCxfpomxHJ6akYTRd2um+e37XqVYokhMgl/9Tu5Pkw9/8+FSn/Jqt4dAz3Umh +vflygwPH1KvW3aexr4wb60Vw1qQyXaqu574GzdCjhQJgsNdhykCN4YdQvFnBxTB3 +kFk7k0sUWp1NUdZS9DWW0bbbjxnB3dhonqlrt/DnmB5FueE5Ugfad7f7jyZqZgCz +LbNlKuOPds1r729jnWSSVwCRMoFuplj7zJIHc1gz0LSw/XBy50Bjv3BaKM0j55R0 +LmzmaPOy2zXZ1dQJPuLv2U4lHGCGhJr6av5DbX2JFInYsv4tLeAXmovmNW9ALqe0 +CQhBxAJnYdCRDMyhEc7mwaxRSt/UTlcP7wQv9NY54Hsi0110dKwlrS57sWU7m+aL +JrpECjpKu8/gKzNhGlhZCq1MRyan0T8TXhxa7V/3fiI28QcUvDc9QdabmARGwvw1 +GF/pjzBjy0dhESHP9H8QgMqlv4FZX2YPe24GClLuBVeoQQ== +-----END CERTIFICATE----- diff --git a/config/application.rb b/config/application.rb index a4bd5f2..6e4d80c 100644 --- a/config/application.rb +++ b/config/application.rb @@ -40,7 +40,17 @@ class Application < Rails::Application Clients::Vault.token = Config[:vault_token] Clients::Vault.configure_kv Clients::Vault.configure_pki + get_oidc_config + issuer = OidcProvider.get_configured_issuer + Clients::Vault.configure_as_oidc_client(issuer, + config.astral.oidc_client_id, + config.astral.oidc_client_secret) + Clients::Vault.rotate_token end + + def get_oidc_config + config.astral.oidc_client_id, config.astral.oidc_client_secret = OidcProvider.new.get_client_info + end end end diff --git a/config/astral.yml b/config/astral.yml index 15eda38..adc2a9c 100644 --- a/config/astral.yml +++ b/config/astral.yml @@ -28,7 +28,29 @@ shared: # User activity logging audit_log_file: <%= "#{Rails.root.join('log')}/astral-audit.log" %> -# Per-environment overrides of shared configs + oidc_client_id: + oidc_client_secret: + oidc_redirect_uris: http://localhost:8250/oidc/callback + oidc_provider_cert_name: cert/oidc_provider + + # set this to "https://oidc_provider:9443" for tls: + oidc_provider_addr: http://oidc_provider:8300 + + # This should just contain the issuer path, not the host:port, which should be in the "oidc_provider_addr" above: + oidc_issuer_path: /v1/identity/oidc/provider/astral + +# if oidc_provider_addr is https with self-signed cert, need to provide + # CA cert (path to file) in "oidc_provider_ssl_cert" below: + oidc_provider_ssl_cert: cert/oidc_provider.pem + # oidc provider client cert if required (path to file) + oidc_provider_ssl_client_cert: + oidc_provider_ssl_client_key: + + + initial_user_name: test + initial_user_password: test + initial_user_email: test@example.com + test: cert_ttl: <%= 24.hours.in_seconds %> @@ -36,3 +58,13 @@ development: production: vault_create_root: false + + oidc_provider_addr: + + # This should just contain the issuer path, not the host:port, which should be in the "oidc_provider_addr" above: + oidc_issuer_path: + + initial_user_name: + initial_user_password: + initial_user_email: + diff --git a/config/environments/development.rb b/config/environments/development.rb index 98128ff..dddb726 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -1,4 +1,5 @@ require "active_support/core_ext/integer/time" +require_relative "../../app/lib/utils/oidc_provider" Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. diff --git a/config/environments/production.rb b/config/environments/production.rb index 4f5d1e6..11bb8fd 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -92,4 +92,7 @@ # ] # Skip DNS rebinding protection for the default health check endpoint. # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } + + def get_oidc_config + end end diff --git a/config/environments/test.rb b/config/environments/test.rb index 0c616a1..3dac6dc 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -1,4 +1,5 @@ require "active_support/core_ext/integer/time" +require_relative "../../app/lib/utils/oidc_provider" # The test environment is used exclusively to run your application's # test suite. You never need to work with it otherwise. Remember that diff --git a/lib/tasks/configure.rake b/lib/tasks/configure.rake index 80ad985..52fc1ad 100644 --- a/lib/tasks/configure.rake +++ b/lib/tasks/configure.rake @@ -3,15 +3,21 @@ require "rake" # Rake tasks for making a vault cert namespace :configure do desc "Make the server cert for vault" - task :ssl do + task :ssl, [ :cert_name ] do |t, args| + cert_name = args[:cert_name] + cert_name = "vault" if cert_name.nil? + sanParam = "subjectAltName=DNS:#{cert_name}" %x( - openssl req -new -newkey rsa:4096 -nodes \ - -keyout cert/vault.key -out cert/vault.csr \ - -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=vault" - openssl x509 -req -days 365 -in cert/vault.csr \ - -signkey cert/vault.key \ - -out cert/vault.pem + openssl req -new -newkey rsa:4096 -nodes \ + -keyout cert/#{cert_name}.key -out cert/#{cert_name}.csr \ + -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=#{cert_name}" \ + -addext #{sanParam} \ + + echo #{sanParam} > /tmp/sanParam + openssl x509 -req -days 365 -in cert/#{cert_name}.csr \ + -signkey cert/#{cert_name}.key \ + -out cert/#{cert_name}.pem -extfile /tmp/sanParam ) - puts "SSL key for vault created" + puts "SSL key for #{cert_name} created" end end diff --git a/lib/tasks/oidc_provider.rake b/lib/tasks/oidc_provider.rake new file mode 100644 index 0000000..ce2491d --- /dev/null +++ b/lib/tasks/oidc_provider.rake @@ -0,0 +1,12 @@ +require "rake" +require_relative "../../app/lib/utils/oidc_provider" +require_relative "../../app/lib/config" + +# Rake tasks for oidc provider +namespace :oidc_provider do + desc "Configure the provider" + task :configure do + OidcProvider.new.configure + puts "oidc provider configured" + end +end diff --git a/test/lib/clients/oidc_provider_test.rb b/test/lib/clients/oidc_provider_test.rb new file mode 100644 index 0000000..518a65d --- /dev/null +++ b/test/lib/clients/oidc_provider_test.rb @@ -0,0 +1,23 @@ +require "test_helper" + +class OidcProviderTest < ActiveSupport::TestCase + setup do + @provider = OidcProvider.new + @info = @provider.get_info + end + + test ".get_info returns correct info" do + assert_equal "email", @info.data[:scopes_supported][0] + end + + test "#get_configured_issuer returns correct issuer" do + issuer = OidcProvider.get_configured_issuer + assert_equal @info.data[:issuer], issuer + end + + test ".get_client_info return correct info" do + info = @provider.get_client_info + assert_equal Config[:oidc_client_id], info[0] + assert_equal Config[:oidc_client_secret], info[1] + end +end diff --git a/test/lib/clients/oidc_test.rb b/test/lib/clients/oidc_test.rb new file mode 100644 index 0000000..73ce009 --- /dev/null +++ b/test/lib/clients/oidc_test.rb @@ -0,0 +1,39 @@ +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 + @client.configure_oidc_user( + Config[:initial_user_name], + Config[:initial_user_email], + test_policy) + @entity = @client.read_entity(Config[:initial_user_name]) + end + + test "policies contain initial users email" do + assert_equal Config[:initial_user_email], @entity.data[:policies][0] + end + + test "aliases contain initial users email" do + aliases = @entity.data[:aliases] + assert aliases.find { |a| a[:name] == Config[:initial_user_email] } + end + + test "vault is configured as oidc client" do + auth = @client.get_oidc_client_config + assert_equal Config[:oidc_client_id], auth.data[:oidc_client_id] + end + + private + def test_policy + policy = <<-EOH + path "sys" { + policy = "read" + } + EOH + end +end