Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
The diff you're trying to view is too large. We only load the first 3000 changed files.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
extractor/target
runtime-samples/**/target

# Binaries for programs and plugins
*.exe
*.exe~
Expand Down Expand Up @@ -32,7 +35,6 @@ out-*/
.svn
# ignore Maven generated target folders
~
target
# ignore downloaded maven
/tools/maven
/tools/maven.zip
Expand Down
230 changes: 230 additions & 0 deletions docs/prd/prd-0002-tls-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
# PRD: TLS Support for Extractor TCP Server

## Overview

This document describes the changes needed to add TLS encryption to the TCP communication between the Exporter (Go) and the Extractor Server (Rust). TLS is configured by providing a private key and certificate file. When these are not provided, the server runs plain TCP for backward compatibility.

## Motivation

- **Security**: Encrypt communication between the exporter and extractor to protect data in transit, even within the same pod.
- **Compliance**: Meet security requirements for encrypted inter-process communication in production deployments.

---

## Architecture

### Request Flow
```
HTTP Client → Exporter (Go) → Extractor Server (Rust) → Coordinator (Rust) → Fingerprints
GET/POST /gather_runtime_info TLS/TCP 3000 /coordinator
```

### TLS Configuration

| Component | TLS Role | Configuration |
|-----------|----------|---------------|
| Extractor Server (Rust) | TLS Server | `--tls-cert` and `--tls-key` CLI arguments |
| Exporter (Go) | TLS Client | `--tls-cert` CLI flag (CA/server cert for verification) |

Both sides are configured in lockstep. If the Rust server has TLS enabled, the Go exporter must also be configured with TLS.

### TLS Library

**Rust**: `rustls` (pure-Rust TLS implementation) with the `ring` crypto backend.

Rationale:
- ADR-0002 mandates static compilation and minimal dependencies
- `rustls` compiles statically without needing a system OpenSSL library
- The `ring` backend avoids `aws-lc-rs` C build complexity
- Memory-safe, well-audited Rust crate

**Go**: Standard library `crypto/tls` package (no additional dependencies).

---

## Implemented Changes

### 1. Extractor Server (Rust)

**File**: `extractor/src/bin/extractor_server.rs`

#### CLI Arguments

Add two optional arguments to the existing `Args` struct:

```rust
#[arg(long, help = "Path to TLS certificate file (PEM format)")]
tls_cert: Option<String>,

#[arg(long, help = "Path to TLS private key file (PEM format)")]
tls_key: Option<String>,
```

Validation: both must be provided together or neither. Exit with error if only one is given.

#### TLS Initialization

When both `--tls-cert` and `--tls-key` are provided:

1. Load PEM certificate and private key files using `rustls-pemfile`
2. Build a `rustls::ServerConfig` with no client authentication
3. Wrap each accepted `TcpStream` in a `rustls::StreamOwned` before handling

When neither is provided, accept plain TCP connections as before.

#### Refactor `handle_trigger_extraction`

Change the function to accept `impl Read + Write` instead of `TcpStream` so it works with both plain TCP and TLS streams. Move the `stream.shutdown(Shutdown::Both)` call to the caller level (TCP-specific).

---

### 2. Rust Dependencies

**File**: `extractor/Cargo.toml`

Add:
```toml
rustls = { version = "0.23", default-features = false, features = ["ring", "logging", "std"] }
rustls-pemfile = "2"
```

TLS 1.3 is enabled by default in `rustls`. TLS 1.2 is not included to enforce modern protocol usage.

Run `cargo vendor` to update `extractor/vendor/` for the offline build.

---

### 3. Exporter (Go)

**File**: `exporter/cmd/exporter/main.go`

#### CLI Flag

Add a `--tls-cert` flag specifying the path to the CA or server certificate for TLS verification.

#### Connection Logic

In `triggerRuntimeInfoExtraction()`:

- If `--tls-cert` is set: load the certificate, create a `tls.Config` with the cert pool, and use `tls.Dial("tcp", EXTRACTOR_ADDRESS, tlsConfig)`
- If not set: use `net.Dial("tcp", EXTRACTOR_ADDRESS)` as before

Go's `tls.Conn` supports `CloseWrite()`, so the existing EOF signaling protocol works unchanged.

---

### 4. Containerfile

**File**: `Containerfile-extractor`

May need to add `perl` to the build dependencies for the `ring` crate's build scripts:
```dockerfile
RUN dnf -y install gcc make wget rust-toolset rustfmt perl
```

---

### 5. Kubernetes Manifests

**File**: `manifests/insights-runtime-extractor.yaml`

When TLS is enabled:

```yaml
containers:
- name: extractor
command:
- /extractor_server
- --tls-cert
- /tls/tls.crt
- --tls-key
- /tls/tls.key
volumeMounts:
- mountPath: /tls
name: tls-certs
readOnly: true
- name: exporter
command:
- /exporter
- --tls-cert
- /tls/tls.crt
volumeMounts:
- mountPath: /tls
name: tls-certs
readOnly: true
volumes:
- name: tls-certs
secret:
secretName: extractor-tls
```

---

## TCP Protocol

The protocol is unchanged. TLS adds an encryption layer but the application-level data flow remains the same:

| Direction | Format | Example |
|-----------|--------|---------|
| Request | Plain text (read until EOF) | `abc123,def456` |
| Response | Plain text (path) | `data/out-1234567890\n` |

EOF is signaled by `CloseWrite()` (Go) which sends a TLS `close_notify` alert. The Rust side detects EOF on `read_to_string()`.

---

## Backward Compatibility

