feat: add mTLS support and TLS directory configuration for ha-gateway and discord-bot
This commit is contained in:
parent
c918c8674a
commit
c581e79434
@ -39,6 +39,7 @@ func main() {
|
|||||||
"version", version,
|
"version", version,
|
||||||
"ha_gateway_addr", cfg.HAGatewayAddr,
|
"ha_gateway_addr", cfg.HAGatewayAddr,
|
||||||
"discord_token", redactToken(cfg.DiscordToken),
|
"discord_token", redactToken(cfg.DiscordToken),
|
||||||
|
"tls_dir", cfg.TLSDir,
|
||||||
"log_level", cfg.LogLevel,
|
"log_level", cfg.LogLevel,
|
||||||
"log_format", cfg.LogFormat,
|
"log_format", cfg.LogFormat,
|
||||||
)
|
)
|
||||||
@ -59,7 +60,7 @@ func main() {
|
|||||||
log.Debug("telemetry disabled")
|
log.Debug("telemetry disabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
haClient, err := gateway.New(ctx, cfg.HAGatewayAddr, log)
|
haClient, err := gateway.New(ctx, cfg.HAGatewayAddr, cfg.TLSDir, log)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("ha-gateway client setup failed", "err", err)
|
log.Error("ha-gateway client setup failed", "err", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|||||||
@ -2,8 +2,12 @@ package gateway
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.nik4nao.com/nik/home-services/discord-bot/internal/core/ports/driven"
|
"gitea.nik4nao.com/nik/home-services/discord-bot/internal/core/ports/driven"
|
||||||
@ -11,6 +15,7 @@ import (
|
|||||||
hav1 "gitea.nik4nao.com/nik/home-services/gen/ha/v1"
|
hav1 "gitea.nik4nao.com/nik/home-services/gen/ha/v1"
|
||||||
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
|
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/credentials"
|
||||||
"google.golang.org/grpc/credentials/insecure"
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -23,10 +28,19 @@ type Client struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New constructs a gRPC client for the internal ha-gateway service.
|
// New constructs a gRPC client for the internal ha-gateway service.
|
||||||
func New(ctx context.Context, addr string, log *slog.Logger) (*Client, error) {
|
func New(ctx context.Context, addr, tlsDir string, log *slog.Logger) (*Client, error) {
|
||||||
|
transportCreds := insecure.NewCredentials()
|
||||||
|
if tlsDir != "" {
|
||||||
|
creds, err := loadTransportCredentials(tlsDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load mTLS credentials: %w", err)
|
||||||
|
}
|
||||||
|
transportCreds = creds
|
||||||
|
}
|
||||||
|
|
||||||
conn, err := grpc.NewClient(
|
conn, err := grpc.NewClient(
|
||||||
addr,
|
addr,
|
||||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
grpc.WithTransportCredentials(transportCreds),
|
||||||
grpc.WithStatsHandler(otelgrpc.NewClientHandler()),
|
grpc.WithStatsHandler(otelgrpc.NewClientHandler()),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -49,6 +63,33 @@ func (c *Client) Close() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadTransportCredentials(tlsDir string) (credentials.TransportCredentials, error) {
|
||||||
|
cert, err := tls.LoadX509KeyPair(
|
||||||
|
filepath.Join(tlsDir, "tls.crt"),
|
||||||
|
filepath.Join(tlsDir, "tls.key"),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load client key pair: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
caPEM, err := os.ReadFile(filepath.Join(tlsDir, "ca.crt"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read server CA: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rootCAs := x509.NewCertPool()
|
||||||
|
if !rootCAs.AppendCertsFromPEM(caPEM) {
|
||||||
|
return nil, fmt.Errorf("append server CA: invalid PEM")
|
||||||
|
}
|
||||||
|
|
||||||
|
return credentials.NewTLS(&tls.Config{
|
||||||
|
Certificates: []tls.Certificate{cert},
|
||||||
|
RootCAs: rootCAs,
|
||||||
|
ServerName: "ha-gateway.home-services.svc.cluster.local",
|
||||||
|
MinVersion: tls.VersionTLS13,
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
|
||||||
// ListLights calls ha-gateway discovery RPCs and maps protobuf messages into
|
// ListLights calls ha-gateway discovery RPCs and maps protobuf messages into
|
||||||
// the driven port type expected by the app layer.
|
// the driven port type expected by the app layer.
|
||||||
func (c *Client) ListLights(ctx context.Context) ([]driven.Light, error) {
|
func (c *Client) ListLights(ctx context.Context) ([]driven.Light, error) {
|
||||||
|
|||||||
@ -10,6 +10,7 @@ type Config struct {
|
|||||||
DiscordToken string
|
DiscordToken string
|
||||||
GuildID string
|
GuildID string
|
||||||
HAGatewayAddr string
|
HAGatewayAddr string
|
||||||
|
TLSDir string
|
||||||
OTELEndpoint string
|
OTELEndpoint string
|
||||||
LogLevel string
|
LogLevel string
|
||||||
LogFormat string
|
LogFormat string
|
||||||
@ -31,6 +32,7 @@ func Load() (*Config, error) {
|
|||||||
DiscordToken: token,
|
DiscordToken: token,
|
||||||
GuildID: os.Getenv("GUILD_ID"),
|
GuildID: os.Getenv("GUILD_ID"),
|
||||||
HAGatewayAddr: addr,
|
HAGatewayAddr: addr,
|
||||||
|
TLSDir: os.Getenv("TLS_DIR"),
|
||||||
OTELEndpoint: os.Getenv("OTEL_ENDPOINT"),
|
OTELEndpoint: os.Getenv("OTEL_ENDPOINT"),
|
||||||
LogLevel: getenvDefault("LOG_LEVEL", "info"),
|
LogLevel: getenvDefault("LOG_LEVEL", "info"),
|
||||||
LogFormat: getenvDefault("LOG_FORMAT", "json"),
|
LogFormat: getenvDefault("LOG_FORMAT", "json"),
|
||||||
|
|||||||
@ -2,14 +2,19 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
|
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/credentials"
|
||||||
"google.golang.org/grpc/health"
|
"google.golang.org/grpc/health"
|
||||||
grpc_health_v1 "google.golang.org/grpc/health/grpc_health_v1"
|
grpc_health_v1 "google.golang.org/grpc/health/grpc_health_v1"
|
||||||
"google.golang.org/grpc/reflection"
|
"google.golang.org/grpc/reflection"
|
||||||
@ -51,6 +56,7 @@ func main() {
|
|||||||
"grpc_port", cfg.GRPCPort,
|
"grpc_port", cfg.GRPCPort,
|
||||||
"ha_base_url", cfg.HABaseURL,
|
"ha_base_url", cfg.HABaseURL,
|
||||||
"ha_token", redactToken(cfg.HAToken),
|
"ha_token", redactToken(cfg.HAToken),
|
||||||
|
"tls_dir", cfg.TLSDir,
|
||||||
"otel_endpoint", cfg.OTELEndpoint,
|
"otel_endpoint", cfg.OTELEndpoint,
|
||||||
"log_level", cfg.LogLevel,
|
"log_level", cfg.LogLevel,
|
||||||
"log_format", cfg.LogFormat,
|
"log_format", cfg.LogFormat,
|
||||||
@ -86,11 +92,24 @@ func main() {
|
|||||||
log.Warn("initial switch discovery failed, will retry on first request", "err", err)
|
log.Warn("initial switch discovery failed, will retry on first request", "err", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
srv := grpc.NewServer(
|
serverOpts := []grpc.ServerOption{
|
||||||
grpc.StatsHandler(otelgrpc.NewServerHandler()),
|
grpc.StatsHandler(otelgrpc.NewServerHandler()),
|
||||||
grpc.ChainUnaryInterceptor(grpcadapter.LoggingUnaryInterceptor(log)),
|
grpc.ChainUnaryInterceptor(grpcadapter.LoggingUnaryInterceptor(log)),
|
||||||
grpc.ChainStreamInterceptor(grpcadapter.LoggingStreamInterceptor(log)),
|
grpc.ChainStreamInterceptor(grpcadapter.LoggingStreamInterceptor(log)),
|
||||||
)
|
}
|
||||||
|
if cfg.TLSDir != "" {
|
||||||
|
creds, err := loadServerCredentials(cfg.TLSDir)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("load mTLS credentials failed", "tls_dir", cfg.TLSDir, "err", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
serverOpts = append(serverOpts, grpc.Creds(creds))
|
||||||
|
log.Info("mTLS enabled", "tls_dir", cfg.TLSDir)
|
||||||
|
} else {
|
||||||
|
log.Info("mTLS disabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
srv := grpc.NewServer(serverOpts...)
|
||||||
healthSrv := health.NewServer()
|
healthSrv := health.NewServer()
|
||||||
healthSrv.SetServingStatus("", grpc_health_v1.HealthCheckResponse_SERVING)
|
healthSrv.SetServingStatus("", grpc_health_v1.HealthCheckResponse_SERVING)
|
||||||
|
|
||||||
@ -137,3 +156,30 @@ func redactToken(token string) string {
|
|||||||
}
|
}
|
||||||
return token[:8] + "..."
|
return token[:8] + "..."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadServerCredentials(tlsDir string) (credentials.TransportCredentials, error) {
|
||||||
|
cert, err := tls.LoadX509KeyPair(
|
||||||
|
filepath.Join(tlsDir, "tls.crt"),
|
||||||
|
filepath.Join(tlsDir, "tls.key"),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load server key pair: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
caPEM, err := os.ReadFile(filepath.Join(tlsDir, "ca.crt"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read client CA: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
clientCAs := x509.NewCertPool()
|
||||||
|
if !clientCAs.AppendCertsFromPEM(caPEM) {
|
||||||
|
return nil, fmt.Errorf("append client CA: invalid PEM")
|
||||||
|
}
|
||||||
|
|
||||||
|
return credentials.NewTLS(&tls.Config{
|
||||||
|
Certificates: []tls.Certificate{cert},
|
||||||
|
ClientCAs: clientCAs,
|
||||||
|
ClientAuth: tls.RequireAndVerifyClientCert,
|
||||||
|
MinVersion: tls.VersionTLS13,
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
|||||||
@ -10,6 +10,7 @@ type Config struct {
|
|||||||
GRPCPort string // GRPC_PORT, default "50051"
|
GRPCPort string // GRPC_PORT, default "50051"
|
||||||
HABaseURL string // HA_BASE_URL, e.g. "http://ha.home.arpa:8123"
|
HABaseURL string // HA_BASE_URL, e.g. "http://ha.home.arpa:8123"
|
||||||
HAToken string // HA_TOKEN — long-lived access token (required)
|
HAToken string // HA_TOKEN — long-lived access token (required)
|
||||||
|
TLSDir string // TLS_DIR, empty disables mTLS for local dev
|
||||||
OTELEndpoint string // OTEL_ENDPOINT, e.g. "otel-collector.monitoring.svc:4317"
|
OTELEndpoint string // OTEL_ENDPOINT, e.g. "otel-collector.monitoring.svc:4317"
|
||||||
LogLevel string // LOG_LEVEL, default "info"
|
LogLevel string // LOG_LEVEL, default "info"
|
||||||
LogFormat string // LOG_FORMAT, default "json"
|
LogFormat string // LOG_FORMAT, default "json"
|
||||||
@ -32,6 +33,7 @@ func Load() (*Config, error) {
|
|||||||
GRPCPort: port,
|
GRPCPort: port,
|
||||||
HABaseURL: os.Getenv("HA_BASE_URL"),
|
HABaseURL: os.Getenv("HA_BASE_URL"),
|
||||||
HAToken: token,
|
HAToken: token,
|
||||||
|
TLSDir: os.Getenv("TLS_DIR"),
|
||||||
OTELEndpoint: os.Getenv("OTEL_ENDPOINT"),
|
OTELEndpoint: os.Getenv("OTEL_ENDPOINT"),
|
||||||
LogLevel: getenvDefault("LOG_LEVEL", "info"),
|
LogLevel: getenvDefault("LOG_LEVEL", "info"),
|
||||||
LogFormat: getenvDefault("LOG_FORMAT", "json"),
|
LogFormat: getenvDefault("LOG_FORMAT", "json"),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user