-
Notifications
You must be signed in to change notification settings - Fork 3.1k
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
Conversation
There was a problem hiding this 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
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. |
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 |
4035b65
to
dcb8b6f
Compare
8e23376
to
fc3f210
Compare
…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>
fc3f210
to
1593baa
Compare
There was a problem hiding this 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
# 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
# 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 justclientId
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 |
There was a problem hiding this comment.
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
# 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
# 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
# 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
# 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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
# 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/ |
There was a problem hiding this comment.
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
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. |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this 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) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
os.Setenv("ARGO_CLIENT_ID", "sso-client-id-value") | ||
os.Setenv("ARGO_CLIENT_SECRET", "sso-client-secret-value") |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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
ctx := context.Background() | ||
_, err = fakeClient.Create(ctx, &apiv1.Secret{ | ||
ObjectMeta: metav1.ObjectMeta{Name: secretName}, | ||
Data: map[string][]byte{}, | ||
}, metav1.CreateOptions{}) | ||
assert.NoError(t, err) |
There was a problem hiding this comment.
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
There was a problem hiding this 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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// 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{}) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// Fallback to getting values from Kube Secrets | |
// Fallback to getting value from Secret |
same as above
// If a custom group key was given, use that | ||
if customKeyName != "" { |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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) |
There was a problem hiding this comment.
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.
var userInfoRawMap map[string]json.RawMessage | ||
|
||
err = json.Unmarshal(responseBody, &userInfoRawMap) | ||
if err != nil { | ||
return nil, fmt.Errorf("Could not marshall userInfo payload") |
There was a problem hiding this comment.
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
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
}) | ||
} | ||
|
||
func TestGetUserInfoGroup(t *testing.T) { |
There was a problem hiding this comment.
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
os.Setenv("ARGO_CLIENT_ID", "sso-client-id-value") | ||
os.Setenv("ARGO_CLIENT_SECRET", "sso-client-secret-value") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
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. |
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. |
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:
Verification
We successfully:
sso
secretsso
resource in our Kube namespace.ARGO_CLIENT_ID
: our SSO client IDARGO_CLIENT_SECRET
: our client secretARGO_PRIVATE_KEY
: PKCS1 key, encoded in base64sso
configmap: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
, whereInsecureSkipVerify
is being set and not propagated tot the groups claim call