Adding option to enable Back End HTTPS for Prow Ingress#573
Adding option to enable Back End HTTPS for Prow Ingress#573NiJuFirenzia wants to merge 2 commits intokubernetes-sigs:mainfrom
Conversation
✅ Deploy Preview for k8s-prow ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
Welcome @NiJuFirenzia! |
|
Hi @NiJuFirenzia. Thanks for your PR. I'm waiting for a github.com member to verify that this patch is reasonable to test. If it is, they should reply with Once the patch is verified, the new status will be reflected by the I understand the commands that are listed here. DetailsInstructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. |
|
This PR addresses Issue #328 |
|
@NiJuFirenzia thanks for your PR, can you check the CLA and sign it? |
|
/ok-to-test |
b32381d to
2cfbae6
Compare
| enableSSL bool | ||
| certFile string | ||
| keyFile string |
There was a problem hiding this comment.
let's group this into a small struct that we can embed at both places, we can share validation etc. similarly to how the ThingOptions bits above
| httpServer := &http.Server{ | ||
| Addr: ":" + strconv.Itoa(o.port), | ||
| Handler: hookMux, | ||
| } |
There was a problem hiding this comment.
this seems identical in both branches, so can be extracted before the condition
| // cacheLife is the time that we keep a pluginhelp.Help struct before considering it stale. | ||
| // We consider help valid for a minute to prevent excessive calls to hook. | ||
| const cacheLife = time.Minute | ||
| const tlsEnabledScehma = "https" |
There was a problem hiding this comment.
| const tlsEnabledScehma = "https" | |
| const tlsEnabledSchema = "https" |
| transport.TLSClientConfig = &tls.Config{ | ||
| RootCAs: caCertPool, | ||
| } | ||
| http.DefaultTransport = transport |
There was a problem hiding this comment.
helpAgent should get a custom client member with this transport instead of modifying the global default transport (this would affect anything else in deck that is a http client and relies on the default)
| caCert, err := os.ReadFile(ha.cert) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("error decoding cert file: %w", err) | ||
| } | ||
| caCertPool := x509.NewCertPool() | ||
| caCertPool.AppendCertsFromPEM(caCert) |
There was a problem hiding this comment.
I am not entirely sure I like this coupling. This essentially reuses Deck's server cert as a CA cert to establish trust with Hook's server cert for this behavior where Deck is a client to hook's pluginhelp endpoint? That's a bit hacky and suprising.
There was a problem hiding this comment.
That would require having to add another option flag to take in a client cert right? I had been trying to limit the options that we would need
2cfbae6 to
9865cdd
Compare
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: NiJuFirenzia The full list of commands accepted by this bot can be found here. DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
Signed-off-by: Nikhil Jiju <Nikhil.Jiju@fmr.com>
9865cdd to
2ad6fa1
Compare
|
All change requests have been addressed and this PR is ready for a re-review |
petr-muller
left a comment
There was a problem hiding this comment.
There's a flag name mismatch between error message and actual names. I have pointed out some opportunities to reduce stuttering (sslEnablement.EnableSSL and verbosity sslEnablement vs ssl) but treat them as non-blocking nits.
| func (o *SSLEnablementOptions) Validate(_ bool) error { | ||
| if o.EnableSSL { | ||
| if o.ServerCertFile == "" { | ||
| return errors.New("flag --enable-ssl was set to true but required flag --cert-file was not set") |
There was a problem hiding this comment.
| return errors.New("flag --enable-ssl was set to true but required flag --cert-file was not set") | |
| return errors.New("flag --enable-ssl was set to true but required flag --server-cert-file was not set") |
actual flags are different
| return errors.New("flag --enable-ssl was set to true but required flag --cert-file was not set") | ||
| } | ||
| if o.ServerKeyFile == "" { | ||
| return errors.New("flag --enable-ssl was set to true but required flag --key-file was not set") |
There was a problem hiding this comment.
| return errors.New("flag --enable-ssl was set to true but required flag --key-file was not set") | |
| return errors.New("flag --enable-ssl was set to true but required flag --server-key-file was not set") |
| } | ||
|
|
||
| func (o *SSLEnablementOptions) Validate(_ bool) error { | ||
| if o.EnableSSL { |
There was a problem hiding this comment.
We should either check also the opposite case (enabled is false but cert was passed, possibly confusing the admin they are using SSL when they are not), or make this an implied field from the presence of at least one file flags (if at least one is set, assume the user wants to have it enabled and validate both are set).
| bugzilla prowflagutil.BugzillaOptions | ||
| instrumentationOptions prowflagutil.InstrumentationOptions | ||
| jira prowflagutil.JiraOptions | ||
| sslEnablement prowflagutil.SSLEnablementOptions |
There was a problem hiding this comment.
| sslEnablement prowflagutil.SSLEnablementOptions | |
| ssl prowflagutil.SSLEnablementOptions |
| // HTTPS backend. If enableSSL is true, both certFile and keyFile must be set | ||
| // to the location of the cert and key files respectively. | ||
|
|
||
| type SSLEnablementOptions struct { |
There was a problem hiding this comment.
tbh I'd call this just SSLOptions or SSLServerOptions and the package flagutil/ssl
| EnableSSL bool | ||
| ServerCertFile string | ||
| ServerKeyFile string |
There was a problem hiding this comment.
| EnableSSL bool | |
| ServerCertFile string | |
| ServerKeyFile string | |
| Enabled bool | |
| CertFile string | |
| KeyFile string |
the instance of this struct will typically called ssl or something so meaning is typically provided by that context
There was a problem hiding this comment.
I agree with the name changes. I'll have updates to this PR out next week
Signed-off-by: Nikhil Jiju <Nikhil.Jiju@fmr.com>
ivankatliarchuk
left a comment
There was a problem hiding this comment.
I'm not too sure. Maybe missing something. What is definately missing is a documentation, like operator docs how to, then it could be easier to understand the intent.
Misleading flag name suggests mTLS was intended but not delivered, aka --client-cert-file name -> it's a CA cert, not a client cert from first look.
I'll try to explain
CA certificate (what the code actually uses) When deck calls hook over HTTPS, it needs to verify that hook's server certificate is trustworthy. If hook uses a self-signed or internal CA cert, the OS trust store won't know about it. So you load that CA cert into a x509.CertPool and put it in RootCAs:
caCertPool.AppendCertsFromPEM(caCert)
client = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: caCertPool, // "trust this CA when verifying the server"
},
},
}This is one-way TLS: deck verifies hook's identity, but hook doesn't verify deck's identity.
Client certificate (what the name implies)
A client cert is deck's own certificate that it presents to hook during the handshake, so hook can verify deck's identity. That's mutual TLS (mTLS). It would look like:
clientCert, _ := tls.LoadX509KeyPair(certFile, keyFile)
TLSClientConfig: &tls.Config{
Certificates: []tls.Certificate{clientCert}, // "here's who I am"
RootCAs: caCertPool,
}The problem
An operator reading --client-cert-file will naturally think "this is the cert deck presents to authenticate itself to hook." But the code uses it purely as a CA cert to trust hook's server cert. These are completely different files with different formats and different roles.
A better name would be --hook-ca-cert-file or --hook-tls-ca-file, which makes it clear: "the CA cert used to verify hook's TLS certificate."
Another problem/concert from security perspective
The solution seems like half implemented.
- One-way TLS only (deck verifies hook, hook doesn't verify deck)
- No cert rotation -> not too clear, missing documentation describing that
| } | ||
|
|
||
| if o.ssl.CertFile != "" && o.clientCertFile == "" { | ||
| return errors.New("flag --server-cert-file and --server-key-file was set to true but required flag --client-cert-file was not set") |
There was a problem hiding this comment.
"flag --server-cert-file and --server-key-file was set to true" — these are string flags, not booleans. Should be "were set" (plural) and drop "to true"
| type helpAgent struct { | ||
| path string | ||
| path string | ||
| cert string |
There was a problem hiding this comment.
cert field is a dead code. The field is set in newHelpAgent but never read after construction — the TLS client is fully initialized at construction time
| // cacheLife is the time that we keep a pluginhelp.Help struct before considering it stale. | ||
| // We consider help valid for a minute to prevent excessive calls to hook. | ||
| const cacheLife = time.Minute | ||
| const sslEnabledSchema = "https" |
There was a problem hiding this comment.
| const sslEnabledSchema = "https" | |
| const sslEnabledScheme = "https" |
should be sslEnabledScheme ("scheme" not "schema")
| } | ||
| } | ||
|
|
||
| if o.ssl.CertFile != "" && o.clientCertFile == "" { |
There was a problem hiding this comment.
I'm not sure about this requirement. Requiring --client-cert-file whenever --server-cert-file is set does not sound correct - these are independent. Deck can serve HTTPS to the ingress without needing to call hook over HTTPS. Maybe the check should only trigger if --hook-url starts with https://
| } | ||
|
|
||
| func (o *SSLOptions) Validate(_ bool) error { | ||
| if o.CertFile != "" || o.KeyFile != "" { |
There was a problem hiding this comment.
Worth to check for path exist and where or not file is missing.
Both kubernetes_cluster_clients.goand k8s_client.go use os.Stat inside their Validate methods to check kubeconfig paths.
Probably something like
if _, err := os.Stat(o.CertFile); err != nil {
return fmt.Errorf("--server-cert-file: %w", err)
}
if _, err := os.Stat(o.KeyFile); err != nil {
return fmt.Errorf("--server-key-file: %w", err)
}The benefit is that you get a clear error at startup rather than a cryptic TLS failure when ListenAndServeTLS is called.
|
For this PR, I would strongly recommend to create a documentation page describing this setup for operators and having in place describing alternatives. The honest answer without offence is: this in Application TLS is probably not worth the complexity it adds, this is opinionated answer, but some security best practice should be documented, and use cases with tradeoffs. Something like a page to consider other approaches, before diving in APP tls setup and certificate lifecycle world. As an operator, I would like to know why I'm now managing certs, handling rotation, and adding TLS overhead on connections that are already encrypted at the network layer(as example). Alternative approaches and tradeoff: Just use this PR
Service mesh (Istio, Linkerd, etc)
Zero trust / SPIFFE
Prow running within a private network / network perimeter
|
Prow currently requires SSL connections to be terminated before connecting to the deck and hook pods. This does not enable users to run prow with backend https enabled on their ingress. This PR adds the option to enable backend https on ingresses by passing in specific flags to the hook and deck pods. The argument flags will look like as below:
hook:
deck:
In the example above, a secret containing the cert file and key file was mounted to the deck and hook pods. Additionally this has been tested to continue working as is with ingress backend to be set to just HTTP if the flags are not passed in.