Skip to content

Commit ab772fd

Browse files
Surya Ahujameta-codesync[bot]
authored andcommitted
add support for reading tls config from a file
Reviewed By: shringiarpit26 Differential Revision: D82873724 fbshipit-source-id: 937786ca77617e9c46923fc5caab9c9f591759e4
1 parent 765b14d commit ab772fd

File tree

5 files changed

+619
-19
lines changed

5 files changed

+619
-19
lines changed

cmds/client/main.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,24 +30,25 @@ var (
3030
authenMode = flag.String("authen-mode", "pap", "valid choices, [pap ascii]")
3131

3232
// TLS options
33-
useTLS = flag.Bool("tls", false, "enable TLS support as per IETF draft-ietf-opsawg-tacacs-tls13-07")
34-
tlsCertFile = flag.String("tls-cert", "", "path to TLS client certificate file")
35-
tlsKeyFile = flag.String("tls-key", "", "path to TLS client key file")
36-
tlsCAFile = flag.String("tls-ca", "", "path to TLS CA certificate file for server certificate validation")
37-
tlsServerName = flag.String("tls-server-name", "", "server name for TLS certificate validation")
38-
tlsInsecureSkipVerify = flag.Bool("tls-insecure-skip-verify", false, "skip TLS certificate verification (not recommended for production)")
33+
tlsConfigFile = flag.String("tls-config", "", "path to TLS configuration file in JSON format. When set, the values inside the file this will override all other TLS cmdline flags")
3934
)
4035

