Skip to content

Commit 9e10790

Browse files
committed
Enhance Tailscale integration by adding OAuth support: update configuration to include OAuth client ID, secret, and scopes, modify Tailscale service to prioritize OAuth over API key, and update README with new authentication methods and deployment instructions. Update Dockerfile to use Go 1.23 and adjust environment variables in Docker Compose and Kubernetes manifests accordingly.
1 parent 96d379f commit 9e10790

File tree

10 files changed

+157
-28
lines changed

10 files changed

+157
-28
lines changed

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ COPY frontend/ ./
1212
RUN npm run build
1313

1414
# Backend build stage
15-
FROM golang:1.21-alpine AS backend-build
15+
FROM golang:1.23-alpine AS backend-build
1616

1717
WORKDIR /app/backend
1818

README.md

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,20 @@ A modern, real-time web application for visualizing and analyzing network traffi
1010

1111
The fastest way to get started using pre-built images:
1212

13+
**Using OAuth (Recommended):**
14+
```bash
15+
docker run -d \
16+
--name tsflow \
17+
-p 8080:8080 \
18+
-e TAILSCALE_OAUTH_CLIENT_ID=your-client-id \
19+
-e TAILSCALE_OAUTH_CLIENT_SECRET=your-client-secret \
20+
-e TAILSCALE_TAILNET=your-organization \
21+
-e ENVIRONMENT=production \
22+
--restart unless-stopped \
23+
ghcr.io/rajsinghtech/tsflow:latest
24+
```
25+
26+
**Using API Key:**
1327
```bash
1428
docker run -d \
1529
--name tsflow \
@@ -29,12 +43,28 @@ Navigate to `http://localhost:8080` to access the dashboard.
2943

