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
Bug 1503289 - Move registry credentials to a secret #502
Conversation
|
This doesn't seem to allow me to specify multiple dockerhub credentials. With our current configuration I can set the broker to talk to dockerhub for |
|
@jmrodri initially I had the credentials in the auth file stored by name i.e.: And then was storing the user/pass based on the name. There is a good reason why I didn't continue down that approach because working with secrets around independent registries just didn't seem like a scalable way to continue. Since we are working to remove the credentials completely I think it makes sense to consolidate the credentials based on registry type. This way you can declare rhcc creds (not needed currently), isv registry creds (will become important), without redeclaring for every different adapter. |
|
@jmrodri Is it a valid use case to want to use two different credentials for two dockerhub adapters? It currently doesn't matter which credentials you use as they are just used to obtain a bearer token that allows us to search the registries index. One credential would work for any dockerhub org. The only scenario I could see that not being true would be private organizations but it would just require the most elevated credentials. |
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.
I don't like the extra "config" approach.
build/entrypoint.sh
Outdated
| pass: ${DOCKERHUB_PASS} | ||
| EOF | ||
|
|
||
| asbd -c $BROKER_CONFIG --registryauth=$REGISTRY_AUTH $FLAGS |
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.
I prefer NOT to have this as a separate option. Why can't the admin specify the location of the secret in the registry config section?
Something like auth: TYPE|NAME where TYPE can be secret or file. Then NAME can be the name of the secret OR the fully qualified filename: /etc/ansible-service-broker/dockerhub-auth
registry:
- type: dockerhub
name: docker
url: https://registry.hub.docker.com
auth: TYPE|NAME
org: DOCKERHUB_ORG
fail_on_error: false
- type: rhcc
name: rhcc
url: registry.access.stage.redhat.com
fail_on_error: true
white_list:
- "^legitimate.*-apb$"
# will allow all the APBs to be included. You must have at least 1 white
# list to retrieve APBs and this is the most permissive
- ".*-apb$"
black_list:
- "malicious.*-apb$"
- "^specific-blacklist-apb$"
dao:OR we could use
auth_type: secret|file
auth_name: NAME_OF_SECRET | FQFNThis gives us the flexibility to know how to read different ways. File makes it easier to run locally. But secret might make it easier in OpenShift by not needing a mount. Just go read the 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.
OR we could reuse the auth format I did for basic auth and you might even be able to use the same structs (maybe not entire sure).
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.
+1, you're right no reason to make this a separate config. I agree with you. I think I pidgeonholed myself there because I was initially mounting the secret as a volume.
etc/example-registry-auth.yaml
Outdated
| --- | ||
| - type: dockerhub | ||
| user: DOCKERHUB_USER | ||
| pass: DOCKERHUB_PASS |
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 looks like it supports multiples by being an array but how do I know which registry this is for?
pkg/app/app.go
Outdated
| @@ -150,7 +150,7 @@ func CreateApp() App { | |||
|
|
|||
| // TODO: Let's take all these validations and delegate them to the client | |||
| // pkg. | |||
| if app.config, err = CreateConfig(app.args.ConfigFile); err != nil { | |||
| if app.config, err = CreateConfig(app.args.ConfigFile, app.args.RegistryAuthFile); err != nil { | |||
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.
Like I mentioned above, I would prefer specifying the auth stuff as a pointer to some other location still in the main registry config.
| username: ${DOCKERHUB_USER} | ||
| password: ${DOCKERHUB_PASS} | ||
|
|
||
| - apiVersion: v1 |
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 secret is still ok, but instead of passing it as a parameter on the cli to the broker, it should be in the registry section of the config.
|
We could support file in the broker as an initial cut, then just create that file from a secret. But it would be nice to read a secret directly. |
|
Thanks for the feedback, I'm pretty much completely refactoring this to fit Jesus suggestions. |
etc/generated_registry_auth.yaml
Outdated
| --- | ||
| - type: dockerhub | ||
| user: dymurray | ||
| pass: qci-test |
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.
Probably want to ignore the generated file
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.
Oops, this shouldn't be there.
pkg/app/config.go
Outdated
| config.Registry[regCount].Pass = regCreds.Password | ||
|
|
||
| } else { | ||
| // ERROR |
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.
Should we still allow the user to specify the user/pass in the config and use them here? This way we don't break old brokers and the real motivation for moving to a secret was for when we are running in openshift.
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.
I also wonder if we support environment variables because that is one way to use secrets?
should be not super hard?
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.
@shawn-hurley that was how I previously implemented this before the refactoring :). That worked fine but required entrypoint changes. Jesus' brought up some good feedback that it would be cleaner to read the secret from the broker itself. We also control the template so its arbitrary which one we pick.
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.
@dymurray If you are saying allow the user to use the old configuration values if they want, that's ok with me. If someone chooses to put a password in a config it is up to them.
|
make lint failed: |
|
I also saw you updated |
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
build/entrypoint.sh
Outdated
| @@ -1,5 +1,4 @@ | |||
| #!/usr/bin/env bash | |||
|
|
|||
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.
Remove this change from this patch. It just adds noise and isn't necessary.
pkg/app/config.go
Outdated
| config.Registry[regCount].Pass = regCreds.Password | ||
|
|
||
| } else { | ||
| // ERROR |
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.
@dymurray If you are saying allow the user to use the old configuration values if they want, that's ok with me. If someone chooses to put a password in a config it is up to them.
pkg/app/config.go
Outdated
| @@ -103,3 +147,24 @@ func validateConfig(c Config) error { | |||
| } | |||
| return nil | |||
| } | |||
|
|
|||
| // Returns the data inside of a given secret | |||
| func getSecretData(secretName, namespace string) (map[string][]byte, error) { | |||
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.
I thought we already had a method to get secrets. I seem to recall a secrets.go that @fabianvf added in a previous patch. Seems that would be useful. If not this seems useful in general to me.
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.
Yeah we did have a method but it was being used to get secrets associated with a specific APB. It also required me to initialize a secrets cache in order to use those methods. I'm not against making this globally available and putting it in a utils package.
scripts/prep_local_devel_env.sh
Outdated
| @@ -58,7 +58,8 @@ fi | |||
|
|
|||
|
|
|||
| # Determine the name of the secret which has the 'asb' service account info | |||
| BROKER_SVC_ACCT_SECRET_NAME=`kubectl get serviceaccount asb -n ansible-service-broker -o jsonpath='{.secrets[0].name}'` | |||
| BROKER_SVC_ACCT_SECRET_NAME=`kubectl get serviceaccount asb -n ansible-service-broker -o yaml | grep -Po asb-token-[a-z0-9]*` | |||
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.
why is this being changed in this PR?
| user: "${DOCKERHUB_USER}" | ||
| pass: "${DOCKERHUB_PASS}" | ||
| auth_type: "${REGISTRY_AUTH_TYPE}" | ||
| auth_name: "${REGISTRY_SECRET_NAME}" |
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.
+1 I like it.
pkg/app/config.go
Outdated
| "io/ioutil" | ||
| meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
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.
I find it weird to have this imported into the config. Seems that the thing that uses this should be in a util package or part of the clients package directly. @shawn-hurley @eriknelson thoughts?
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 would be the getSecretData method so I'm fine with moving that out to a util package.
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.
| var err error | ||
| var req *http.Request | ||
| if r.Config.User == "" { | ||
| r.Log.Debugf("hereeee") |
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.
Probably should remove this debug statement, my bad @dymurray.
|
@shawn-hurley The secret data is base64 encoded. Right now I'm expecting the user to set the user/pass creds already base64 encoded much like we already do when passing in broker user/pass. |
pkg/app/config.go
Outdated
| if dat, err = ioutil.ReadFile(reg.AuthName); err != nil { | ||
| return Config{}, err | ||
| } | ||
| if err = yaml.Unmarshal(dat, ®Cred); err != nil { |
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 logging above here is really good. We could improve the logging here by adding a message about what failed. Could be something like 'missing or invalid username/password'.
pkg/registries/registry.go
Outdated
| Name string | ||
| Images []string | ||
| URL string | ||
| AuthType string `yaml:"auth_type"` |
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.
Can you put a comment about exactly what AuthType and AuthName are.
| - description: Dockerhub user password | ||
| displayname: Dockerhub user password | ||
| name: DOCKERHUB_PASS | ||
| value: Y2hhbmdlbWUK |
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.
Is this a real docker user and pw?
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 "changeme/changeme" base64 encoded so I figured it was safe. I can make it an empty string too.
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.
@dymurray now that we have the ability to do empty username/password I don't see why we wouldn't do that?
| @@ -189,6 +189,15 @@ objects: | |||
| password: ${BROKER_PASS} | |||
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.
Do we need any changes in deploy-local-dev-changes.yaml for the local broker?
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.
I don't believe so because that template doesn't include the broker config.
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.
I don't believe we should go through the trouble of creating an empty secret for the user every time since it won't be used unless someone is going through the trouble of configuring a private registry. In that case they can configure it themselves.
|
@rthallisey Thanks updated! |
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 more enhancements. Overall I like this solution better.
pkg/app/config.go
Outdated
| "io/ioutil" | ||
| "os" | ||
|
|
||
| yaml "gopkg.in/yaml.v2" |
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.
goimports seems to move this back, why was yaml moved above io/ioutil? I'd move it back since it doesn't add any value to moving it.
pkg/app/config.go
Outdated
| @@ -74,6 +82,34 @@ func CreateConfig(configFile string) (Config, error) { | |||
| config.Openshift.Namespace = string(dat) | |||
| } | |||
|
|
|||
| for regCount, reg := range config.Registry { | |||
| if reg.AuthType == "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.
I'd go with a switch statement, then we can add other types as we want.
for regCount, reg := range config.Registry {
var username, password string
var err error
switch reg.AuthType {
case "secret":
username, password, err = readSecret(reg.AuthName, config.Openshift.Namespace)
if err != nil {
return Config{}, err
}
case "file":
username, password, err = readFile(reg.AuthName)
if err != nil {
return Config{}, err
}
default:
return Config{}, fmt.Errorf("Invalid authType: %s", reg.AuthType)
}
config.Registry[regCount].User = username
config.Registry[regCount].Pass = password
}The supporting methods:
func readSecret(authname string, namespace string) (string, string, error) {
data, err := clients.GetSecretData(authname, namespace)
if err != nil {
return "", "", fmt.Errorf("Failed to find Dockerhub credentials in secret: %s", authname)
}
var username = strings.TrimSpace(string(data["username"]))
var password = strings.TrimSpace(string(data["password"]))
if username == "" || password == "" {
return username, password, fmt.Errorf("Secret: %s did not contain username/password credentials", authname)
}
return username, password, nil
}
func readFile(authname string) (string, string, error) {
regCred := regCreds{}
data, err := ioutil.ReadFile(authname)
if err != nil {
return "", "", fmt.Errorf("Failed to read registry credentials from file: %s", authname)
}
if err = yaml.Unmarshal(data, ®Cred); err != nil {
return "", "", fmt.Errorf("Failed to unmarshal registry credentials from file: %s", authname)
}
return regCred.Username, regCred.Password, nil
}
pkg/app/config_test.go
Outdated
| @@ -44,6 +44,8 @@ func TestCreateConfig(t *testing.T) { | |||
| "mismatch whitelist entry") | |||
| ft.AssertEqual(t, config.Registry[1].BlackList[1], "^specific-blacklist-apb$", | |||
| "mismatch blacklist entry") | |||
| ft.AssertEqual(t, config.Registry[2].User, "", "registry user not empty") | |||
| ft.AssertEqual(t, config.Registry[2].Pass, "", "registry pass not empty") | |||
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.
👍
| @@ -3,12 +3,14 @@ registry: | |||
| - type: dockerhub | |||
| name: docker | |||
| url: https://registry.hub.docker.com | |||
| auth_type: config | |||
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.
is config a valid auth_type? If so, then my switch suggestion needs another case for config.
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.
Yes it is, to simply declare user/pass in the config. I have updated the test cases and switch statement to reflect that.
pkg/app/testdata/test-config.yaml
Outdated
| @@ -16,6 +16,11 @@ registry: | |||
| black_list: | |||
| - "malicious.*-apb$" | |||
| - "^specific-blacklist-apb$" | |||
| - type: dockerhub | |||
| name: docker2 | |||
| url: https://registry.hub.docker.com | |||
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.
So empty auth_type/auth_name is also valid? If so, maybe the switch isn't the best then though it would be obvious if the switch was on the following:
switch reg.AuthType:
case "secret":
// do secret stuff
case "file":
// do file stuff
case "config":
// do config stuff
case "":
// do empty stuff
default:
// error case since this is an authtype we do not recognize
pkg/clients/kubernetes.go
Outdated
| } | ||
| var ret = make(map[string][]byte) | ||
|
|
||
| secretData, err := k8scli.CoreV1().Secrets(namespace).Get(secretName, meta_v1.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.
I'd put the line break before the var ret and remove the one after it.
BEFORE:
}
var ret = make(map[string][]byte)
secretData, err := k8scli.CoreV1().Secrets(namespace).Get(secretName, meta_v1.GetOptions{})AFTER:
}
var ret = make(map[string][]byte)
secretData, err := k8scli.CoreV1().Secrets(namespace).Get(secretName, meta_v1.GetOptions{})
pkg/clients/kubernetes.go
Outdated
| log.Error("Unable to load secret '%s' from namespace '%s'", secretName, namespace) | ||
| return ret, nil | ||
| } | ||
| log.Debug("Found secret with name %v\n", secretName) |
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.
I think this should be log.Debugf
pkg/clients/kubernetes.go
Outdated
|
|
||
| ret = secretData.Data | ||
|
|
||
| return ret, nil |
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.
feels weird to assign a value to ret just to return it on the next line, I'd expected: return secretData.Data I'd get rid of ret entirely. Because you're creating an array that you ultimately throwaway on every successful call. I'd rather create an array only during the error condition.
// GetSecretData - Returns the data inside of a given secret
func GetSecretData(secretName, namespace string) (map[string][]byte, error) {
var log logging.Logger
k8scli, err := Kubernetes(&log)
if err != nil {
return nil, err
}
secretData, err := k8scli.CoreV1().Secrets(namespace).Get(secretName, meta_v1.GetOptions{})
if err != nil {
log.Errorf("Unable to load secret '%s' from namespace '%s'", secretName, namespace)
return make(map[string][]byte), nil
}
log.Debugf("Found secret with name %v\n", secretName)
return secretData.Data, nil| if err != nil { | ||
| return "", err | ||
| } | ||
| req.SetBasicAuth(r.Config.User, r.Config.Pass) |
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.
these changes look okay 👍
pkg/registries/registry.go
Outdated
| @@ -60,6 +66,11 @@ func (c Config) Validate() bool { | |||
| if c.Name == "" { | |||
| return false | |||
| } | |||
| if c.AuthType == "file" || c.AuthType == "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.
do we need to validate the AuthType at all? i.e. if I enter "foobarauth" what will happen?
|
Valid options for registry config
If set to ``, then we are expecting that the registry does NOT require credentials. If set to If set to |
* Bug 1503289 - Move registry credentials to a secret and consumed from a different file * Linting fix * Refactored to read directly from the secret * More refactoring * Remove stale files * Updates * Moved getSecretData method to kube client * Update comment for public function * Better error returns * Removed Dockerhub user/pass requirement * Update testdata to fix tests * Updates for debug statements and generated config * Update CI * Update more tests * Logging updates and additional comments * Updated config error validation
Move registry credentials to a secret and consumed from a different file
This PR moves the registry credentials into a secret so that it is not visible in plaintext from the configmap.
Changes proposed in this pull request
IMPORTANT:
You now need your Dockerhub environment variables to be base64 or else the secret will fail to be created. Be sure to update catasb or the template vars.