Skip to content

Commit 1126a62

Browse files
authored
Use auth-realm annotations instead of labels. (#7)
Labels have more restrictive value validation than annotations, so switch to annotations for specifying the basic authentication realm and type. To make up for the use of annotations, add a label selector flag so that operators can filter which Secrets to actually use. This fixes #4. Signed-off-by: James Peach <jpeach@vmware.com>
1 parent 6c15b6f commit 1126a62

File tree

4 files changed

+95
-36
lines changed

4 files changed

+95
-36
lines changed

README.md

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ Flags:
4141
--auth-realm string Basic authentication realm. (default "default")
4242
-h, --help help for htpasswd
4343
--metrics-address string The address the metrics endpoint binds to. (default ":8080")
44+
--selector string Selector (label-query) to filter Secrets, supports '=', '==', and '!='.
4445
--tls-ca-path string Path to the TLS CA certificate bundle.
4546
--tls-cert-path string Path to the TLS server certificate.
4647
--tls-key-path string Path to the TLS server key.
@@ -55,14 +56,25 @@ The htpasswd data must be stored in the `data` key, which is compatible
5556
with ingress-nginx [`auth-file` Secrets][2].
5657

5758
The `htpasswd` backend only accesses Secrets that are
58-
labeled with `projectcontour.io/auth-type: basic`. If the
59-
only be used if its value matches the value of the `--auth-realm` flag.
60-
The label `projectcontour.io/auth-realm: *` can be used to specify that a
61-
Secret can be used for all realms.
59+
annotated with `projectcontour.io/auth-type: basic`.
6260

63-
When it authenticates a request, the `htpassd` backend injects the
61+
Secrets that are annotated with the `projectcontour.io/auth-realm`
62+
will only be used if the annotation value matches the value of the
63+
`--auth-realm` flag.
64+
The `projectcontour.io/auth-realm: *` annotation explicitly marks
65+
a Secret as being valid for all realms.
66+
This is equivalent to omitting the annotation.
67+
68+
When it authenticates a request, the `htpasswd` backend injects the
6469
`Auth-Username` and `Auth-Realm` headers, which contain the
65-
authenticated user name basic authentication nd realm.
70+
authenticated user name and the basic authentication realm respectively.
71+
72+
The `--watch-namespaces` flag specifies the namespaces where the
73+
`htpasswd` backend will discover Secrets.
74+
If this flag is empty, Secrets from all namespaces will be used.
75+
76+
The `--selector` flag accepts a [label selector][5] that can be
77+
used to further restrict which Secrets the `htpasswd` backend will consume.
6678

6779
# Request Headers
6880

@@ -79,8 +91,8 @@ does.)
7991

8092
# Deploying `contour-authserver`
8193

