Skip to content

Commit

Permalink
Run+Hook sockets over TLS + token auth for CAAS
Browse files Browse the repository at this point in the history
- juju-run multiplex server listens on TCP+TLS
- juju-run local listener per uniter on unix socket
- jujuc listener per hook context either over TCP+TLS or unix socket
- x509 certificates generated by apiserver on request from
caasoperatorprovisioner and injected into operator config map
- juju-run auth token generated per unit and store in operator.yaml alongside
ca.crt file (controller's CA cert)
- jujuc listner pass token via hook env vars & ca cert filename passed
by env var
- caasoperatorprovisioner updates existing operators with certificates
- increase rsa key size to 3072 bits to future proof past 2030
  • Loading branch information
hpidcock committed Sep 13, 2019
1 parent a003b31 commit a50da75
Show file tree
Hide file tree
Showing 60 changed files with 1,561 additions and 352 deletions.
31 changes: 31 additions & 0 deletions api/caasoperatorprovisioner/client.go
Expand Up @@ -135,3 +135,34 @@ func filesystemFromParams(in params.KubernetesFilesystemParams) storage.Kubernet
ResourceTags: in.Tags,
}
}

// OperatorCertificate provides all the information an operator needs to
// create a TLS listener.
type OperatorCertificate struct {
CACert string
Cert string
PrivateKey string
}

// IssueOperatorCertificate issues an x509 certificate for use by the specified application operator.
func (c *Client) IssueOperatorCertificate(applicationName string) (OperatorCertificate, error) {
args := params.Entities{[]params.Entity{
{Tag: applicationName},
}}
var result params.IssueOperatorCertificateResults
if err := c.facade.FacadeCall("IssueOperatorCertificate", args, &result); err != nil {
return OperatorCertificate{}, errors.Trace(err)
}
if len(result.Results) != 1 {
return OperatorCertificate{}, errors.Errorf("expected one result, got %d", len(result.Results))
}
certInfo := result.Results[0]
if err := certInfo.Error; err != nil {
return OperatorCertificate{}, errors.Trace(err)
}
return OperatorCertificate{
CACert: certInfo.CACert,
Cert: certInfo.Cert,
PrivateKey: certInfo.PrivateKey,
}, nil
}
38 changes: 38 additions & 0 deletions api/caasoperatorprovisioner/client_test.go
Expand Up @@ -194,3 +194,41 @@ func (s *provisionerSuite) OperatorProvisioningInfo(c *gc.C) {
},
})
}

func (s *provisionerSuite) TestIssueOperatorCertificate(c *gc.C) {
client := newClient(func(objType string, version int, id, request string, a, result interface{}) error {
c.Check(objType, gc.Equals, "CAASOperatorProvisioner")
c.Check(id, gc.Equals, "")
c.Assert(request, gc.Equals, "IssueOperatorCertificate")
c.Assert(a, jc.DeepEquals, params.Entities{Entities: []params.Entity{{Tag: "appymcappface"}}})
c.Assert(result, gc.FitsTypeOf, &params.IssueOperatorCertificateResults{})
*(result.(*params.IssueOperatorCertificateResults)) = params.IssueOperatorCertificateResults{
Results: []params.IssueOperatorCertificateResult{{
CACert: "ca cert",
Cert: "cert",
PrivateKey: "private key",
}},
}
return nil
})
info, err := client.IssueOperatorCertificate("appymcappface")
c.Assert(err, jc.ErrorIsNil)
c.Assert(info, jc.DeepEquals, caasoperatorprovisioner.OperatorCertificate{
CACert: "ca cert",
Cert: "cert",
PrivateKey: "private key",
})
}

func (s *provisionerSuite) TestIssueOperatorCertificateArity(c *gc.C) {
client := newClient(func(objType string, version int, id, request string, a, result interface{}) error {
c.Check(objType, gc.Equals, "CAASOperatorProvisioner")
c.Check(id, gc.Equals, "")
c.Assert(request, gc.Equals, "IssueOperatorCertificate")
c.Assert(a, jc.DeepEquals, params.Entities{Entities: []params.Entity{{Tag: "appymcappface"}}})
c.Assert(result, gc.FitsTypeOf, &params.IssueOperatorCertificateResults{})
return nil
})
_, err := client.IssueOperatorCertificate("appymcappface")
c.Assert(err, gc.ErrorMatches, "expected one result, got 0")
}
Expand Up @@ -70,6 +70,13 @@ func (st *mockState) Model() (caasoperatorprovisioner.Model, error) {
return st.model, nil
}

