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

feat: Add support for retrieving ClientId, ClientSecret and SSO secret from env var as opposed to creating a kube Secret #11777

Closed
wants to merge 1 commit into from

Conversation

ebourgeois
Copy link

@ebourgeois ebourgeois commented Sep 8, 2023

Fixes #10395
Fixes #11772

Motivation

This change adds support for retrieving ClientId, ClientSecret and SSO secret from env var as opposed to creating a kube Secret, allowing us to provide our own and not use Kube Secrets at all.

Modifications

The changes made were:

In the SSO auth code and config, to solve the above problem statement, we believe the following
would be required:

  • Allow setting clientId from environment variable
  • Allow setting clientSecret from environment variable
  • Allow setting of PKCS1 SSO secret from environment variable

Verification

We successfully:

  1. backed up the sso secret
  2. deleted the sso resource in our Kube namespace.
  3. created 3 environment variables from our secret store:
    1. ARGO_CLIENT_ID: our SSO client ID
    2. ARGO_CLIENT_SECRET: our client secret
    3. ARGO_PRIVATE_KEY: PKCS1 key, encoded in base64
  4. Added the the new mapping into the sso configmap:
      privateKeyEnvName: ARGO_PRIVATE_KEY
      clientIdEnvName: ARGO_CLIENT_ID
      clientSecretEnvName: ARGO_CLIENT_SECRET
  1. started workflow server successfully
  2. able to login with our SSO and use our custom groups

Regarding fix #10395:

In this PR, we are fixing issue: #10395
- this issue was caused by the creation of a new httpClient and not
re-using the existing one in sso.go, where InsecureSkipVerify
is being set and not propagated tot the groups claim call

Copy link
Member

@agilgur5 agilgur5 left a comment

Choose a reason for hiding this comment

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

Just a quick first pass with some small feedback:

  • Lint and DCO are failing.

    • See in-line comment re: lint.
    • All commits must have DCO (only one of the three current has it), so you'll need to force push on top with and amendment w/ DCO
  • Would be good to have some docs around this

    • the example configmap in the docs would be a good first start
    • I think you could also add a subsection here in the SSO docs describing this

config/sso.go Outdated Show resolved Hide resolved
config/sso.go Outdated Show resolved Hide resolved
@agilgur5
Copy link
Member

Fixes #10395

If these are to be part of the same PR, could you add a description about that to this PR? It's only mentioned on this one line.
I did see that you did some root cause analysis in #10395 (comment)

@agilgur5
Copy link
Member

agilgur5 commented Sep 27, 2023

Hmm, the groups change might be better as a separate PR since it's a fix while the secrets are a feature. When together, the fix is not cherry-pickable.

If I'm reading correctly, the groups change is isolated to claims while the secrets feature is in sso, so there's no overlap? EDIT: nvm, I see the groups changes in sso as well

@ebourgeois ebourgeois closed this Sep 27, 2023
@ebourgeois ebourgeois reopened this Sep 27, 2023
@ebourgeois ebourgeois force-pushed the feat-support-sso-via-env branch 2 times, most recently from 8e23376 to fc3f210 Compare September 28, 2023 02:35
@ebourgeois ebourgeois changed the title feat: Add support for retrieving ClientId, ClientSecret and SSO secret from env var as opposed to creating a kube Secret, allowing us to provide our own and not use Kube Secrets at all. feat: Add support for retrieving ClientId, ClientSecret and SSO secret from env var as opposed to creating a kube Secret Sep 28, 2023
…t from env var as opposed to creating a kube Secret

This will allow us to provide our own SSO private key and not use Kube Secrets at all.

Also, in this PR, we are fixing issue: argoproj#10395
    - this issue was caused by the creation of a new httpClient and not
      re-using the existing one in `sso.go`, where `InsecureSkipVerify`
      is being set and not propagated tot the groups claim call

Signed-off-by: Erick Bourgeois <erick.bourgeois@rbccm.com>
Copy link
Member

@agilgur5 agilgur5 left a comment

Choose a reason for hiding this comment

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

Several more docs changes below. Will leave a separate review for the code

Comment on lines +398 to +400
# This is the environment variable name that will contain the Client ID; this is equivalent to
# what is in the secret `argo-workflows-sso.client-id`. If this is not provided, then we
# default to using kube Secret above.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
# This is the environment variable name that will contain the Client ID; this is equivalent to
# what is in the secret `argo-workflows-sso.client-id`. If this is not provided, then we
# default to using kube Secret above.
# Environment variable that contains the OIDC client ID. Alternative to the Secret clientId above.
  • use simple and direct language, per k8s style guide

  • use present tense, per k8s style guide

  • be specific that this is an alternative

  • less duplication and consistent references -- "secret", "Secret above", "kube Secret" are all referring to the same thing, use "Secret" instead, per k8s style guide

    • consistently use "OIDC client id" to match above text
  • there is no argo-workflows-sso.client-id. sso.clientId is valid, but we can shorten that to just clientId as it's in the same section and is just a few lines above

    • use clientId without backticks to match existing text