3044
Go to the [Logs tab](https://login.tailscale.com/admin/logs) in your Tailscale Admin Console and ensure that Network Flow Logs are **enabled**. **Note**: This requires a **Premium** or **Enterprise** plan.
3145

32-
### Finding Your Tailscale Credentials
46+
### Authentication Methods
47+
48+
TSFlow supports two authentication methods with Tailscale. You only need to configure one method.
49+
50+
#### Method 1: OAuth Client Credentials (Recommended)
51+
52+
OAuth provides better security through automatic token refresh and fine-grained permissions.
53+
54+
1. Go to the [OAuth clients page](https://login.tailscale.com/admin/settings/oauth) in your Tailscale Admin Console
55+
2. Create a new OAuth client
56+
3. Copy the Client ID and Client Secret
57+
4. Set the following environment variables:
58+
- `TAILSCALE_OAUTH_CLIENT_ID=your-client-id`
59+
- `TAILSCALE_OAUTH_CLIENT_SECRET=your-client-secret`
60+
- `TAILSCALE_OAUTH_SCOPES=all:read,devices:read,network-logs:read` (optional, defaults to `all:read`)
61+
62+
#### Method 2: API Key (Legacy)
3363

34-
#### API Key
3564
1. Go to the [API keys page](https://login.tailscale.com/admin/settings/keys) in your Tailscale Admin Console
3665
2. Create a new API key
3766
3. Copy the generated API key (starts with `tskey-api-`)
67+
4. Set `TAILSCALE_API_KEY=your-api-key`
3868

3969
#### Organization Name
4070
1. Go to the [Settings page](https://login.tailscale.com/admin/settings/general) in your Tailscale Admin Console
@@ -45,16 +75,41 @@ Go to the [Logs tab](https://login.tailscale.com/admin/logs) in your Tailscale A
4575

4676
| Variable | Description | Required | Default |
4777
|----------|-------------|----------|---------|
48-
| `TAILSCALE_API_KEY` | Your Tailscale API key | Yes | - |
4978
| `TAILSCALE_TAILNET` | Your organization name | Yes | - |
79+
| **OAuth Method** |
80+
| `TAILSCALE_OAUTH_CLIENT_ID` | OAuth client ID | Yes* | - |
81+
| `TAILSCALE_OAUTH_CLIENT_SECRET` | OAuth client secret | Yes* | - |
82+
| `TAILSCALE_OAUTH_SCOPES` | OAuth scopes (comma-separated) | No | `all:read` |
83+
| **API Key Method** |
84+
| `TAILSCALE_API_KEY` | Your Tailscale API key | Yes* | - |
85+
| **Other** |
5086
| `PORT` | Backend server port | No | `8080` |
5187

88+
*Either OAuth credentials OR API key must be provided
89+
5290
## Deployment Options
5391

5492
### Using Docker Compose
5593

5694
Create a `docker-compose.yml` file:
5795

96+
**Using OAuth (Recommended):**
97+
```yaml
98+
services:
99+
tsflow:
100+
image: ghcr.io/rajsinghtech/tsflow:latest
101+
container_name: tsflow
102+
ports:
103+
- "8080:8080"
104+
environment:
105+
- TAILSCALE_OAUTH_CLIENT_ID=your-client-id
106+
- TAILSCALE_OAUTH_CLIENT_SECRET=your-client-secret
107+
- TAILSCALE_TAILNET=your-organization
108+
- PORT=8080
109+
restart: unless-stopped
110+
```
111+
112+
**Using API Key:**
58113
```yaml
59114
services:
60115
tsflow:

backend/backend

12 MB
Binary file not shown.

backend/go.mod

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
module github.com/rajsinghtech/tsflow/backend
22

3-
go 1.21
3+
go 1.23.0
4+
5+
toolchain go1.24.4
46

57
require (
68
github.com/gin-contrib/cors v1.4.0
79
github.com/gin-gonic/gin v1.9.1
810
github.com/joho/godotenv v1.4.0
11+
golang.org/x/oauth2 v0.30.0
912
)
1013

1114
require (

backend/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0
9898
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
9999
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
100100
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
101+
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
102+
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
101103
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
102104
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
103105
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

backend/internal/config/config.go

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,52 @@ package config
22

33
import (
44
"errors"
5+
"log"
56
"os"
7+
"strings"
68
)
79

810
// Config holds the application configuration
911
type Config struct {
10-
TailscaleAPIKey string
11-
TailscaleTailnet string
12-
Port string
13-
Environment string
12+
TailscaleAPIKey string
13+
TailscaleTailnet string
14+
TailscaleOAuthClientID string
15+
TailscaleOAuthClientSecret string
16+
TailscaleOAuthScopes []string
17+
Port string
18+
Environment string
1419
}
1520

1621
// Load loads configuration from environment variables
1722
func Load() *Config {
1823
return &Config{
19-
TailscaleAPIKey: os.Getenv("TAILSCALE_API_KEY"),
20-
TailscaleTailnet: os.Getenv("TAILSCALE_TAILNET"),
21-
Port: getEnvWithDefault("PORT", "8080"),
22-
Environment: getEnvWithDefault("ENVIRONMENT", "development"),
24+
TailscaleAPIKey: os.Getenv("TAILSCALE_API_KEY"),
25+
TailscaleTailnet: os.Getenv("TAILSCALE_TAILNET"),
26+
TailscaleOAuthClientID: os.Getenv("TAILSCALE_OAUTH_CLIENT_ID"),
27+
TailscaleOAuthClientSecret: os.Getenv("TAILSCALE_OAUTH_CLIENT_SECRET"),
28+
TailscaleOAuthScopes: parseScopes(os.Getenv("TAILSCALE_OAUTH_SCOPES")),
29+
Port: getEnvWithDefault("PORT", "8080"),
30+
Environment: getEnvWithDefault("ENVIRONMENT", "development"),
2331
}
2432
}
2533

2634
// Validate validates the configuration
2735
func (c *Config) Validate() error {
28-
if c.TailscaleAPIKey == "" {
29-
return errors.New("TAILSCALE_API_KEY is required")
30-
}
3136
if c.TailscaleTailnet == "" {
3237
return errors.New("TAILSCALE_TAILNET is required")
3338
}
39+
40+
hasAPIKey := c.TailscaleAPIKey != ""
41+
hasOAuth := c.TailscaleOAuthClientID != "" && c.TailscaleOAuthClientSecret != ""
42+
43+
if !hasAPIKey && !hasOAuth {
44+
return errors.New("either TAILSCALE_API_KEY or both TAILSCALE_OAUTH_CLIENT_ID and TAILSCALE_OAUTH_CLIENT_SECRET must be provided")
45+
}
46+
47+
if hasAPIKey && hasOAuth {
48+
log.Println("Both API key and OAuth credentials provided. OAuth will take precedence.")
49+
}
50+
3451
return nil
3552
}
3653

@@ -41,3 +58,15 @@ func getEnvWithDefault(key, defaultValue string) string {
4158
}
4259
return defaultValue
4360
}
61+
62+
// parseScopes parses a comma-separated string of OAuth scopes
63+
func parseScopes(scopesStr string) []string {
64+
if scopesStr == "" {
65+
return []string{"all:read"}
66+
}
67+
scopes := strings.Split(scopesStr, ",")
68+
for i, scope := range scopes {
69+
scopes[i] = strings.TrimSpace(scope)
70+
}
71+
return scopes
72+
}

backend/internal/services/tailscale.go

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
11
package services
22

33
import (
4+
"context"
45
"encoding/json"
56
"fmt"
67
"io"
78
"net/http"
89
"time"
10+
11+
"github.com/rajsinghtech/tsflow/backend/internal/config"
12+
"golang.org/x/oauth2/clientcredentials"
913
)
1014

1115
// TailscaleService handles interactions with the Tailscale API
1216
type TailscaleService struct {
13-
apiKey string
14-
tailnet string
15-
client *http.Client
17+
apiKey string
18+
oauthConfig *clientcredentials.Config
19+
tailnet string
20+
client *http.Client
21+
useOAuth bool
1622
}
1723

1824
// Device represents a Tailscale device
@@ -58,14 +64,31 @@ type NetworkLogsResponse struct {
5864
}
5965

6066
// NewTailscaleService creates a new Tailscale service
61-
func NewTailscaleService(apiKey, tailnet string) *TailscaleService {
62-
return &TailscaleService{
63-
apiKey: apiKey,
64-
tailnet: tailnet,
65-
client: &http.Client{
67+
func NewTailscaleService(cfg *config.Config) *TailscaleService {
68+
ts := &TailscaleService{
69+
tailnet: cfg.TailscaleTailnet,
70+
}
71+
72+
// Prioritize OAuth if configured, fallback to API key
73+
if cfg.TailscaleOAuthClientID != "" && cfg.TailscaleOAuthClientSecret != "" {
74+
ts.oauthConfig = &clientcredentials.Config{
75+
ClientID: cfg.TailscaleOAuthClientID,
76+
ClientSecret: cfg.TailscaleOAuthClientSecret,
77+
Scopes: cfg.TailscaleOAuthScopes,
78+
TokenURL: "https://api.tailscale.com/api/v2/oauth/token",
79+
}
80+
ts.client = ts.oauthConfig.Client(context.Background())
81+
ts.client.Timeout = 2 * time.Minute
82+
ts.useOAuth = true
83+
} else if cfg.TailscaleAPIKey != "" {
84+
ts.apiKey = cfg.TailscaleAPIKey
85+
ts.client = &http.Client{
6686
Timeout: 2 * time.Minute,
67-
},
87+
}
88+
ts.useOAuth = false
6889
}
90+
91+
return ts
6992
}
7093

7194
// makeRequest makes an authenticated request to the Tailscale API
@@ -77,7 +100,11 @@ func (ts *TailscaleService) makeRequest(endpoint string) ([]byte, error) {
77100
return nil, fmt.Errorf("failed to create request: %w", err)
78101
}
79102

80-
req.Header.Set("Authorization", "Bearer "+ts.apiKey)
103+
// OAuth client handles authentication automatically via HTTP client
104+
// For API key authentication, set Authorization header manually
105+
if !ts.useOAuth && ts.apiKey != "" {
106+
req.Header.Set("Authorization", "Bearer "+ts.apiKey)
107+
}
81108
req.Header.Set("Accept", "application/json")
82109

83110
resp, err := ts.client.Do(req)

backend/main.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ func main() {
2222
log.Fatalf("Configuration error: %v", err)
2323
}
2424

25-
tailscaleService := services.NewTailscaleService(cfg.TailscaleAPIKey, cfg.TailscaleTailnet)
25+
tailscaleService := services.NewTailscaleService(cfg)
2626
handlerService := handlers.NewHandlers(tailscaleService)
2727

2828
if cfg.Environment == "production" {
@@ -77,6 +77,13 @@ func main() {
7777
log.Printf("Starting TSFlow server on port %s", port)
7878
log.Printf("Tailnet: %s", cfg.TailscaleTailnet)
7979
log.Printf("Environment: %s", cfg.Environment)
80+
81+
// Log authentication method being used
82+
if cfg.TailscaleOAuthClientID != "" && cfg.TailscaleOAuthClientSecret != "" {
83+
log.Printf("Authentication: OAuth Client Credentials (Client ID: %s)", cfg.TailscaleOAuthClientID)
84+
} else {
85+
log.Printf("Authentication: API Key")
86+
}
8087

8188
if err := router.Run("0.0.0.0:" + port); err != nil {
8289
log.Fatalf("Failed to start server: %v", err)

docker-compose.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ services:
66
environment:
77
- TAILSCALE_API_KEY=${TAILSCALE_API_KEY}
88
- TAILSCALE_TAILNET=${TAILSCALE_TAILNET}
9+
- TAILSCALE_OAUTH_CLIENT_ID=${TAILSCALE_OAUTH_CLIENT_ID}
10+
- TAILSCALE_OAUTH_CLIENT_SECRET=${TAILSCALE_OAUTH_CLIENT_SECRET}
11+
- TAILSCALE_OAUTH_SCOPES=${TAILSCALE_OAUTH_SCOPES}
912
- PORT=8080
1013
- ENVIRONMENT=production
1114
env_file:

k8s/secret.yaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,7 @@ metadata:
55
type: Opaque
66
stringData:
77
TAILSCALE_API_KEY: ${TAILSCALE_API_KEY}
8-
TAILSCALE_TAILNET: ${TAILSCALE_TAILNET}
8+
TAILSCALE_TAILNET: ${TAILSCALE_TAILNET}
9+
TAILSCALE_OAUTH_CLIENT_ID: ${TAILSCALE_OAUTH_CLIENT_ID}
10+
TAILSCALE_OAUTH_CLIENT_SECRET: ${TAILSCALE_OAUTH_CLIENT_SECRET}
11+
TAILSCALE_OAUTH_SCOPES: ${TAILSCALE_OAUTH_SCOPES}

0 commit comments

Comments
 (0)