Skip to content

Commit 920e168

Browse files
committed
feat(otelcol): add support for htpasswd file authentication
1 parent 6b8c5ee commit 920e168

File tree

4 files changed

+161
-11
lines changed

4 files changed

+161
-11
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Main (unreleased)
1616
- Add the `otelcol.exporter.faro` exporter to export traces and logs to Faro endpoint. (@mar4uk)
1717

1818
- Add entropy support for `loki.secretfilter` (@romain-gaillard)
19+
- Add htpasswd file based authentication for `otelcol.auth.basic` (@pkarakal)
1920

2021
### Enhancements
2122

docs/sources/reference/components/otelcol/otelcol.auth.basic.md

Lines changed: 98 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,21 @@ You can specify multiple `otelcol.auth.basic` components by giving them differen
3131
otelcol.auth.basic "<LABEL>" {
3232
username = "<USERNAME>"
3333
password = "<PASSWORD>"
34+
35+
htpasswd_file = "/etc/alloy/.htpasswd"
3436
}
3537
```
3638

3739
## Arguments
3840

3941
You can use the following arguments with `otelcol.auth.basic`:
4042

41-
| Name | Type | Description | Default | Required |
42-
| ---------- | -------- | -------------------------------------------------- | ------- | -------- |
43-
| `password` | `secret` | Password to use for basic authentication requests. | | yes |
44-
| `username` | `string` | Username to use for basic authentication requests. | | yes |
43+
| Name | Type | Description | Default | Required |
44+
|-----------------|----------|---------------------------------------------------------------------------------|---------|----------|
45+
| `password` | `secret` | Password to use for basic authentication requests. | | no |
46+
| `username` | `string` | Username to use for basic authentication requests. | | no |
47+
| `htpasswd_file` | `string` | File to use for basic authentication requests. It can be used in receivers only | | no |
48+
4549

4650
## Blocks
4751

@@ -73,8 +77,9 @@ The following fields are exported and can be referenced by other components:
7377

7478
`otelcol.auth.basic` doesn't expose any component-specific debug information.
7579

76-
## Example
80+
## Examples
7781

82+
### Forward signals to exporters
7883
This example configures [`otelcol.exporter.otlp`][otelcol.exporter.otlp] to use basic authentication:
7984

8085
```alloy
@@ -91,4 +96,91 @@ otelcol.auth.basic "creds" {
9196
}
9297
```
9398

94-
[otelcol.exporter.otlp]: ../otelcol.exporter.otlp/
99+
100+
### Authenticating requests for receivers
101+
102+
#### Use Username/Password
103+
This example configures [`otelcol.receiver.otlp`][otelcol.receiver.otlp] to use basic authentication using a single
104+
username and password combination:
105+
106+
```alloy
107+
otelcol.receiver.otlp "example" {
108+
grpc {
109+
endpoint = "127.0.0.1:4317"
110+
111+
auth = otelcol.auth.basic.creds.handler
112+
}
113+
114+
output {
115+
metrics = [otelcol.exporter.debug.default.input]
116+
logs = [otelcol.exporter.debug.default.input]
117+
traces = [otelcol.exporter.debug.default.input]
118+
}
119+
}
120+
121+
otelcol.exporter.debug "default" {}
122+
123+
otelcol.auth.basic "creds" {
124+
username = "demo"
125+
password = sys.env("API_KEY")
126+
}
127+
```
128+
129+
#### Use htpasswd file
130+
This example configures [`otelcol.receiver.otlp`][otelcol.receiver.otlp] to use basic authentication using an htpasswd
131+
file containing the users to use for basic auth:
132+
133+
```alloy
134+
otelcol.receiver.otlp "example" {
135+
grpc {
136+
endpoint = "127.0.0.1:4317"
137+
138+
auth = otelcol.auth.basic.creds.handler
139+
}
140+
141+
output {
142+
metrics = [otelcol.exporter.debug.default.input]
143+
logs = [otelcol.exporter.debug.default.input]
144+
traces = [otelcol.exporter.debug.default.input]
145+
}
146+
}
147+
148+
otelcol.exporter.debug "default" {}
149+
150+
otelcol.auth.basic "creds" {
151+
htpasswd_file = "/etc/alloy/.htpasswd"
152+
}
153+
```
154+
155+
#### Combination of both
156+
This example configures [`otelcol.receiver.otlp`][otelcol.receiver.otlp] to use basic authentication using a combination
157+
of both an htpasswd file and username/password. Note that if the username provided also exists in the htpasswd file, it
158+
takes precedence over the one in the htpasswd file:
159+
160+
```alloy
161+
otelcol.receiver.otlp "example" {
162+
grpc {
163+
endpoint = "127.0.0.1:4317"
164+
165+
auth = otelcol.auth.basic.creds.handler
166+
}
167+
168+
output {
169+
metrics = [otelcol.exporter.debug.default.input]
170+
logs = [otelcol.exporter.debug.default.input]
171+
traces = [otelcol.exporter.debug.default.input]
172+
}
173+
}
174+
175+
otelcol.exporter.debug "default" {}
176+
177+
otelcol.auth.basic "creds" {
178+
username = "demo"
179+
password = sys.env("API_KEY")
180+
181+
htpasswd_file = "/etc/alloy/.htpasswd"
182+
}
183+
```
184+
185+
186+
[otelcol.receiver.otlp]: ../otelcol.receiver.otlp/

internal/component/otelcol/auth/basic/basic.go

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
package basic
33

44
import (
5+
"errors"
56
"fmt"
67

78
"github.com/grafana/alloy/internal/component"
@@ -15,6 +16,11 @@ import (
1516
"go.opentelemetry.io/collector/pipeline"
1617
)
1718

19+
var (
20+
errNoCredentialSource = errors.New("no credential source provided") //nolint:gofmt
21+
errNoPasswordProvided = errors.New("no password provided")
22+
)
23+
1824
func init() {
1925
component.Register(component.Registration{
2026
Name: "otelcol.auth.basic",
@@ -31,8 +37,10 @@ func init() {
3137

3238
// Arguments configures the otelcol.auth.basic component.
3339
type Arguments struct {
34-
Username string `alloy:"username,attr"`
35-
Password alloytypes.Secret `alloy:"password,attr"`
40+
Username string `alloy:"username,attr,optional"`
41+
Password alloytypes.Secret `alloy:"password,attr,optional"`
42+
43+
HtpasswdFile string `alloy:"htpasswd_file,attr,optional"`
3644

3745
// DebugMetrics configures component internal metrics. Optional.
3846
DebugMetrics otelcolCfg.DebugMetricsArguments `alloy:"debug_metrics,block,optional"`
@@ -45,6 +53,24 @@ func (args *Arguments) SetToDefault() {
4553
args.DebugMetrics.SetToDefault()
4654
}
4755

56+
// Validate implements syntax.Validator
57+
func (args Arguments) Validate() error {
58+
// check if no argument was provided
59+
if args.Username == "" && args.Password == "" && args.HtpasswdFile == "" {
60+
return errNoCredentialSource
61+
}
62+
// the downstream basicauthextension package supports having both inline
63+
// and htpasswd files, so we should not error out in case both are
64+
// provided
65+
66+
// check if password was not provided when username is provided
67+
if args.Username != "" && args.Password == "" {
68+
return errNoPasswordProvided
69+
}
70+
71+
return nil
72+
}
73+
4874
// ConvertClient implements auth.Arguments.
4975
func (args Arguments) ConvertClient() (otelcomponent.Config, error) {
5076
return &basicauthextension.Config{
@@ -59,6 +85,7 @@ func (args Arguments) ConvertClient() (otelcomponent.Config, error) {
5985
func (args Arguments) ConvertServer() (otelcomponent.Config, error) {
6086
return &basicauthextension.Config{
6187
Htpasswd: &basicauthextension.HtpasswdSettings{
88+
File: args.HtpasswdFile,
6289
Inline: fmt.Sprintf("%s:%s", args.Username, args.Password),
6390
},
6491
}, nil

internal/component/otelcol/auth/basic/basic_test.go

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"net/http"
88
"net/http/httptest"
9+
"os"
910
"testing"
1011
"time"
1112

@@ -18,18 +19,24 @@ import (
1819
"github.com/stretchr/testify/assert"
1920
"github.com/stretchr/testify/require"
2021
extauth "go.opentelemetry.io/collector/extension/extensionauth"
22+
23+
"golang.org/x/crypto/bcrypt"
2124
)
2225

2326
const (
24-
actualUsername = "foo"
25-
actualPassword = "bar"
27+
actualUsername = "foo"
28+
actualPassword = "bar"
29+
htpasswdPath = ".htpasswd"
30+
htpasswdUser = "user"
31+
htpasswdPassword = "password"
2632
)
2733

2834
var (
2935
cfg = fmt.Sprintf(`
3036
username = "%s"
3137
password = "%s"
32-
`, actualUsername, actualPassword)
38+
htpasswd_file = "%s"
39+
`, actualUsername, actualPassword, htpasswdPath)
3340
)
3441