4136
func main() {
4237
flag.Parse()
38+
4339
verifyFlags()
4440

4541
var c *tq.Client
4642
var err error
4743

48-
if *useTLS {
44+
if *tlsConfigFile != "" {
45+
config, err := tq.LoadTLSConfig(*tlsConfigFile)
46+
if err != nil {
47+
fmt.Printf("Error loading TLS config file: %v\n", err)
48+
os.Exit(1)
49+
}
4950
// Create TLS configuration
50-
tlsConfig, tlsErr := tq.GenClientTLSConfig(*tlsServerName, *tlsCertFile, *tlsKeyFile, *tlsCAFile, *tlsInsecureSkipVerify)
51+
tlsConfig, tlsErr := tq.GenClientTLSConfig(config)
5152
if tlsErr != nil {
5253
fmt.Printf("Error creating TLS config: %v\n", tlsErr)
5354
os.Exit(1)

cmds/client/tls.conf

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"cert_file": "./client.crt",
3+
"key_file": "./client.key",
4+
"ca_file": "./ca.crt",
5+
"server_name": "localhost",
6+
"insecure_skip_verify": false
7+
}

docs/tls_support.md

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,140 @@ openssl rsa -noout -modulus -in server.key | openssl md5
413413
# The MD5 hashes should match
414414
```
415415

416+
# TLS Configuration File
417+
418+
The TACACS+ client supports loading TLS configuration from a JSON file using the
419+
`-tls-config` flag. When a TLS configuration file is specified, TLS is
420+
automatically enabled for the connection.
421+
422+
## Architecture Overview
423+
424+
The TLS configuration follows this flow:
425+
426+
1. **JSON Config File**`LoadTLSConfig()`**`ParsedTLSConfig` struct**
427+
2. **`ParsedTLSConfig`**`GenClientTLSConfig()`**`tls.Config` for client**
428+
3. **Client** uses the generated `tls.Config` to establish TLS connections
429+
430+
### Key Components
431+
432+
- **`ParsedTLSConfig`**: A Go struct that represents the TLS configuration loaded from JSON
433+
- **`LoadTLSConfig()`**: Function that loads and validates the JSON configuration file
434+
- **`GenClientTLSConfig()`**: Function that creates a `tls.Config` from `ParsedTLSConfig`
435+
- **Automatic path resolution**: Relative paths are automatically converted to absolute paths
436+
- **File validation**: All certificate files are verified to exist during configuration loading
437+
438+
## File Format
439+
440+
The TLS configuration file must be in JSON format. See `tls.conf` for a sample config.
441+
442+
## Configuration Options
443+
444+
| Field | Type | Description | Required |
445+
| ---------------------- | ------- | ---------------------------------------------------------------- | -------- |
446+
| `cert_file` | string | Absolute/Relative Path to client certificate file (PEM format) | No |
447+
| `key_file` | string | Absolute/Relative Path to client private key file (PEM format) | No |
448+
| `ca_file` | string | Absolute/Relative Path to CA certificate file for server verification (PEM format) | No |
449+
| `server_name` | string | Server name for certificate validation (must match server's SAN) | No |
450+
| `insecure_skip_verify` | boolean | Skip certificate verification (not recommended for production) | No |
451+
452+
### Path Handling
453+
454+
- **Relative paths**: `./client.crt`, `../certs/ca.crt`, `config/client.key`
455+
- **Absolute paths**: `/etc/ssl/certs/client.crt`, `/path/to/ca.crt`
456+
- **Path validation**: All specified certificate files are verified to exist during configuration loading
457+
- **Automatic resolution**: Relative paths are automatically converted to absolute paths
458+
- **No shell expansion**: Tilde (`~`) and environment variables are not supported
459+
460+
## Usage Examples
461+
462+
### Basic TLS with server certificate verification:
463+
464+
```json
465+
{
466+
"ca_file": "./ca.crt",
467+
"server_name": "localhost",
468+
"insecure_skip_verify": false
469+
}
470+
```
471+
472+
```bash
473+
./client -tls-config tls.json -username cisco
474+
```
475+
476+
### Mutual TLS (client and server certificates):
477+
478+
```json
479+
{
480+
"cert_file": "./client.crt",
481+
"key_file": "./client.key",
482+
"ca_file": "./ca.crt",
483+
"server_name": "localhost",
484+
"insecure_skip_verify": false
485+
}
486+
```
487+
488+
```bash
489+
./client -tls-config tls.json -username cisco
490+
```
491+
492+
### Development/Testing with insecure skip verify:
493+
494+
```json
495+
{
496+
"cert_file": "./client.crt",
497+
"key_file": "./client.key",
498+
"server_name": "localhost",
499+
"insecure_skip_verify": true
500+
}
501+
```
502+
503+
```bash
504+
./client -tls-config tls.json -username cisco
505+
```
506+
507+
**⚠️ Warning**: Never use `insecure_skip_verify: true` in production environments.
508+
509+
## Implementation Details
510+
511+
### ParsedTLSConfig Structure
512+
513+
The `ParsedTLSConfig` struct contains:
514+
515+
```go
516+
type ParsedTLSConfig struct {
517+
// Certificate files
518+
CertFile string `json:"cert_file"`
519+
KeyFile string `json:"key_file"`
520+
CAFile string `json:"ca_file"`
521+
522+
// Server name for certificate validation
523+
ServerName string `json:"server_name"`
524+
525+
// Skip certificate verification (not recommended for production)
526+
InsecureSkipVerify bool `json:"insecure_skip_verify"`
527+
}
528+
```
529+
530+
### Configuration Validation
531+
532+
During `LoadTLSConfig()`, the system:
533+
534+
-**Validates JSON syntax** and structure
535+
-**Converts relative paths** to absolute paths using `filepath.Abs()`
536+
-**Verifies file existence** for all certificate files using `os.Stat()`
537+
-**Validates certificate/key pairs** (cert_file requires key_file and vice versa)
538+
-**Returns descriptive errors** for missing files or invalid configurations
539+
540+
### Client TLS Config Generation
541+
542+
The `GenClientTLSConfig()` function creates a standard Go `tls.Config` with:
543+
544+
- **TLS 1.3 minimum version** (as per IETF draft requirements)
545+
- **Server name indication** from `server_name` field
546+
- **Client certificates** loaded from `cert_file` and `key_file`
547+
- **Root CA certificates** loaded from `ca_file` for server verification
548+
- **Skip verification flag** from `insecure_skip_verify` (if enabled)
549+
416550
## Security Considerations
417551

418552
When deploying TACACS+ over TLS in production:

tls.go

Lines changed: 104 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ package tacquito
33
import (
44
"crypto/tls"
55
"crypto/x509"
6+
"encoding/json"
67
"errors"
8+
"fmt"
79
"net"
810
"os"
11+
"path/filepath"
912
"time"
1013
)
1114

@@ -45,21 +48,19 @@ func (l *TLSDeadlineListener) SetDeadline(t time.Time) error {
4548
func GenTLSConfig(certFile, keyFile, CAFile string, requireMutualAuth bool) (*tls.Config, error) {
4649
config := &tls.Config{
4750
MinVersion: tls.VersionTLS13, // Require TLS 1.3 as per the IETF draft
48-
ClientAuth: tls.VerifyClientCertIfGiven, // Mutual authentication is optional
51+
ClientAuth: tls.VerifyClientCertIfGiven, // Mutual authentication is optional to start with, but is highly recommended
4952
}
5053

5154
if certFile == "" || keyFile == "" {
5255
return nil, errors.New("TLS is enabled but certificate or key file is not provided")
5356
}
5457

55-
// Load server certificate and key
5658
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
5759
if err != nil {
5860
return nil, err
5961
}
6062
config.Certificates = []tls.Certificate{cert}
6163

62-
// Configure client certificate verification if CA file is provided
6364
if CAFile != "" {
6465
data, err := os.ReadFile(CAFile)
6566
if err != nil {
@@ -80,25 +81,24 @@ func GenTLSConfig(certFile, keyFile, CAFile string, requireMutualAuth bool) (*tl
8081
}
8182

8283
// createTLSConfig creates a TLS configuration based on the provided command-line flags
83-
func GenClientTLSConfig(serverName, certFile, keyFile, CAFile string, skipVerification bool) (*tls.Config, error) {
84+
func GenClientTLSConfig(p *ParsedTLSConfig) (*tls.Config, error) {
8485
config := &tls.Config{
8586
MinVersion: tls.VersionTLS13, // Require TLS 1.3 as per the IETF draft
86-
ServerName: serverName,
87-
InsecureSkipVerify: skipVerification,
87+
ServerName: p.ServerName,
88+
InsecureSkipVerify: p.InsecureSkipVerify, // false by default
8889
}
8990

9091
// Client certificates are optional - only load them for mutual TLS when both are provided
91-
if certFile != "" && keyFile != "" {
92-
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
92+
if p.CertFile != "" && p.KeyFile != "" {
93+
cert, err := tls.LoadX509KeyPair(p.CertFile, p.KeyFile)
9394
if err != nil {
9495
return nil, err
9596
}
9697
config.Certificates = []tls.Certificate{cert}
9798
}
9899

99-
// Set RootCAs for server certificate verification if CA file is provided
100-
if CAFile != "" {
101-
data, err := os.ReadFile(CAFile)
100+
if p.CAFile != "" {
101+
data, err := os.ReadFile(p.CAFile)
102102
if err != nil {
103103
return nil, err
104104
}
@@ -111,3 +111,96 @@ func GenClientTLSConfig(serverName, certFile, keyFile, CAFile string, skipVerifi
111111

112112
return config, nil
113113
}
114+
115+
// TLSConfig represents the TLS configuration that can be loaded from a JSON file
116+
// If a TLS config file is specified, TLS is automatically enabled
117+
type ParsedTLSConfig struct {
118+
// Certificate files
119+
CertFile string `json:"cert_file"`
120+
KeyFile string `json:"key_file"`
121+
CAFile string `json:"ca_file"`
122+
123+
// Server name for certificate validation
124+
ServerName string `json:"server_name"`
125+
126+
// Skip certificate verification (not recommended for production)
127+
InsecureSkipVerify bool `json:"insecure_skip_verify"`
128+
}
129+
130+
// LoadTLSConfig loads TLS configuration from a JSON file
131+
func LoadTLSConfig(filename string) (*ParsedTLSConfig, error) {
132+
if filename == "" {
133+
return nil, fmt.Errorf("TLS config file path is empty")
134+
}
135+
136+
if _, err := os.Stat(filename); os.IsNotExist(err) {
137+
return nil, fmt.Errorf("TLS config file does not exist: %s", filename)
138+
}
139+
140+
data, err := os.ReadFile(filename)
141+
if err != nil {
142+
return nil, fmt.Errorf("failed to read TLS config file: %w", err)
143+
}
144+
145+
var config ParsedTLSConfig
146+
err = json.Unmarshal(data, &config)
147+
if err != nil {
148+
return nil, fmt.Errorf("failed to parse JSON TLS config: %w", err)
149+
}
150+
151+
if err := config.Validate(); err != nil {
152+
return nil, fmt.Errorf("invalid TLS config: %w", err)
153+
}
154+
155+
return &config, nil
156+
}
157+
158+
// Validate checks if the TLS configuration is valid
159+
func (c *ParsedTLSConfig) Validate() error {
160+
var err error
161+
162+
if c.CertFile, err = resolvePath(c.CertFile, "TLS certificate"); err != nil {
163+
return err
164+
}
165+
166+
if c.KeyFile, err = resolvePath(c.KeyFile, "TLS key"); err != nil {
167+
return err
168+
}
169+
170+
if c.CAFile, err = resolvePath(c.CAFile, "TLS CA"); err != nil {
171+
return err
172+
}
173+
174+
// If client cert is specified, key must also be specified and vice versa
175+
if c.CertFile != "" && c.KeyFile == "" {
176+
return fmt.Errorf("TLS key file must be specified when certificate file is provided")
177+
}
178+
if c.KeyFile != "" && c.CertFile == "" {
179+
return fmt.Errorf("TLS certificate file must be specified when key file is provided")
180+
}
181+
182+
return nil
183+
}
184+
185+
// resolvePath converts relative paths to absolute paths and checks if the file exists
186+
// Returns the absolute path and an error if the file doesn't exist or path conversion fails
187+
func resolvePath(path, fileType string) (string, error) {
188+
if path == "" {
189+
return "", nil
190+
}
191+
192+
// Convert to absolute path (handles relative paths like ./file or ../file)
193+
absPath, err := filepath.Abs(path)
194+
if err != nil {
195+
return "", fmt.Errorf("failed to convert %s file path '%s' to absolute path: %w", fileType, path, err)
196+
}
197+
198+
if _, err := os.Stat(absPath); err != nil {
199+
if os.IsNotExist(err) {
200+
return "", fmt.Errorf("%s file does not exist: %s", fileType, absPath)
201+
}
202+
return "", err
203+
}
204+
205+
return absPath, nil
206+
}

0 commit comments

Comments
 (0)