@@ -395,6 +395,20 @@ data:
# be in the form <argo-server-root-url>/oauth2/callback. It must be
# browser-accessible. If omitted, will be automatically generated.
redirectUrl: https://argo-server/oauth2/callback
Copy link
Member

Choose a reason for hiding this comment

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

let's move redirectURL to be below these, so that all related configurations are next to each other

Comment on lines +403 to +406
# This is the environment variable name that contains OIDC client
# secret issued to the application by the provider; this is equivalent to
# what is in the secret `argo-workflows-sso.client-secret`. If this is not provided, then we
# default to using kube Secret above.
Copy link
Member

@agilgur5 agilgur5 Oct 12, 2023

Choose a reason for hiding this comment

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

Suggested change
# This is the environment variable name that contains OIDC client
# secret issued to the application by the provider; this is equivalent to
# what is in the secret `argo-workflows-sso.client-secret`. If this is not provided, then we
# default to using kube Secret above.
# Environment variable that contains the OIDC client secret. Alternative to the Secret clientSecret above.

same as above

Comment on lines +409 to +410
# This is the environment variable name that contains PKCS1 private key, if this is not
# provided, then Argo will create one dynamically and store in Kube Secret.
Copy link
Member

@agilgur5 agilgur5 Oct 12, 2023

Choose a reason for hiding this comment

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

Suggested change
# This is the environment variable name that contains PKCS1 private key, if this is not
# provided, then Argo will create one dynamically and store in Kube Secret.
# Environment variable that contains the PKCS1 private key. If not provided,
# Argo will create one dynamically and store it in a Secret.
  • similar to above
  • fix run-on sentence

@@ -395,6 +395,20 @@ data:
# be in the form <argo-server-root-url>/oauth2/callback. It must be
Copy link
Member

Choose a reason for hiding this comment

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

GH won't let me comment on unchanged lines above, but clientId and clientSecret should be changed to "(required if clientIdEnvName is not provided)" etc instead of just "(required)"

