Skip to content

Commit dbb2c35

Browse files
ricardomaraschiniCraig O'Donnell
andauthored
feat: add tls to internal registry (#630)
* feat: add tls to internal registry this pr adds tls configuration to the registry add-on. certs are valid for 1 year and there is no rotation (this will be implemented later). containerd configuration to access the registry through http was removed. * chore: bump kots * use ECO v0.32.0 and remove ttl override * bump ECO go mod to 0.32.0 too * Revert "bump ECO go mod to 0.32.0 too" This reverts commit cc94f26. * eco 0.32.1 * eco 0.32.2 * label registry-tls secret for DR --------- Co-authored-by: Craig O'Donnell <craig@replicated.com>
1 parent 67d9de8 commit dbb2c35

File tree

7 files changed

+322
-24
lines changed

7 files changed

+322
-24
lines changed

Makefile

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ ARCH := $(shell uname -m)
44
APP_NAME = embedded-cluster
55
ADMIN_CONSOLE_CHART_URL = oci://registry.replicated.com/library
66
ADMIN_CONSOLE_CHART_NAME = admin-console
7-
ADMIN_CONSOLE_CHART_VERSION = 1.109.4-build.1
7+
ADMIN_CONSOLE_CHART_VERSION = 1.109.5
88
ADMIN_CONSOLE_IMAGE_OVERRIDE =
99
ADMIN_CONSOLE_MIGRATIONS_IMAGE_OVERRIDE =
1010
EMBEDDED_OPERATOR_CHART_URL = oci://registry.replicated.com/library
1111
EMBEDDED_OPERATOR_CHART_NAME = embedded-cluster-operator
12-
EMBEDDED_OPERATOR_CHART_VERSION = 0.31.1
12+
EMBEDDED_OPERATOR_CHART_VERSION = 0.32.2
1313
EMBEDDED_OPERATOR_UTILS_IMAGE = busybox:1.36.1
1414
EMBEDDED_CLUSTER_OPERATOR_IMAGE_OVERRIDE =
1515
OPENEBS_CHART_URL = https://openebs.github.io/openebs
@@ -31,6 +31,7 @@ PREVIOUS_K0S_VERSION ?= v1.28.9+k0s.0
3131
K0S_BINARY_SOURCE_OVERRIDE =
3232
TROUBLESHOOT_VERSION = v0.92.1
3333
KOTS_VERSION = v$(shell echo $(ADMIN_CONSOLE_CHART_VERSION) | sed 's/\([0-9]\+\.[0-9]\+\.[0-9]\+\).*/\1/')
34+
KOTS_BINARY_URL_OVERRIDE =
3435
LD_FLAGS = -X github.com/replicatedhq/embedded-cluster/pkg/defaults.K0sVersion=$(K0S_VERSION) \
3536
-X github.com/replicatedhq/embedded-cluster/pkg/defaults.Version=$(VERSION) \
3637
-X github.com/replicatedhq/embedded-cluster/pkg/defaults.K0sBinaryURL=$(K0S_BINARY_SOURCE_OVERRIDE) \
@@ -102,7 +103,11 @@ pkg/goods/bins/local-artifact-mirror: Makefile
102103
pkg/goods/internal/bins/kubectl-kots: Makefile
103104
mkdir -p pkg/goods/internal/bins
104105
mkdir -p output/tmp/kots
105-
curl -L -o output/tmp/kots/kots.tar.gz https://github.com/replicatedhq/kots/releases/download/$(KOTS_VERSION)/kots_linux_amd64.tar.gz
106+
if [ "$(KOTS_BINARY_URL_OVERRIDE)" != "" ]; then \
107+
curl -L -o output/tmp/kots/kots.tar.gz "$(KOTS_BINARY_URL_OVERRIDE)" ; \
108+
else \
109+
curl -L -o output/tmp/kots/kots.tar.gz https://github.com/replicatedhq/kots/releases/download/$(KOTS_VERSION)/kots_linux_amd64.tar.gz ; \
110+
fi
106111
tar -xzf output/tmp/kots/kots.tar.gz -C output/tmp/kots
107112
mv output/tmp/kots/kots pkg/goods/internal/bins/kubectl-kots
108113
touch pkg/goods/internal/bins/kubectl-kots

cmd/local-artifact-mirror/artifact.go

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@ package main
22

33
import (
44
"context"
5+
"crypto/tls"
56
"encoding/json"
67
"fmt"
8+
"net/http"
79
"os"
810

911
"github.com/sirupsen/logrus"
12+
"go.uber.org/multierr"
1013
corev1 "k8s.io/api/core/v1"
1114
"k8s.io/apimachinery/pkg/api/errors"
1215
"k8s.io/apimachinery/pkg/types"
@@ -97,14 +100,32 @@ func pullArtifact(ctx context.Context, from string) (string, error) {
97100
}
98101
defer fs.Close()
99102

100-
// setup the pull options. XXX we are using a registry over http.
101-
repo.Client = &auth.Client{Credential: store.Get}
102-
repo.PlainHTTP = true
103+
transp, ok := http.DefaultTransport.(*http.Transport)
104+
if !ok {
105+
return "", fmt.Errorf("unable to get default transport")
106+
}
107+
108+
transp = transp.Clone()
109+
transp.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
110+
repo.Client = &auth.Client{
111+
Client: &http.Client{Transport: transp},
112+
Credential: store.Get,
113+
}
103114

104115
tag := imgref.Reference
116+
_, tlserr := oras.Copy(ctx, repo, tag, fs, tag, oras.DefaultCopyOptions)
117+
if tlserr == nil {
118+
return tmpdir, nil
119+
}
120+
121+
// if we fail to fetch the artifact using https we gonna try once more using plain
122+
// http as some versions of the registry were deployed without tls.
123+
repo.PlainHTTP = true
124+
logrus.Infof("unable to fetch artifact using tls, retrying with http")
105125
if _, err := oras.Copy(ctx, repo, tag, fs, tag, oras.DefaultCopyOptions); err != nil {
106126
os.RemoveAll(tmpdir)
107-
return "", fmt.Errorf("unable to copy: %w", err)
127+
err = multierr.Combine(tlserr, err)
128+
return "", fmt.Errorf("unable to fetch artifacts with or without tls: %w", err)
108129
}
109130
return tmpdir, nil
110131
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ require (
132132
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect
133133
github.com/zitadel/oidc/v2 v2.7.0 // indirect
134134
go.mongodb.org/mongo-driver v1.11.3 // indirect
135-
go.uber.org/multierr v1.11.0 // indirect
135+
go.uber.org/multierr v1.11.0
136136
go.uber.org/zap v1.27.0 // indirect
137137
golang.org/x/net v0.25.0 // indirect
138138
golang.org/x/oauth2 v0.18.0 // indirect

pkg/addons/registry/registry.go

Lines changed: 72 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,27 @@ package registry
33
import (
44
"context"
55
"fmt"
6+
"time"
67

78
"github.com/k0sproject/k0s/pkg/apis/k0s/v1beta1"
89
"github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
910
"golang.org/x/crypto/bcrypt"
1011
"gopkg.in/yaml.v2"
1112
corev1 "k8s.io/api/core/v1"
1213
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
14+
"k8s.io/apimachinery/pkg/types"
1315
"sigs.k8s.io/controller-runtime/pkg/client"
1416

1517
"github.com/replicatedhq/embedded-cluster/pkg/airgap"
18+
"github.com/replicatedhq/embedded-cluster/pkg/certs"
1619
"github.com/replicatedhq/embedded-cluster/pkg/helpers"
1720
"github.com/replicatedhq/embedded-cluster/pkg/kubeutils"
1821
"github.com/replicatedhq/embedded-cluster/pkg/spinner"
1922
)
2023

2124
const (
22-
releaseName = "docker-registry"
25+
releaseName = "docker-registry"
26+
tlsSecretName = "registry-tls"
2327
)
2428

2529
// Overwritten by -ldflags in Makefile
@@ -134,6 +138,10 @@ func (o *Registry) GenerateHelmConfig(onlyDefaults bool) ([]v1beta1.Chart, []v1b
134138
"clusterIP": registryServiceIP.String(),
135139
}
136140

141+
if !onlyDefaults {
142+
helmValues["tlsSecretName"] = tlsSecretName
143+
}
144+
137145
valuesStringData, err := yaml.Marshal(helmValues)
138146
if err != nil {
139147
return nil, nil, fmt.Errorf("unable to marshal helm values: %w", err)
@@ -147,6 +155,34 @@ func (o *Registry) GetAdditionalImages() []string {
147155
return nil
148156
}
149157

158+
func (o *Registry) generateRegistryTLS(ctx context.Context, cli client.Client) (string, string, error) {
159+
nsn := types.NamespacedName{Name: "registry", Namespace: o.namespace}
160+
var svc corev1.Service
161+
if err := cli.Get(ctx, nsn, &svc); err != nil {
162+
return "", "", fmt.Errorf("unable to get registry service: %w", err)
163+
}
164+
165+
opts := []certs.Option{
166+
certs.WithCommonName("registry"),
167+
certs.WithDuration(365 * 24 * time.Hour),
168+
certs.WithIPAddress(registryAddress),
169+
}
170+
171+
for _, name := range []string{
172+
"registry",
173+
fmt.Sprintf("registry.%s.svc", o.namespace),
174+
fmt.Sprintf("registry.%s.svc.cluster.local", o.namespace),
175+
} {
176+
opts = append(opts, certs.WithDNSName(name))
177+
}
178+
179+
builder, err := certs.NewBuilder(opts...)
180+
if err != nil {
181+
return "", "", fmt.Errorf("failed to create cert builder: %w", err)
182+
}
183+
return builder.Generate()
184+
}
185+
150186
// Outro is executed after the cluster deployment.
151187
func (o *Registry) Outro(ctx context.Context, cli client.Client) error {
152188
if !o.isAirgap {
@@ -157,13 +193,13 @@ func (o *Registry) Outro(ctx context.Context, cli client.Client) error {
157193
loading.Infof("Waiting for Registry to be ready")
158194

159195
if err := kubeutils.WaitForNamespace(ctx, cli, o.namespace); err != nil {
160-
loading.Close()
196+
loading.CloseWithError()
161197
return err
162198
}
163199

164200
hashPassword, err := bcrypt.GenerateFromPassword([]byte(registryPassword), bcrypt.DefaultCost)
165201
if err != nil {
166-
loading.Close()
202+
loading.CloseWithError()
167203
return fmt.Errorf("unable to hash registry password: %w", err)
168204
}
169205

@@ -186,27 +222,51 @@ func (o *Registry) Outro(ctx context.Context, cli client.Client) error {
186222
}
187223
err = cli.Create(ctx, &htpasswd)
188224
if err != nil {
189-
loading.Close()
225+
loading.CloseWithError()
190226
return fmt.Errorf("unable to create registry-auth secret: %w", err)
191227
}
192228

193-
if err := kubeutils.WaitForDeployment(ctx, cli, o.namespace, "registry"); err != nil {
194-
loading.Close()
195-
return err
196-
}
197229
if err := kubeutils.WaitForService(ctx, cli, o.namespace, "registry"); err != nil {
198-
loading.Close()
230+
loading.CloseWithError()
199231
return err
200232
}
201233

202234
if err := InitRegistryClusterIP(ctx, cli, o.namespace); err != nil {
203-
loading.Close()
235+
loading.CloseWithError()
204236
return fmt.Errorf("failed to determine registry cluster IP: %w", err)
205237
}
206238

239+
tlsCert, tlsKey, err := o.generateRegistryTLS(ctx, cli)
240+
if err != nil {
241+
loading.CloseWithError()
242+
return fmt.Errorf("unable to generate registry tls: %w", err)
243+
}
244+
245+
tlsSecret := &corev1.Secret{
246+
TypeMeta: metav1.TypeMeta{Kind: "Secret", APIVersion: "v1"},
247+
ObjectMeta: metav1.ObjectMeta{
248+
Name: tlsSecretName,
249+
Namespace: o.namespace,
250+
Labels: map[string]string{
251+
"app": "docker-registry", // this is the backup/restore label for the registry component
252+
},
253+
},
254+
StringData: map[string]string{"tls.crt": tlsCert, "tls.key": tlsKey},
255+
Type: "Opaque",
256+
}
257+
if err := cli.Create(ctx, tlsSecret); err != nil {
258+
loading.CloseWithError()
259+
return fmt.Errorf("unable to create %s secret: %w", tlsSecretName, err)
260+
}
261+
262+
if err := kubeutils.WaitForDeployment(ctx, cli, o.namespace, "registry"); err != nil {
263+
loading.CloseWithError()
264+
return err
265+
}
266+
207267
if err := airgap.AddInsecureRegistry(fmt.Sprintf("%s:5000", registryAddress)); err != nil {
208-
loading.Close()
209-
return fmt.Errorf("failed to add insecure registry: %w", err)
268+
loading.CloseWithError()
269+
return fmt.Errorf("unable to add containerd registry config: %w", err)
210270
}
211271

212272
loading.Closef("Registry is ready!")

pkg/airgap/containerd.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,6 @@ import (
1010

1111
const registryConfigTemplate = `
1212
[plugins."io.containerd.grpc.v1.cri".registry]
13-
[plugins."io.containerd.grpc.v1.cri".registry.mirrors]
14-
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."%s"]
15-
endpoint = ["http://%s"]
1613
[plugins."io.containerd.grpc.v1.cri".registry.configs]
1714
[plugins."io.containerd.grpc.v1.cri".registry.configs."%s".tls]
1815
insecure_skip_verify = true
@@ -22,7 +19,7 @@ const registryConfigTemplate = `
2219
// are allowed to be accessed over HTTP.
2320
func AddInsecureRegistry(registry string) error {
2421
parentDir := defaults.PathToK0sContainerdConfig()
25-
contents := fmt.Sprintf(registryConfigTemplate, registry, registry, registry)
22+
contents := fmt.Sprintf(registryConfigTemplate, registry)
2623

2724
if err := os.MkdirAll(parentDir, 0755); err != nil {
2825
return fmt.Errorf("failed to ensure containerd directory exists: %w", err)

pkg/certs/certs.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package certs
2+
3+
import (
4+
"bytes"
5+
"crypto/rand"
6+
"crypto/rsa"
7+
"crypto/x509"
8+
"crypto/x509/pkix"
9+
"encoding/pem"
10+
"fmt"
11+
"math/big"
12+
"net"
13+
"time"
14+
)
15+
16+
// NewBuilder returns a new certificate builder.
17+
func NewBuilder(opts ...Option) (*Builder, error) {
18+
builder := &Builder{
19+
organizations: []string{"replicated"},
20+
expiration: time.Now().Add(time.Hour * 24 * 365),
21+
commonName: "localhost",
22+
dnsNames: []string{"localhost"},
23+
ipAddresses: []net.IP{net.ParseIP("127.0.0.1")},
24+
}
25+
for _, opt := range opts {
26+
if err := opt(builder); err != nil {
27+
return nil, fmt.Errorf("unable to apply option: %w", err)
28+
}
29+
}
30+
return builder, nil
31+
}
32+
33+
// Builder is a helper to generate self signed certificates.
34+
type Builder struct {
35+
organizations []string
36+
expiration time.Time
37+
commonName string
38+
dnsNames []string
39+
ipAddresses []net.IP
40+
signBy *SignCA
41+
}
42+
43+
// SignCA holds a path to a key and a certificate we use to sign the new certificate.
44+
type SignCA struct {
45+
crt []byte
46+
key []byte
47+
}
48+
49+
// parse reads and parses the CA certificate and key from disk.
50+
func (s *SignCA) parse() (*x509.Certificate, *rsa.PrivateKey, error) {
51+
caCertBlock, _ := pem.Decode(s.crt)
52+
if caCertBlock == nil {
53+
return nil, nil, fmt.Errorf("unable to decode certificate PEM")
54+
}
55+
caCert, err := x509.ParseCertificate(caCertBlock.Bytes)
56+
if err != nil {
57+
return nil, nil, err
58+
}
59+
60+
caKeyBlock, _ := pem.Decode(s.key)
61+
if caKeyBlock == nil {
62+
return nil, nil, fmt.Errorf("unable to decode key PEM")
63+
}
64+
caKey, err := x509.ParsePKCS1PrivateKey(caKeyBlock.Bytes)
65+
if err != nil {
66+
return nil, nil, err
67+
}
68+
69+
return caCert, caKey, nil
70+
}
71+
72+
// generatePair creates a new key/crt pair.
73+
func (b *Builder) Generate() (string, string, error) {
74+
pkey, err := rsa.GenerateKey(rand.Reader, 2048)
75+
if err != nil {
76+
return "", "", fmt.Errorf("unable to generate private key: %w", err)
77+
}
78+
79+
kusage := x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature
80+
tpl := x509.Certificate{
81+
SerialNumber: big.NewInt(time.Now().UnixNano()),
82+
NotBefore: time.Now(),
83+
NotAfter: b.expiration,
84+
KeyUsage: kusage,
85+
BasicConstraintsValid: true,
86+
IPAddresses: b.ipAddresses,
87+
DNSNames: b.dnsNames,
88+
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
89+
Subject: pkix.Name{
90+
Organization: b.organizations,
91+
CommonName: b.commonName,
92+
},
93+
}
94+
95+
cacert := &tpl
96+
cakey := pkey
97+
if b.signBy != nil {
98+
cacert, cakey, err = b.signBy.parse()
99+
if err != nil {
100+
return "", "", fmt.Errorf("unable to parse signer ca: %w", err)
101+
}
102+
}
103+
104+
cert, err := x509.CreateCertificate(rand.Reader, &tpl, cacert, &pkey.PublicKey, cakey)
105+
if err != nil {
106+
return "", "", fmt.Errorf("unable to create certificate: %w", err)
107+
}
108+
109+
crtbuf := bytes.NewBuffer(nil)
110+
if err := pem.Encode(
111+
crtbuf, &pem.Block{Type: "CERTIFICATE", Bytes: cert},
112+
); err != nil {
113+
return "", "", fmt.Errorf("unable to encode certificate: %w", err)
114+
}
115+
116+
pbytes, err := x509.MarshalPKCS8PrivateKey(pkey)
117+
if err != nil {
118+
return "", "", fmt.Errorf("unable to marshal private key: %w", err)
119+
}
120+
121+
keybuf := bytes.NewBuffer(nil)
122+
if err := pem.Encode(
123+
keybuf, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: pbytes},
124+
); err != nil {
125+
return "", "", fmt.Errorf("unable to encode private key: %w", err)
126+
}
127+
128+
return crtbuf.String(), keybuf.String(), nil
129+
}

0 commit comments

Comments
 (0)