Skip to content

Commit

Permalink
feat: reimplement apid certificate generation on top of COSI
Browse files Browse the repository at this point in the history
This PR can be split into two parts:

* controllers
* apid binding into COSI world

Controllers
-----------

* `k8s.EndpointController` provides control plane endpoints on worker
nodes (it isn't required for now on control plane nodes)
* `secrets.RootController` now provides OS top-level secrets (CA cert)
and secret configuration
* `secrets.APIController` generates API secrets (certificates) in a bit
different way for workers and control plane nodes: controlplane nodes
generate directly, while workers reach out to `trustd` on control plane
nodes via `k8s.Endpoint` resource

apid Binding
------------

Resource `secrets.API` provides binding to protobuf by converting
itself back and forth to protobuf spec.

apid no longer receives machine configuration, instead it receives
gRPC-backed socket to access Resource API. apid watches `secrets.API`
resource, fetches certs and CA from it and uses that in its TLS
configuration.

Signed-off-by: Andrey Smirnov <smirnov.andrey@gmail.com>
  • Loading branch information
smira authored and talos-bot committed Jun 23, 2021
1 parent 3c1b321 commit d8c2bca
Show file tree
Hide file tree
Showing 30 changed files with 1,442 additions and 362 deletions.
3 changes: 3 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@ COPY ./api/cluster/cluster.proto /api/cluster/cluster.proto
RUN protoc -I/api -I/api/vendor/ --go_out=paths=source_relative:/api --go-grpc_out=paths=source_relative:/api cluster/cluster.proto
COPY ./api/resource/resource.proto /api/resource/resource.proto
RUN protoc -I/api -I/api/vendor/ --go_out=paths=source_relative:/api --go-grpc_out=paths=source_relative:/api resource/resource.proto
COPY ./api/resource/secrets/secrets.proto /api/resource/secrets/secrets.proto
RUN protoc -I/api -I/api/vendor/ --go_out=paths=source_relative:/api --go-grpc_out=paths=source_relative:/api resource/secrets/secrets.proto
COPY ./api/inspect/inspect.proto /api/inspect/inspect.proto
RUN protoc -I/api -I/api/vendor/ --go_out=paths=source_relative:/api --go-grpc_out=paths=source_relative:/api inspect/inspect.proto
# Gofumports generated files to adjust import order
Expand All @@ -176,6 +178,7 @@ COPY --from=generate-build /api/network/*.pb.go /pkg/machinery/api/network/
COPY --from=generate-build /api/cluster/*.pb.go /pkg/machinery/api/cluster/
COPY --from=generate-build /api/storage/*.pb.go /pkg/machinery/api/storage/
COPY --from=generate-build /api/resource/*.pb.go /pkg/machinery/api/resource/
COPY --from=generate-build /api/resource/secrets/*.pb.go /pkg/machinery/api/resource/secrets/
COPY --from=generate-build /api/inspect/*.pb.go /pkg/machinery/api/inspect/
COPY --from=go-generate /src/pkg/resources/network/ /pkg/resources/network/
COPY --from=go-generate /src/pkg/machinery/config/types/v1alpha1/ /pkg/machinery/config/types/v1alpha1/
Expand Down
17 changes: 17 additions & 0 deletions api/resource/secrets/secrets.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
syntax = "proto3";

package resource.secrets;

option go_package = "github.com/talos-systems/talos/pkg/machinery/api/resource/secrets";

message CertAndKeyPEM {
bytes cert = 1;
bytes key = 2;
}

// APISpec describes secrets.API.
message APISpec {
bytes ca_pem = 1;
CertAndKeyPEM server = 2;
CertAndKeyPEM client = 3;
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ require (
github.com/containernetworking/plugins v0.9.1
github.com/coreos/go-iptables v0.6.0
github.com/coreos/go-semver v0.3.0
github.com/cosi-project/runtime v0.0.0-20210621171302-3698c5142954
github.com/cosi-project/runtime v0.0.0-20210623125951-f1649aff7641
github.com/docker/distribution v2.7.1+incompatible
github.com/docker/docker v20.10.7+incompatible
github.com/docker/go-connections v0.4.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -305,8 +305,8 @@ github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzA
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cosi-project/runtime v0.0.0-20210621171302-3698c5142954 h1:IvvTxWEugWa0kbkSELltW7idPl35CSZ7Q+M/yJ2tIFs=
github.com/cosi-project/runtime v0.0.0-20210621171302-3698c5142954/go.mod h1:v/3MIWNuuOSdXXMl3QgCSwZrAk1fTOmQHEnTAfvDqP4=
github.com/cosi-project/runtime v0.0.0-20210623125951-f1649aff7641 h1:InlDrG3Vg+wAwA/V9Uts5h+/upC9cNwmoB6kSYehBPg=
github.com/cosi-project/runtime v0.0.0-20210623125951-f1649aff7641/go.mod h1:v/3MIWNuuOSdXXMl3QgCSwZrAk1fTOmQHEnTAfvDqP4=
github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
Expand Down
32 changes: 11 additions & 21 deletions internal/app/apid/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import (
"flag"
"log"
"regexp"
"strings"

"github.com/cosi-project/runtime/api/v1alpha1"
"github.com/cosi-project/runtime/pkg/state"
"github.com/cosi-project/runtime/pkg/state/protobuf/client"
debug "github.com/talos-systems/go-debug"
"github.com/talos-systems/grpc-proxy/proxy"
"golang.org/x/sync/errgroup"
Expand All @@ -23,15 +25,11 @@ import (
"github.com/talos-systems/talos/pkg/grpc/factory"
"github.com/talos-systems/talos/pkg/grpc/middleware/authz"
"github.com/talos-systems/talos/pkg/grpc/proxy/backend"
"github.com/talos-systems/talos/pkg/machinery/config/configloader"
"github.com/talos-systems/talos/pkg/machinery/constants"
"github.com/talos-systems/talos/pkg/startup"
)

var (
endpoints *string
useK8sEndpoints *bool
)
var rbacEnabled *bool

func runDebugServer(ctx context.Context) {
const debugAddr = ":9981"
Expand All @@ -49,8 +47,7 @@ func runDebugServer(ctx context.Context) {
func Main() {
log.SetFlags(log.Lshortfile | log.Ldate | log.Lmicroseconds | log.Ltime)

endpoints = flag.String("endpoints", "", "the static list of IPs of the control plane nodes")
useK8sEndpoints = flag.Bool("use-kubernetes-endpoints", false, "use Kubernetes master node endpoints as control plane endpoints")
rbacEnabled = flag.Bool("enable-rbac", false, "enable RBAC for Talos API")

flag.Parse()

Expand All @@ -60,22 +57,15 @@ func Main() {
log.Fatalf("failed to seed RNG: %v", err)
}

config, err := configloader.NewFromStdin()
runtimeConn, err := grpc.Dial("unix://"+constants.APIRuntimeSocketPath, grpc.WithInsecure())
if err != nil {
log.Fatalf("open config: %v", err)
log.Fatalf("failed to dial runtime connection: %v", err)
}

var endpointsProvider provider.Endpoints

if *useK8sEndpoints {
endpointsProvider = &provider.KubernetesEndpoints{}
} else {
endpointsProvider = &provider.StaticEndpoints{
Endpoints: strings.Split(*endpoints, ","),
}
}
stateClient := v1alpha1.NewStateClient(runtimeConn)
resources := state.WrapCore(client.NewAdapter(stateClient))

tlsConfig, err := provider.NewTLSConfig(config, endpointsProvider)
tlsConfig, err := provider.NewTLSConfig(resources)
if err != nil {
log.Fatalf("failed to create remote certificate provider: %+v", err)
}
Expand Down Expand Up @@ -121,7 +111,7 @@ func Main() {

errGroup.Go(func() error {
mode := authz.Disabled
if config.Machine().Features().RBACEnabled() {
if *rbacEnabled {
mode = authz.Enabled
}

Expand Down
57 changes: 0 additions & 57 deletions internal/app/apid/pkg/provider/endpoints.go

This file was deleted.

150 changes: 150 additions & 0 deletions internal/app/apid/pkg/provider/provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

// Package provider provides TLS config for client & server.
package provider

import (
"context"
stdlibtls "crypto/tls"
"fmt"
"log"
"sync"

"github.com/cosi-project/runtime/pkg/resource"
"github.com/cosi-project/runtime/pkg/state"
"github.com/talos-systems/crypto/tls"

"github.com/talos-systems/talos/pkg/resources/secrets"
)

// TLSConfig provides client & server TLS configs for apid.
type TLSConfig struct {
certificateProvider *certificateProvider
}

// NewTLSConfig builds provider from configuration and endpoints.
func NewTLSConfig(resources state.State) (*TLSConfig, error) {
watchCh := make(chan state.Event)

if err := resources.Watch(context.TODO(), resource.NewMetadata(secrets.NamespaceName, secrets.APIType, secrets.APIID, resource.VersionUndefined), watchCh); err != nil {
return nil, fmt.Errorf("error setting up watch: %w", err)
}

// wait for the first event to set up certificate provider
provider := &certificateProvider{}

for {
event := <-watchCh
if event.Type == state.Destroyed {
continue
}

apiCerts := event.Resource.(*secrets.API) //nolint:errcheck,forcetypeassert

if err := provider.Update(apiCerts); err != nil {
return nil, err
}

break
}

go func() {
for {
event := <-watchCh
if event.Type == state.Destroyed {
continue
}

apiCerts := event.Resource.(*secrets.API) //nolint:errcheck,forcetypeassert

if err := provider.Update(apiCerts); err != nil {
log.Printf("failed updating cert: %v", err)
}
}
}()

return &TLSConfig{
certificateProvider: provider,
}, nil
}

// ServerConfig generates server-side tls.Config.
func (tlsConfig *TLSConfig) ServerConfig() (*stdlibtls.Config, error) {
ca, err := tlsConfig.certificateProvider.GetCA()
if err != nil {
return nil, fmt.Errorf("failed to get root CA: %w", err)
}

return tls.New(
tls.WithClientAuthType(tls.Mutual),
tls.WithCACertPEM(ca),
tls.WithServerCertificateProvider(tlsConfig.certificateProvider),
)
}

// ClientConfig generates client-side tls.Config.
func (tlsConfig *TLSConfig) ClientConfig() (*stdlibtls.Config, error) {
ca, err := tlsConfig.certificateProvider.GetCA()
if err != nil {
return nil, fmt.Errorf("failed to get root CA: %w", err)
}

return tls.New(
tls.WithClientAuthType(tls.Mutual),
tls.WithCACertPEM(ca),
tls.WithClientCertificateProvider(tlsConfig.certificateProvider),
)
}

type certificateProvider struct {
mu sync.Mutex

apiCerts *secrets.API
clientCert, serverCert *stdlibtls.Certificate
}

func (p *certificateProvider) Update(apiCerts *secrets.API) error {
p.mu.Lock()
defer p.mu.Unlock()

p.apiCerts = apiCerts

serverCert, err := stdlibtls.X509KeyPair(p.apiCerts.TypedSpec().Server.Crt, p.apiCerts.TypedSpec().Server.Key)
if err != nil {
return fmt.Errorf("failed to parse server cert and key into a TLS Certificate: %w", err)
}

p.serverCert = &serverCert

clientCert, err := stdlibtls.X509KeyPair(p.apiCerts.TypedSpec().Client.Crt, p.apiCerts.TypedSpec().Client.Key)
if err != nil {
return fmt.Errorf("failed to parse client cert and key into a TLS Certificate: %w", err)
}

p.clientCert = &clientCert

return nil
}

func (p *certificateProvider) GetCA() ([]byte, error) {
p.mu.Lock()
defer p.mu.Unlock()

return p.apiCerts.TypedSpec().CA.Crt, nil
}

func (p *certificateProvider) GetCertificate(h *stdlibtls.ClientHelloInfo) (*stdlibtls.Certificate, error) {
p.mu.Lock()
defer p.mu.Unlock()

return p.serverCert, nil
}

func (p *certificateProvider) GetClientCertificate(*stdlibtls.CertificateRequestInfo) (*stdlibtls.Certificate, error) {
p.mu.Lock()
defer p.mu.Unlock()

return p.clientCert, nil
}
Loading

0 comments on commit d8c2bca

Please sign in to comment.