| Component | Backward Compatible | Notes |
|-----------|---------------------|-------|
| Extractor Server | Yes | Plain TCP when `--tls-cert`/`--tls-key` absent |
| Exporter | Yes | Plain TCP when `--tls-cert` absent |
| Coordinator | N/A | No changes needed |
| Kubernetes Manifests | Yes | TLS volume mount is optional |

---

## Security Considerations

- Private key files must be mounted read-only from Kubernetes Secrets
- No client certificate authentication (server-only TLS)
- Certificate lifecycle management (rotation, expiry) is out of scope — handled by external tooling (e.g., cert-manager)
- `rustls` and `ring` are well-audited, memory-safe Rust crates

---

## Design Decisions

1. **`rustls` over OpenSSL**: Pure Rust, static compilation, no system library dependency. Aligns with ADR-0002.

2. **`ring` crypto backend**: Avoids `aws-lc-rs` C build dependency. `ring` includes some C/ASM but is well-supported.

3. **No separate TLS toggle**: Both sides are configured in lockstep. The presence of cert/key parameters enables TLS — no separate enable/disable flag.

4. **No client certificate authentication**: The extractor and exporter run in the same pod. Server-side TLS is sufficient for encrypting the channel.

5. **Generic stream handling**: Refactoring `handle_trigger_extraction` to `impl Read + Write` keeps the code clean and avoids duplicating logic for TLS vs plain TCP.

---

## Files Modified

| File | Changes |
|------|---------|
| `extractor/src/bin/extractor_server.rs` | Add TLS CLI args, TLS initialization, refactor to generic `Read + Write` |
| `extractor/Cargo.toml` | Add `rustls` and `rustls-pemfile` dependencies |
| `extractor/vendor/` | Vendor new dependencies for offline build |
| `exporter/cmd/exporter/main.go` | Add `--tls-cert` flag, use `tls.Dial()` when configured |
| `Containerfile-extractor` | Add `perl` build dependency if needed for `ring` |
| `manifests/insights-runtime-extractor.yaml` | Add TLS secret volume mount and CLI args |

---

## Implementation Order

1. Refactor `handle_trigger_extraction` to generic `Read + Write` (independent, testable change)
2. Add `rustls`/`rustls-pemfile` to `Cargo.toml` and vendor
3. Add TLS initialization and accept logic to `extractor_server.rs`
4. Update Go exporter with TLS client support
5. Update Containerfile and Kubernetes manifests
6. Test with self-signed certificates in both TLS and non-TLS modes
53 changes: 43 additions & 10 deletions exporter/cmd/exporter/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package main
import (
"bufio"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"encoding/json"
"flag"
"fmt"
Expand Down Expand Up @@ -86,29 +88,42 @@ func gatherRuntimeInfo(w http.ResponseWriter, r *http.Request) {
w.Write(response)
}

var tlsCertPath string
var tlsServerName string
var tlsConfig *tls.Config

func triggerRuntimeInfoExtraction(containerIds []string) (string, error) {
conn, err := net.Dial("tcp", EXTRACTOR_ADDRESS)
var conn net.Conn
var err error

if tlsConfig != nil {
conn, err = tls.Dial("tcp", EXTRACTOR_ADDRESS, tlsConfig)
} else {
conn, err = net.Dial("tcp", EXTRACTOR_ADDRESS)
}
if err != nil {
return "", err
}
defer conn.Close()

log.Println("Requesting a new runtime extraction")
tcpConn, ok := conn.(*net.TCPConn)
if !ok {
return "", fmt.Errorf("failed to get TCP connection")
}

// Send comma-separated container IDs (empty string if no specific containers requested)
payload := strings.Join(containerIds, ",")
// write to TCP connection to trigger a runtime extraction
fmt.Fprintf(tcpConn, "%s", payload)
// and close the write side to signal EOF to the server
if err := tcpConn.CloseWrite(); err != nil {
return "", fmt.Errorf("failed to close write side: %w", err)
fmt.Fprintf(conn, "%s", payload)
// close the write side to signal EOF to the server
// Both *net.TCPConn and *tls.Conn support CloseWrite()
type closeWriter interface {
CloseWrite() error
}
if cw, ok := conn.(closeWriter); ok {
if err := cw.CloseWrite(); err != nil {
return "", fmt.Errorf("failed to close write side: %w", err)
}
}

dataPath, err := bufio.NewReader(tcpConn).ReadString('\n')
dataPath, err := bufio.NewReader(conn).ReadString('\n')
if err != nil {
return "", err
}
Expand Down Expand Up @@ -191,9 +206,27 @@ func collectWorkloadPayload(hash bool, dataPath string) (types.NodeRuntimeInfo,

func main() {
bindAddress := flag.String("bind", "127.0.0.1", "Bind address")
flag.StringVar(&tlsCertPath, "tls-cert", "", "Path to TLS certificate file (PEM format) for verifying the extractor server")
flag.StringVar(&tlsServerName, "tls-server-name", "", "Server name for TLS certificate verification (must match a SAN in the server certificate)")

flag.Parse()

if tlsCertPath != "" {
caCert, err := os.ReadFile(tlsCertPath)
if err != nil {
log.Fatalf("Failed to read TLS certificate file: %v", err)
}
caCertPool := x509.NewCertPool()
if !caCertPool.AppendCertsFromPEM(caCert) {
log.Fatal("Failed to parse TLS certificate")
}
tlsConfig = &tls.Config{
RootCAs: caCertPool,
ServerName: tlsServerName,
}
log.Printf("TLS enabled with cert=%s", tlsCertPath)
}

http.HandleFunc("/gather_runtime_info", gatherRuntimeInfo)

address := *bindAddress + ":8000"
Expand Down
Loading