Skip to content

Commit e7c8dbc

Browse files
Merge pull request #26 from progressive-kiwi/feat-mtls-support
Feat: mTLS support
2 parents 72e0adc + d28e3ca commit e7c8dbc

File tree

9 files changed

+290
-31
lines changed

9 files changed

+290
-31
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
newt
22
.DS_Store
3-
bin/
3+
bin/
4+
.idea
5+
*.iml
6+
certs/

README.md

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,9 @@ When Newt receives WireGuard control messages, it will use the information encod
3737
- `dns`: DNS server to use to resolve the endpoint
3838
- `log-level` (optional): The log level to use. Default: INFO
3939
- `updown` (optional): A script to be called when targets are added or removed.
40-
41-
Example:
40+
- `tls-client-cert` (optional): Client certificate (p12 or pfx) for mTLS. See [mTLS](#mtls)
41+
42+
- Example:
4243

4344
```bash
4445
./newt \
@@ -107,6 +108,38 @@ Returning a string from the script in the format of a target (`ip:dst` so `10.0.
107108

108109
You can look at updown.py as a reference script to get started!
109110

111+
### mTLS
112+
Newt supports mutual TLS (mTLS) authentication, if the server has been configured to request a client certificate.
113+
* Only PKCS12 (.p12 or .pfx) file format is accepted
114+
* The PKCS12 file must contain:
115+
* Private key
116+
* Public certificate
117+
* CA certificate
118+
* Encrypted PKCS12 files are currently not supported
119+
120+
Examples:
121+
122+
```bash
123+
./newt \
124+
--id 31frd0uzbjvp721 \
125+
--secret h51mmlknrvrwv8s4r1i210azhumt6isgbpyavxodibx1k2d6 \
126+
--endpoint https://example.com \
127+
--tls-client-cert ./client.p12
128+
```
129+
130+
```yaml
131+
services:
132+
newt:
133+
image: fosrl/newt
134+
container_name: newt
135+
restart: unless-stopped
136+
environment:
137+
- PANGOLIN_ENDPOINT=https://example.com
138+
- NEWT_ID=2ix2t8xk22ubpfy
139+
- NEWT_SECRET=nnisrfsdfc7prqsp9ewo1dvtvci50j5uiqotez00dgap0ii2
140+
- TLS_CLIENT_CERT=./client.p12
141+
```
142+
110143
## Build
111144
112145
### Container

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ require (
1010
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
1111
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
1212
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259
13+
software.sslmate.com/src/go-pkcs12 v0.5.0
1314
)
1415

1516
require (

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,5 @@ golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvY
2020
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80=
2121
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ=
2222
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY=
23+
software.sslmate.com/src/go-pkcs12 v0.5.0 h1:EC6R394xgENTpZ4RltKydeDUjtlM5drOYIG9c6TVj2M=
24+
software.sslmate.com/src/go-pkcs12 v0.5.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=

main.go

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -340,16 +340,17 @@ func resolveDomain(domain string) (string, error) {
340340
}
341341

342342
var (
343-
endpoint string
344-
id string
345-
secret string
346-
mtu string
347-
mtuInt int
348-
dns string
349-
privateKey wgtypes.Key
350-
err error
351-
logLevel string
352-
updownScript string
343+
endpoint string
344+
id string
345+
secret string
346+
mtu string
347+
mtuInt int
348+
dns string
349+
privateKey wgtypes.Key
350+
err error
351+
logLevel string
352+
updownScript string
353+
tlsPrivateKey string
353354
)
354355

355356
func main() {
@@ -361,6 +362,7 @@ func main() {
361362
dns = os.Getenv("DNS")
362363
logLevel = os.Getenv("LOG_LEVEL")
363364
updownScript = os.Getenv("UPDOWN_SCRIPT")
365+
tlsPrivateKey = os.Getenv("TLS_CLIENT_CERT")
364366

365367
if endpoint == "" {
366368
flag.StringVar(&endpoint, "endpoint", "", "Endpoint of your pangolin server")
@@ -383,6 +385,9 @@ func main() {
383385
if updownScript == "" {
384386
flag.StringVar(&updownScript, "updown", "", "Path to updown script to be called when targets are added or removed")
385387
}
388+
if tlsPrivateKey == "" {
389+
flag.StringVar(&tlsPrivateKey, "tls-client-cert", "", "Path to client certificate used for mTLS")
390+
}
386391

387392
// do a --version check
388393
version := flag.Bool("version", false, "Print the version")
@@ -408,12 +413,16 @@ func main() {
408413
if err != nil {
409414
logger.Fatal("Failed to generate private key: %v", err)
410415
}
411-
416+
var opt websocket.ClientOption
417+
if tlsPrivateKey != "" {
418+
opt = websocket.WithTLSConfig(tlsPrivateKey)
419+
}
412420
// Create a new client
413421
client, err := websocket.NewClient(
414422
id, // CLI arg takes precedence
415423
secret, // CLI arg takes precedence
416424
endpoint,
425+
opt,
417426
)
418427
if err != nil {
419428
logger.Fatal("Failed to create client: %v", err)
@@ -642,10 +651,13 @@ persistent_keepalive_interval=5`, fixKey(fmt.Sprintf("%s", privateKey)), fixKey(
642651
// Wait for interrupt signal
643652
sigCh := make(chan os.Signal, 1)
644653
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
645-
<-sigCh
654+
sigReceived := <-sigCh
646655

647656
// Cleanup
648-
dev.Close()
657+
logger.Info("Received %s signal, stopping", sigReceived.String())
658+
if dev != nil {
659+
dev.Close()
660+
}
649661
}
650662

651663
func parseTargetData(data interface{}) (TargetData, error) {

self-signed-certs-for-mtls.sh

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
#!/usr/bin/env bash
2+
set -eu
3+
4+
echo -n "Enter username for certs (eg alice): "
5+
read CERT_USERNAME
6+
echo
7+
8+
echo -n "Enter domain of user (eg example.com): "
9+
read DOMAIN
10+
echo
11+
12+
# Prompt for password at the start
13+
echo -n "Enter password for certificate: "
14+
read -s PASSWORD
15+
echo
16+
echo -n "Confirm password: "
17+
read -s PASSWORD2
18+
echo
19+
20+
if [ "$PASSWORD" != "$PASSWORD2" ]; then
21+
echo "Passwords don't match!"
22+
exit 1
23+
fi
24+
CA_DIR="./certs/ca"
25+
CLIENT_DIR="./certs/clients"
26+
FILE_PREFIX=$(echo "$CERT_USERNAME-at-$DOMAIN" | sed 's/\./-/')
27+
28+
mkdir -p "$CA_DIR"
29+
mkdir -p "$CLIENT_DIR"
30+
31+
if [ ! -f "$CA_DIR/ca.crt" ]; then
32+
# Generate CA private key
33+
openssl genrsa -out "$CA_DIR/ca.key" 4096
34+
echo "CA key ✅"
35+
36+
# Generate CA root certificate
37+
openssl req -x509 -new -nodes \
38+
-key "$CA_DIR/ca.key" \
39+
-sha256 \
40+
-days 3650 \
41+
-out "$CA_DIR/ca.crt" \
42+
-subj "/C=US/ST=State/L=City/O=Organization/OU=Unit/CN=ca.$DOMAIN"
43+
44+
echo "CA cert ✅"
45+
fi
46+
47+
# Generate client private key
48+
openssl genrsa -aes256 -passout pass:"$PASSWORD" -out "$CLIENT_DIR/$FILE_PREFIX.key" 2048
49+
echo "Client key ✅"
50+
51+
# Generate client Certificate Signing Request (CSR)
52+
openssl req -new \
53+
-key "$CLIENT_DIR/$FILE_PREFIX.key" \
54+
-out "$CLIENT_DIR/$FILE_PREFIX.csr" \
55+
-passin pass:"$PASSWORD" \
56+
-subj "/C=US/ST=State/L=City/O=Organization/OU=Unit/CN=$CERT_USERNAME@$DOMAIN"
57+
echo "Client cert ✅"
58+
59+
echo -n "Signing client cert..."
60+
# Create client certificate configuration file
61+
cat > "$CLIENT_DIR/$FILE_PREFIX.ext" << EOF
62+
authorityKeyIdentifier=keyid,issuer
63+
basicConstraints=CA:FALSE
64+
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
65+
subjectAltName = @alt_names
66+
67+
[alt_names]
68+
DNS.1 = $DOMAIN
69+
EOF
70+
71+
# Generate client certificate signed by CA
72+
openssl x509 -req \
73+
-in "$CLIENT_DIR/$FILE_PREFIX.csr" \
74+
-CA "$CA_DIR/ca.crt" \
75+
-CAkey "$CA_DIR/ca.key" \
76+
-CAcreateserial \
77+
-out "$CLIENT_DIR/$FILE_PREFIX.crt" \
78+
-days 365 \
79+
-sha256 \
80+
-extfile "$CLIENT_DIR/$FILE_PREFIX.ext"
81+
82+
# Verify the client certificate
83+
openssl verify -CAfile "$CA_DIR/ca.crt" "$CLIENT_DIR/$FILE_PREFIX.crt"
84+
echo "Signed ✅"
85+
86+
# Create encrypted PEM bundle
87+
openssl rsa -in "$CLIENT_DIR/$FILE_PREFIX.key" -passin pass:"$PASSWORD" \
88+
| cat "$CLIENT_DIR/$FILE_PREFIX.crt" - > "$CLIENT_DIR/$FILE_PREFIX-bundle.enc.pem"
89+
90+
91+
# Convert to PKCS12
92+
echo "Converting to PKCS12 format..."
93+
openssl pkcs12 -export \
94+
-out "$CLIENT_DIR/$FILE_PREFIX.enc.p12" \
95+
-inkey "$CLIENT_DIR/$FILE_PREFIX.key" \
96+
-in "$CLIENT_DIR/$FILE_PREFIX.crt" \
97+
-certfile "$CA_DIR/ca.crt" \
98+
-name "$CERT_USERNAME@$DOMAIN" \
99+
-passin pass:"$PASSWORD" \
100+
-passout pass:"$PASSWORD"
101+
echo "Converted to encrypted p12 for macOS ✅"
102+
103+
# Convert to PKCS12 format without encryption
104+
echo "Converting to non-encrypted PKCS12 format..."
105+
openssl pkcs12 -export \
106+
-out "$CLIENT_DIR/$FILE_PREFIX.p12" \
107+
-inkey "$CLIENT_DIR/$FILE_PREFIX.key" \
108+
-in "$CLIENT_DIR/$FILE_PREFIX.crt" \
109+
-certfile "$CA_DIR/ca.crt" \
110+
-name "$CERT_USERNAME@$DOMAIN" \
111+
-passin pass:"$PASSWORD" \
112+
-passout pass:""
113+
echo "Converted to non-encrypted p12 ✅"
114+
115+
# Clean up intermediate files
116+
rm "$CLIENT_DIR/$FILE_PREFIX.csr" "$CLIENT_DIR/$FILE_PREFIX.ext" "$CA_DIR/ca.srl"
117+
echo
118+
echo
119+
120+
echo "CA certificate: $CA_DIR/ca.crt"
121+
echo "CA private key: $CA_DIR/ca.key"
122+
echo "Client certificate: $CLIENT_DIR/$FILE_PREFIX.crt"
123+
echo "Client private key: $CLIENT_DIR/$FILE_PREFIX.key"
124+
echo "Client cert bundle: $CLIENT_DIR/$FILE_PREFIX.p12"
125+
echo "Client cert bundle (encrypted): $CLIENT_DIR/$FILE_PREFIX.enc.p12"

0 commit comments

Comments
 (0)