func (st *mockState) StateServingInfo() (state.StateServingInfo, error) {
st.MethodCall(st, "StateServingInfo")
return state.StateServingInfo{
CAPrivateKey: coretesting.CAKey,
}, nil
}

type mockStorageRegistry struct {
storage.ProviderRegistry
}
Expand Down
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/juju/juju/apiserver/params"
"github.com/juju/juju/caas"
"github.com/juju/juju/caas/kubernetes/provider"
"github.com/juju/juju/cert"
"github.com/juju/juju/cloudconfig/podcfg"
"github.com/juju/juju/environs/config"
"github.com/juju/juju/environs/tags"
Expand All @@ -39,7 +40,6 @@ type API struct {

// NewStateCAASOperatorProvisionerAPI provides the signature required for facade registration.
func NewStateCAASOperatorProvisionerAPI(ctx facade.Context) (*API, error) {

authorizer := ctx.Auth()
resources := ctx.Resources()

Expand Down Expand Up @@ -147,6 +147,45 @@ func (a *API) OperatorProvisioningInfo() (params.OperatorProvisioningInfo, error
}, nil
}

// IssueOperatorCertificate issues an x509 certificate for use by the specified application operator.
func (a *API) IssueOperatorCertificate(args params.Entities) (params.IssueOperatorCertificateResults, error) {
cfg, err := a.state.ControllerConfig()
if err != nil {
return params.IssueOperatorCertificateResults{}, errors.Trace(err)
}
caCert, _ := cfg.CACert()

si, err := a.state.StateServingInfo()
if err != nil {
return params.IssueOperatorCertificateResults{}, errors.Trace(err)
}

res := params.IssueOperatorCertificateResults{
Results: make([]params.IssueOperatorCertificateResult, len(args.Entities)),
}
for i, entity := range args.Entities {
applicationName := entity.Tag

hostnames := []string{
applicationName,
}
cert, privateKey, err := cert.NewDefaultServer(caCert, si.CAPrivateKey, hostnames)
if err != nil {
res.Results[i] = params.IssueOperatorCertificateResult{
Error: common.ServerError(err),
}
continue
}
res.Results[i] = params.IssueOperatorCertificateResult{
CACert: caCert,
Cert: cert,
PrivateKey: privateKey,
}
}

return res, nil
}

// CharmStorageParams returns filesystem parameters needed
// to provision storage used for a charm operator or workload.
func CharmStorageParams(
Expand Down
Expand Up @@ -4,6 +4,13 @@
package caasoperatorprovisioner_test

import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/pem"

"github.com/juju/errors"
jc "github.com/juju/testing/checkers"
"github.com/juju/version"
Expand Down Expand Up @@ -201,3 +208,36 @@ func (s *CAASProvisionerSuite) TestAddresses(c *gc.C) {
c.Assert(err, jc.ErrorIsNil)
s.st.CheckCallNames(c, "APIHostPortsForAgents")
}

func (s *CAASProvisionerSuite) TestIssueOperatorCertificate(c *gc.C) {
res, err := s.api.IssueOperatorCertificate(params.Entities{
Entities: []params.Entity{{Tag: "appname"}},
})
c.Assert(err, jc.ErrorIsNil)
s.st.CheckCallNames(c, "StateServingInfo")
c.Assert(res.Results, gc.HasLen, 1)
certInfo := res.Results[0]
c.Assert(certInfo.Error, gc.IsNil)
certBlock, rem := pem.Decode([]byte(certInfo.Cert))
c.Assert(rem, gc.HasLen, 0)
keyBlock, rem := pem.Decode([]byte(certInfo.PrivateKey))
c.Assert(rem, gc.HasLen, 0)
cert, err := x509.ParseCertificate(certBlock.Bytes)
c.Assert(err, jc.ErrorIsNil)
roots := x509.NewCertPool()
ok := roots.AppendCertsFromPEM([]byte(certInfo.CACert))
c.Assert(ok, jc.IsTrue)
_, err = cert.Verify(x509.VerifyOptions{
DNSName: "appname",
Roots: roots,
})
c.Assert(err, jc.ErrorIsNil)
key, err := x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
c.Assert(err, jc.ErrorIsNil)
toSign := []byte("hello juju")
hash := sha256.Sum256(toSign)
sig, err := rsa.SignPKCS1v15(rand.Reader, key, crypto.SHA256, hash[:])
c.Assert(err, jc.ErrorIsNil)
err = cert.CheckSignature(x509.SHA256WithRSA, toSign, sig)
c.Assert(err, jc.ErrorIsNil)
}
Expand Up @@ -16,6 +16,7 @@ import (
// required by the CAAS operator provisioner facade.
type CAASOperatorProvisionerState interface {
ControllerConfig() (controller.Config, error)
StateServingInfo() (state.StateServingInfo, error)
WatchApplications() state.StringsWatcher
FindEntity(tag names.Tag) (state.Entity, error)
Addresses() ([]string, error)
Expand Down
49 changes: 49 additions & 0 deletions apiserver/facades/schema.json
Expand Up @@ -6299,6 +6299,17 @@
}
}
},
"IssueOperatorCertificate": {
"type": "object",
"properties": {
"Params": {
"$ref": "#/definitions/Entities"
},
"Result": {
"$ref": "#/definitions/IssueOperatorCertificateResults"
}
}
},
"Life": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -6540,6 +6551,44 @@
"port"
]
},
"IssueOperatorCertificateResult": {
"type": "object",
"properties": {
"ca-cert": {
"type": "string"
},
"cert": {
"type": "string"
},
"error": {
"$ref": "#/definitions/Error"
},
"private-key": {
"type": "string"
}
},
"additionalProperties": false,
"required": [
"ca-cert",
"cert",
"private-key"
]
},
"IssueOperatorCertificateResults": {
"type": "object",
"properties": {
"results": {
"type": "array",
"items": {
"$ref": "#/definitions/IssueOperatorCertificateResult"
}
}
},
"additionalProperties": false,
"required": [
"results"
]
},
"KubernetesFilesystemAttachmentParams": {
"type": "object",
"properties": {
Expand Down
14 changes: 14 additions & 0 deletions apiserver/params/params.go
Expand Up @@ -314,6 +314,20 @@ type OperatorProvisioningInfo struct {
CharmStorage KubernetesFilesystemParams `json:"charm-storage"`
}

// IssueOperatorCertificateResult contains an x509 certificate
// for a CAAS Operator.
type IssueOperatorCertificateResult struct {
CACert string `json:"ca-cert"`
Cert string `json:"cert"`
PrivateKey string `json:"private-key"`
Error *Error `json:"error,omitempty"`
}

// IssueOperatorCertificateResults holds IssueOperatorCertificate results.
type IssueOperatorCertificateResults struct {
Results []IssueOperatorCertificateResult `json:"results"`
}

// PublicAddress holds parameters for the PublicAddress call.
type PublicAddress struct {
Target string `json:"target"`
Expand Down
4 changes: 4 additions & 0 deletions caas/broker.go
Expand Up @@ -300,6 +300,7 @@ type Operator struct {
Id string
Dying bool
Status status.StatusInfo
Config *OperatorConfig
}

// CharmStorageParams defines parameters used to create storage
Expand Down Expand Up @@ -335,6 +336,9 @@ type OperatorConfig struct {
// AgentConf is the contents of the agent.conf file.
AgentConf []byte

// OperatorInfo is the contents of the operator.yaml file.
OperatorInfo []byte

// ResourceTags is a set of tags to set on the operator pod.
ResourceTags map[string]string
}
3 changes: 3 additions & 0 deletions caas/kubernetes/provider/exec/copy_test.go
Expand Up @@ -140,6 +140,9 @@ func (s *execSuite) TestCopyToPod(c *gc.C) {
{Name: "gitlab-container"},
},
},
Status: core.PodStatus{
Phase: core.PodRunning,
},
}
pod.SetName("gitlab-k8s-0")

Expand Down

0 comments on commit a50da75

Please sign in to comment.