@@ -107,6 +107,44 @@ data:
redirectUrl: https://argo-workflows.mydomain.com/oauth2/callback
```

## Example SSO config with Secrets from environment variable
Copy link
Member

Choose a reason for hiding this comment

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

this is the ArgoCD Dex integration page, not the general SSO page. this configuration is general to all SSO, so it should be in the general SSO page.

In my previous review comment, I suggested putting it on that page in a specific subsection that has instructions for creating Secrets.

Comment on lines +122 to +124
# SSO Configuration for the Argo server.
# You must also start argo server with `--auth-mode sso`.
# https://argoproj.github.io/argo-workflows/argo-server-auth-mode/
Copy link
Member

@agilgur5 agilgur5 Oct 12, 2023

Choose a reason for hiding this comment

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

Instead of specifying how to start it, you can add a sample manifest for the Argo Server Deployment with this flag. That is what is used in other examples on this page as well (and is why there is a --- in them to split the YAML manifests).
The Deployment would also be useful to specify the environment variables that are provided in the ConfigMap.

I thought I suggested adding a Deployment previously, but couldn't find my comment

Comment on lines +112 to +113
The following example shows how you can use environment variables to get your secrets. This is
especially useful if you use CSI driver to get your secrets from, say, Azure Key Vault.
Copy link
Member

Choose a reason for hiding this comment

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

Are you referencing the Secrets Store CSI Driver? That does require that secrets are created in k8s first before you can use env vars. It's a CSI Driver, so the default is to mount the secret, not set an env variable

Also in general, should be specific with what is being referenced in docs and avoid ambiguity

Copy link
Member

@agilgur5 agilgur5 left a comment

Choose a reason for hiding this comment

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

I mentioned before, but I do think it would be good to split up the fix from the feature. That way we can cherry-pick the fix into 3.4.12 (#11851). I think that bug may have been hit many times in different issues in particular

Copy link
Member

@agilgur5 agilgur5 left a comment

Choose a reason for hiding this comment

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

Some comments on the tests below

})
}

func TestGetUserInfoGroup(t *testing.T) {
Copy link
Member

Choose a reason for hiding this comment

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

Am I missing something or this test identical to the one above?

Copy link
Member

@agilgur5 agilgur5 Apr 26, 2024

Choose a reason for hiding this comment

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

Looks like this one passes "group" in, I assume as part of the third fix/feature. Could probably be done within the same suite and should be named differently etc

Comment on lines +165 to +166
os.Setenv("ARGO_CLIENT_ID", "sso-client-id-value")
os.Setenv("ARGO_CLIENT_SECRET", "sso-client-secret-value")
Copy link
Member

Choose a reason for hiding this comment

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

you'll want to unset these at the end of the test, which is done in most other tests

Copy link
Member

@agilgur5 agilgur5 Apr 26, 2024

Choose a reason for hiding this comment

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

Suggested change
os.Setenv("ARGO_CLIENT_ID", "sso-client-id-value")
os.Setenv("ARGO_CLIENT_SECRET", "sso-client-secret-value")
t.Setenv("ARGO_CLIENT_ID", "sso-client-id-value")
t.Setenv("ARGO_CLIENT_SECRET", "sso-client-secret-value")

there's a helper for that, t.Setenv

privateKeyBlobEncoded := base64.StdEncoding.EncodeToString(privateKeyBlob)
assert.NotEmpty(t, privateKeyBlobEncoded)

os.Setenv("ARGO_PRIVATE_KEY", privateKeyBlobEncoded)
Copy link
Member

@agilgur5 agilgur5 Oct 12, 2023

Choose a reason for hiding this comment

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

same as above, unset this at the end

privateKeyBlob := x509.MarshalPKCS1PrivateKey(generatedKey)
assert.NotEmpty(t, privateKeyBlob)

privateKeyBlobEncoded := base64.StdEncoding.EncodeToString(privateKeyBlob)
Copy link
Member

Choose a reason for hiding this comment

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

the encoding should be specified in the docs as well

Comment on lines +201 to +206
ctx := context.Background()
_, err = fakeClient.Create(ctx, &apiv1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: secretName},
Data: map[string][]byte{},
}, metav1.CreateOptions{})
assert.NoError(t, err)
Copy link
Member

@agilgur5 agilgur5 Oct 12, 2023

Choose a reason for hiding this comment

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

hmm, creating an empty PKCS1 secret that would fail normally relies on a good bit of indirection. that's confusing and is also not specified in a comment here either.

instead, we should be direct that no PKCS1 secret is created by asserting that a get finds no secret at the end

Copy link
Member

@agilgur5 agilgur5 left a comment

Choose a reason for hiding this comment

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

Several comments on the source below.

The core env variable retrieval is mostly good, though there is one performance regression in the code (see below).

The GetUserInfoGroups changes require some iteration, there are some anti-patterns being used there. And I also did not expect that there was a third feature/fix to be included there/in this PR, a custom group claim on top of a custom user info path.


func getSsoPrivateKey(ctx context.Context, c Config, secretsIf corev1.SecretInterface) (*rsa.PrivateKey, error) {
privateKeyBlobEncoded := os.Getenv(c.PrivateKeyEnvName)
log.Debug("privateKeyBlobEncoded: ", privateKeyBlobEncoded)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
log.Debug("privateKeyBlobEncoded: ", privateKeyBlobEncoded)

this is a secret, so we should never log it out, even in encoded form


privateKey, err := getSsoPrivateKey(ctx, c, secretsIf)
if err != nil {
return nil, fmt.Errorf("failed to parse private key. If you have already defined a Secret named %s, delete it and retry: %w", secretName, err)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
return nil, fmt.Errorf("failed to parse private key. If you have already defined a Secret named %s, delete it and retry: %w", secretName, err)
return nil, err

the formatted error already exists in the getSsoPrivateKey function. we shouldn't override it, especially as the other errors are more specific

ctx := context.Background()
clientSecretObj, err := secretsIf.Get(ctx, c.ClientSecret.Name, metav1.GetOptions{})

// Fallback to getting values from Kube Secrets
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// Fallback to getting values from Kube Secrets
// Fallback to getting value from Secret
  • since there is no integration with any specific providers, k8s Secrets are the default (and are native), no need to specify that.
  • it's a singular value and singular secret in this case

clientSecretObj, err := secretsIf.Get(ctx, c.ClientSecret.Name, metav1.GetOptions{})

// Fallback to getting values from Kube Secrets
secretObj, err := secretsIf.Get(ctx, c.ClientID.Name, metav1.GetOptions{})
Copy link
Member

Choose a reason for hiding this comment

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

it looks like you're missing the logic that was previously on lines 126-127 -- if the clientId Secret is the same name as the clientSecret secret, it just uses that existing Secret.

that logic would only make a single network request

return "", fmt.Errorf("clientSecret empty")
}

// Fallback to getting values from Kube Secrets
Copy link
Member

@agilgur5 agilgur5 Oct 12, 2023

Choose a reason for hiding this comment

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

Suggested change
// Fallback to getting values from Kube Secrets
// Fallback to getting value from Secret

same as above

server/auth/types/claims.go Show resolved Hide resolved
Comment on lines +128 to +129
// If a custom group key was given, use that
if customKeyName != "" {
Copy link
Member

Choose a reason for hiding this comment

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

wait a minute, this is a third fix/feature entirely? this is not mentioned in the PR either...
this PR is definitely addressing a little too many concerns simultaneously... single-purpose PRs are best; they are easy to review, easy to revert, easy to trace. 3 purposes in one PR is a bit much...

as a leading indicator, 3 purposes do not fit in a PR title (not even 2 fit)

Copy link
Member

Choose a reason for hiding this comment

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

Also your provider returns the groups with a different key even when it has a separate URL to retrieve them?


err = json.Unmarshal(responseBody, &userInfoRawMap)
if err != nil {
return nil, fmt.Errorf("Could not marshall userInfo payload")
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
return nil, fmt.Errorf("Could not marshall userInfo payload")
return nil, fmt.Errorf("Could not unmarshal userInfo payload")

typo?


userInfo := UserInfo{}
err = json.Unmarshal(responseBody, &userInfo)
Copy link
Member

Choose a reason for hiding this comment

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

correct me if I'm wrong, but if the Groups field is empty, I believe this would fail. there's no omitempty in the struct.

Comment on lines +134 to +138
var userInfoRawMap map[string]json.RawMessage

err = json.Unmarshal(responseBody, &userInfoRawMap)
if err != nil {
return nil, fmt.Errorf("Could not marshall userInfo payload")
Copy link
Member

Choose a reason for hiding this comment

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

if we change the typings a bit, we can consolidate this by combining it with the top block, which I think would be a lot neater than doing 2 separate unmarshals.

then the conditional logic is just grabbing the custom claim

@agilgur5 agilgur5 added the problem/more information needed Not enough information has been provide to diagnose this issue. label Nov 12, 2023
Copy link
Contributor

This PR has been automatically marked as stale because it has not had recent activity and needs further changes. It will be closed if no further activity occurs.

@github-actions github-actions bot added the problem/stale This has not had a response in some time label Jan 11, 2024
@agilgur5 agilgur5 self-assigned this Jan 17, 2024
@github-actions github-actions bot removed problem/stale This has not had a response in some time problem/more information needed Not enough information has been provide to diagnose this issue. labels Jan 18, 2024
Copy link
Member

@agilgur5 agilgur5 left a comment

Choose a reason for hiding this comment

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

The fix for #10395, UserInfoGroups not having the httpClient, was properly completed in #12982

server/auth/sso/sso.go Show resolved Hide resolved
server/auth/types/claims.go Show resolved Hide resolved
server/auth/types/claims.go Show resolved Hide resolved
})
}

func TestGetUserInfoGroup(t *testing.T) {
Copy link
Member

@agilgur5 agilgur5 Apr 26, 2024

Choose a reason for hiding this comment

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

Looks like this one passes "group" in, I assume as part of the third fix/feature. Could probably be done within the same suite and should be named differently etc

Comment on lines +165 to +166
os.Setenv("ARGO_CLIENT_ID", "sso-client-id-value")
os.Setenv("ARGO_CLIENT_SECRET", "sso-client-secret-value")
Copy link
Member

@agilgur5 agilgur5 Apr 26, 2024

Choose a reason for hiding this comment

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

Suggested change
os.Setenv("ARGO_CLIENT_ID", "sso-client-id-value")
os.Setenv("ARGO_CLIENT_SECRET", "sso-client-secret-value")
t.Setenv("ARGO_CLIENT_ID", "sso-client-id-value")
t.Setenv("ARGO_CLIENT_SECRET", "sso-client-secret-value")

there's a helper for that, t.Setenv

@agilgur5 agilgur5 added the problem/more information needed Not enough information has been provide to diagnose this issue. label May 1, 2024
Copy link
Contributor

This PR has been automatically marked as stale because it has not had recent activity and needs further changes. It will be closed if no further activity occurs.

@github-actions github-actions bot added the problem/stale This has not had a response in some time label May 16, 2024
Copy link
Contributor

This PR has been closed due to inactivity and lack of changes. If you would like to still work on this PR, please address the review comments and re-open.

@github-actions github-actions bot closed this May 30, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/sso-rbac problem/more information needed Not enough information has been provide to diagnose this issue. problem/stale This has not had a response in some time
Projects
None yet
3 participants