3542
// Test performs a basic integration test which runs the otelcol.auth.basic
@@ -87,6 +94,7 @@ func TestServerAuth(t *testing.T) {
8794
ctx := componenttest.TestContext(t)
8895
ctx, cancel := context.WithTimeout(ctx, time.Minute)
8996
defer cancel()
97+
createTestHtpasswdFile(t, htpasswdPath, htpasswdUser, htpasswdPassword)
9098

9199
ctrl := newTestComponent(t, ctx)
92100
require.NoError(t, ctrl.WaitRunning(time.Second), "component never started")
@@ -118,6 +126,28 @@ func TestServerAuth(t *testing.T) {
118126
b64EncodingAuth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", actualUsername, actualPassword)))
119127
_, err = otelServerExtension.Authenticate(ctx, map[string][]string{"Authorization": {"Basic " + b64EncodingAuth}})
120128
require.NoError(t, err)
129+
130+
b64EncodingAuth = base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", htpasswdUser, htpasswdPassword)))
131+
_, err = otelServerExtension.Authenticate(ctx, map[string][]string{"Authorization": {"Basic " + b64EncodingAuth}})
132+
require.NoError(t, err)
133+
}
134+
135+
func createTestHtpasswdFile(t *testing.T, path, username, password string) {
136+
t.Helper()
137+
138+
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
139+
require.NoError(t, err)
140+
141+
content := fmt.Sprintf("%s:%s\n", username, string(hash))
142+
143+
// create file
144+
f, err := os.OpenFile(".htpasswd", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
145+
require.NoError(t, err)
146+
defer f.Close()
147+
148+
// Write the entry to the file
149+
_, err = f.WriteString(content)
150+
require.NoError(t, err)
121151
}
122152

123153
// newTestComponent brings up and runs the test component.

0 commit comments

Comments
 (0)