82-
The recommended way to deploe `contour-authserver` is to use the Kustomize
83-
[deployment YAML](./config/default). This sill deploy services for both
94+
The recommended way to deploy `contour-authserver` is to use the Kustomize
95+
[deployment YAML](./config/default). This will deploy services for both
8496
the `testserver` and `htpasswd` backends. For developer deployments,
8597
[Skaffold](https://skaffold.dev/) seems to work reasonably well.
8698

@@ -90,3 +102,4 @@ There are no versioned releases or container images yet.
90102
[2]: https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/#authentication
91103
[3]: https://tools.ietf.org/html/rfc7617
92104
[4]: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/security/ext_authz_filter
105+
[5]: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors

pkg/auth/htpasswd.go

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,16 @@ import (
2323
"github.com/go-logr/logr"
2424
"github.com/tg123/go-htpasswd"
2525
v1 "k8s.io/api/core/v1"
26+
"k8s.io/apimachinery/pkg/labels"
2627
ctrl "sigs.k8s.io/controller-runtime"
2728
"sigs.k8s.io/controller-runtime/pkg/client"
2829
)
2930

3031
const (
31-
// LabelAuthType labels Secrets that can be used for basic Auth.
32-
LabelAuthType = "projectcontour.io/auth-type"
33-
// LabelAuthRealm labels Secrets that match our authentication realm
34-
LabelAuthRealm = "projectcontour.io/auth-realm"
32+
// AnnotationAuthType labels Secrets that can be used for basic Auth.
33+
AnnotationAuthType = "projectcontour.io/auth-type"
34+
// AnnotationAuthRealm labels Secrets that match our authentication realm
35+
AnnotationAuthRealm = "projectcontour.io/auth-realm"
3536
)
3637

3738
// Htpasswd watches Secrets for htpasswd files and uses them for HTTP Basic Authentication.
@@ -40,6 +41,7 @@ type Htpasswd struct {
4041
Realm string
4142
Client client.Client
4243
Passwords *htpasswd.File
44+
Selector labels.Selector
4345

4446
Lock sync.Mutex
4547
}
@@ -126,22 +128,33 @@ func (h *Htpasswd) Check(ctx context.Context, request *Request) (*Response, erro
126128

127129
// Reconcile ...
128130
func (h *Htpasswd) Reconcile(ctrl.Request) (ctrl.Result, error) {
129-
// First, find all the basic auth secrets for this realm.
131+
var opts []client.ListOption
132+
133+
if h.Selector != nil {
134+
opts = append(opts, client.MatchingLabelsSelector{Selector: h.Selector})
135+
}
136+
137+
// First, find all the auth secrets for this realm.
130138
secrets := &v1.SecretList{}
131-
if err := h.Client.List(context.Background(), secrets,
132-
client.MatchingLabels{LabelAuthType: "basic"},
133-
client.HasLabels{LabelAuthRealm}); err != nil {
139+
if err := h.Client.List(context.Background(), secrets, opts...); err != nil {
134140
return ctrl.Result{}, err
135141
}
136142

137143
passwdData := bytes.Buffer{}
138144

139145
for _, s := range secrets.Items {
140-
if s.Labels[LabelAuthRealm] != h.Realm &&
141-
s.Labels[LabelAuthRealm] != "*" {
146+
// Only look at basic auth secrets.
147+
if s.Annotations[AnnotationAuthType] != "basic" {
142148
continue
143149
}
144150

151+
// Accept the secret if it is for our realm or for any realm.
152+
if realm := s.Annotations[AnnotationAuthRealm]; realm != "" {
153+
if realm != h.Realm && realm != "*" {
154+
continue
155+
}
156+
}
157+
145158
// Check for the "auth" key, which is the format used by ingress-nginx.
146159
authData, ok := s.Data["auth"]
147160
if !ok {

pkg/auth/htpasswd_test.go

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,37 @@ import (
2323
"github.com/stretchr/testify/require"
2424
v1 "k8s.io/api/core/v1"
2525
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
26+
"k8s.io/apimachinery/pkg/labels"
2627
ctrl "sigs.k8s.io/controller-runtime"
2728
"sigs.k8s.io/controller-runtime/pkg/client/fake"
2829
"sigs.k8s.io/controller-runtime/pkg/log"
2930
)
3031

3132
func TestHtpasswdAuth(t *testing.T) {
3233
client := fake.NewFakeClient(
34+
&v1.Secret{
35+
ObjectMeta: metav1.ObjectMeta{
36+
Name: "notmatched",
37+
Namespace: metav1.NamespaceDefault,
38+
Annotations: map[string]string{
39+
AnnotationAuthType: "basic",
40+
AnnotationAuthRealm: "*",
41+
},
42+
},
43+
Type: v1.SecretTypeOpaque,
44+
Data: map[string][]byte{
45+
// user=notmatched, pass=notmatched
46+
"auth": []byte("notmatched:$apr1$4W6cRE66$iANZepJfRTrpk3OxlzxAC0"),
47+
},
48+
},
3349
&v1.Secret{
3450
ObjectMeta: metav1.ObjectMeta{
3551
Name: "example1",
3652
Namespace: metav1.NamespaceDefault,
37-
Labels: map[string]string{
38-
LabelAuthType: "basic",
39-
LabelAuthRealm: "*",
53+
Labels: map[string]string{"app": "authserver"},
54+
Annotations: map[string]string{
55+
AnnotationAuthType: "basic",
56+
AnnotationAuthRealm: "*",
4057
},
4158
},
4259
Type: v1.SecretTypeOpaque,
@@ -45,14 +62,14 @@ func TestHtpasswdAuth(t *testing.T) {
4562
"auth": []byte("example1:$apr1$WBCC5B.w$fUu8qiKG/rLdMs3OTy9gc0"),
4663
},
4764
},
48-
4965
&v1.Secret{
5066
ObjectMeta: metav1.ObjectMeta{
5167
Name: "example2",
5268
Namespace: metav1.NamespaceDefault,
53-
Labels: map[string]string{
54-
LabelAuthType: "basic",
55-
LabelAuthRealm: "*",
69+
Labels: map[string]string{"app": "authserver"},
70+
Annotations: map[string]string{
71+
AnnotationAuthType: "basic",
72+
AnnotationAuthRealm: "*",
5673
},
5774
},
5875
Type: v1.SecretTypeOpaque,
@@ -65,9 +82,10 @@ func TestHtpasswdAuth(t *testing.T) {
6582
ObjectMeta: metav1.ObjectMeta{
6683
Name: "example3",
6784
Namespace: metav1.NamespaceDefault,
68-
Labels: map[string]string{
69-
LabelAuthType: "basic",
70-
LabelAuthRealm: "example3",
85+
Labels: map[string]string{"app": "authserver"},
86+
Annotations: map[string]string{
87+
AnnotationAuthType: "basic",
88+
AnnotationAuthRealm: "example3",
7189
},
7290
},
7391
Type: v1.SecretTypeOpaque,
@@ -78,19 +96,26 @@ func TestHtpasswdAuth(t *testing.T) {
7896
},
7997
)
8098

99+
selector, err := labels.Parse("app=authserver")
100+
if err != nil {
101+
t.Fatalf("failed to parse selector: %s", err)
102+
}
103+
81104
auth := Htpasswd{
82-
Log: log.NullLogger{},
83-
Realm: "default",
84-
Client: client,
85-
Passwords: nil,
105+
Log: log.NullLogger{},
106+
Realm: "default",
107+
Client: client,
108+
Selector: selector,
86109
}
87110

88-
_, err := auth.Reconcile(ctrl.Request{})
111+
_, err = auth.Reconcile(ctrl.Request{})
89112
assert.NoError(t, err, "reconciliation should not have failed")
90113
assert.NotNil(t, auth.Passwords, "reconciliation should have set a htpasswd file")
91114
assert.True(t, auth.Match("example1", "example1"), "auth for example1:example1 should have succeeded")
92115
assert.True(t, auth.Match("example2", "example2"), "auth for example2:example2 should have succeeded")
93116
assert.False(t, auth.Match("example3", "example3"), "auth for example3:example3 should have failed (wrong realm)")
117+
assert.False(t, auth.Match("notmatched", "notmatched"),
118+
"auth for notmatched:notmatched should have failed (filtered by label selector)")
94119

95120
// Check an unauthorized response.
96121
response, err := auth.Check(context.TODO(), &Request{

pkg/cli/htpasswd.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"github.com/projectcontour/contour-authserver/pkg/auth"
2020
"github.com/spf13/cobra"
2121
"google.golang.org/grpc"
22+
"k8s.io/apimachinery/pkg/labels"
2223
"k8s.io/apimachinery/pkg/runtime"
2324
"k8s.io/client-go/kubernetes/scheme"
2425
ctrl "sigs.k8s.io/controller-runtime"
@@ -52,10 +53,16 @@ func NewHtpasswdCommand() *cobra.Command {
5253
return ExitErrorf(EX_CONFIG, "failed to create controller manager: %s", err)
5354
}
5455

56+
secretsSelector, err := labels.Parse(mustString(cmd.Flags().GetString("selector")))
57+
if err != nil {
58+
return ExitErrorf(EX_CONFIG, "failed to parse secrets selector: %s", err)
59+
}
60+
5561
htpasswd := &auth.Htpasswd{
56-
Log: log,
57-
Client: mgr.GetClient(),
58-
Realm: mustString(cmd.Flags().GetString("auth-realm")),
62+
Log: log,
63+
Client: mgr.GetClient(),
64+
Realm: mustString(cmd.Flags().GetString("auth-realm")),
65+
Selector: secretsSelector,
5966
}
6067

6168
if err := htpasswd.RegisterWithManager(mgr); err != nil {
@@ -128,6 +135,7 @@ func NewHtpasswdCommand() *cobra.Command {
128135
// Controller flags.
129136
cmd.Flags().String("metrics-address", ":8080", "The address the metrics endpoint binds to.")
130137
cmd.Flags().StringSlice("watch-namespaces", []string{}, "The list of namespaces to watch for Secrets.")
138+
cmd.Flags().String("selector", "", "Selector (label-query) to filter Secrets, supports '=', '==', and '!='.")
131139

132140
// GRPC flags.
133141
cmd.Flags().String("address", ":9090", "The address the authentication endpoint binds to.")

0 commit comments

Comments
 (0)