Skip to content

Commit

Permalink
Download mTLS files from Web (#14526)
Browse files Browse the repository at this point in the history
In the context of Teleport Discover we are trying to ease the usage of Teleport for the user's first interaction.

When adding a new database resource the user must, among other things, generate the mTLS files
Examples:
https://goteleport.com/docs/database-access/guides/postgres-self-hosted/#step-25-create-a-certificatekey-pair
https://goteleport.com/docs/database-access/guides/mysql-self-hosted/#step-24-create-a-certificatekey-pair

This PR aims to reduce this friction: the user should be able to setup the resource without prior setup of local tools (`tsh login`)
We're doing this by providing an endpoint that will return those exact files


Demo
```shell
marco@lenix ~/p/downloadmtls> curl --silent --insecure 'https://127.0.0.1.nip.io:3080/v1/webapi/sites/lenix/sign' --dat
a '{"hostname":"discover.example.com", "ttl":"9999h", "format": "db"}' --header 'Authorization: Bearer 308bf3dd3019ddc4
2cff44a48e028480' --header 'Content-Type: application/json' -OJ
marco@lenix ~/p/downloadmtls> tar -xvf teleport_mTLS_discover.example.com.tar.gz
server.key
server.crt
server.cas
marco@lenix ~/p/downloadmtls> head -1 server.*
==> server.cas <==
-----BEGIN CERTIFICATE-----

==> server.crt <==
-----BEGIN CERTIFICATE-----

==> server.key <==
-----BEGIN RSA PRIVATE KEY-----
```

Fixes #14049
marcoandredinis authored Aug 1, 2022

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent 89f4d54 commit a60d1c0
Showing 16 changed files with 1,048 additions and 220 deletions.
8 changes: 8 additions & 0 deletions api/identityfile/identityfile.go
Original file line number Diff line number Diff line change
@@ -36,6 +36,14 @@ import (

const (
// FilePermissions defines file permissions for identity files.
//
// Specifically, for postgres, this must be 0600 or 0640 (choosing 0600 as it's more restrictive)
// https://www.postgresql.org/docs/current/libpq-ssl.html
// On Unix systems, the permissions on the private key file must disallow any access to world or group;
// achieve this by a command such as chmod 0600 ~/.postgresql/postgresql.key.
// Alternatively, the file can be owned by root and have group read access (that is, 0640 permissions).
//
// Other services should accept 0600 as well, if not, we must change the Write function (in `lib/client/identityfile/identity.go`)
FilePermissions = 0600
)

3 changes: 3 additions & 0 deletions api/types/constants.go
Original file line number Diff line number Diff line change
@@ -259,6 +259,9 @@ const (
// KindConnectionDiagnostic is a resource that tracks the result of testing a connection
KindConnectionDiagnostic = "connection_diagnostic"

// KindDatabaseCertificate is a resource to control Database Certificates generation
KindDatabaseCertificate = "database_certificate"

// V5 is the fifth version of resources.
V5 = "v5"

26 changes: 19 additions & 7 deletions lib/auth/auth_with_roles.go
Original file line number Diff line number Diff line change
@@ -18,6 +18,7 @@ package auth

import (
"context"
"fmt"
"net/url"
"time"

@@ -3692,14 +3693,25 @@ func (a *ServerWithRoles) SignDatabaseCSR(ctx context.Context, req *proto.Databa
// role Db.
// - Database service when initiating connection to a database instance to
// produce a client certificate.
// - Proxy service when generating mTLS files to a database
func (a *ServerWithRoles) GenerateDatabaseCert(ctx context.Context, req *proto.DatabaseCertRequest) (*proto.DatabaseCertResponse, error) {
// Check if this is a local cluster admin, or a database service, or a
// user that is allowed to impersonate database service.
if !a.hasBuiltinRole(types.RoleDatabase, types.RoleAdmin) {
if err := a.canImpersonateBuiltinRole(types.RoleDatabase); err != nil {
log.WithError(err).Warnf("User %v tried to generate database certificate but is not allowed to impersonate %q system role.",
a.context.User.GetName(), types.RoleDatabase)
return nil, trace.AccessDenied(`access denied. The user must be able to impersonate the builtin role and user "Db" in order to generate database certificates, for more info see https://goteleport.com/docs/database-access/reference/cli/#tctl-auth-sign.`)
// Check if the User can `create` DatabaseCertificates
err := a.action(apidefaults.Namespace, types.KindDatabaseCertificate, types.VerbCreate)
if err != nil {
if !trace.IsAccessDenied(err) {
return nil, trace.Wrap(err)
}

// Err is access denied, trying the old way

// Check if this is a local cluster admin, or a database service, or a
// user that is allowed to impersonate database service.
if !a.hasBuiltinRole(types.RoleDatabase, types.RoleAdmin) {
if err := a.canImpersonateBuiltinRole(types.RoleDatabase); err != nil {
log.WithError(err).Warnf("User %v tried to generate database certificate but does not have '%s' permission for '%s' kind, nor is allowed to impersonate %q system role",
a.context.User.GetName(), types.VerbCreate, types.KindDatabaseCertificate, types.RoleDatabase)
return nil, trace.AccessDenied(fmt.Sprintf("access denied. User must have '%s' permission for '%s' kind to generate the certificate ", types.VerbCreate, types.KindDatabaseCertificate))
}
}
}
return a.authServer.GenerateDatabaseCert(ctx, req)
205 changes: 78 additions & 127 deletions lib/auth/permissions.go
Original file line number Diff line number Diff line change
@@ -362,6 +362,80 @@ func (a *authorizer) authorizeRemoteBuiltinRole(r RemoteBuiltinRole) (*Context,
}, nil
}

func roleSpecForProxyWithRecordAtProxy(clusterName string) types.RoleSpecV5 {
base := roleSpecForProxy(clusterName)
base.Allow.Rules = append(base.Allow.Rules, types.NewRule(types.KindHostCert, services.RW()))
return base
}

func roleSpecForProxy(clusterName string) types.RoleSpecV5 {
return types.RoleSpecV5{
Allow: types.RoleConditions{
Namespaces: []string{types.Wildcard},
ClusterLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
NodeLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
AppLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
DatabaseLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
KubernetesLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
Rules: []types.Rule{
types.NewRule(types.KindProxy, services.RW()),
types.NewRule(types.KindOIDCRequest, services.RW()),
types.NewRule(types.KindSSHSession, services.RW()),
types.NewRule(types.KindSession, services.RO()),
types.NewRule(types.KindEvent, services.RW()),
types.NewRule(types.KindSAMLRequest, services.RW()),
types.NewRule(types.KindOIDC, services.ReadNoSecrets()),
types.NewRule(types.KindSAML, services.ReadNoSecrets()),
types.NewRule(types.KindGithub, services.ReadNoSecrets()),
types.NewRule(types.KindGithubRequest, services.RW()),
types.NewRule(types.KindNamespace, services.RO()),
types.NewRule(types.KindNode, services.RO()),
types.NewRule(types.KindAuthServer, services.RO()),
types.NewRule(types.KindReverseTunnel, services.RO()),
types.NewRule(types.KindCertAuthority, services.ReadNoSecrets()),
types.NewRule(types.KindUser, services.RO()),
types.NewRule(types.KindRole, services.RO()),
types.NewRule(types.KindClusterAuthPreference, services.RO()),
types.NewRule(types.KindClusterName, services.RO()),
types.NewRule(types.KindClusterAuditConfig, services.RO()),
types.NewRule(types.KindClusterNetworkingConfig, services.RO()),
types.NewRule(types.KindSessionRecordingConfig, services.RO()),
types.NewRule(types.KindStaticTokens, services.RO()),
types.NewRule(types.KindTunnelConnection, services.RW()),
types.NewRule(types.KindRemoteCluster, services.RO()),
types.NewRule(types.KindSemaphore, services.RW()),
types.NewRule(types.KindAppServer, services.RO()),
types.NewRule(types.KindWebSession, services.RW()),
types.NewRule(types.KindWebToken, services.RW()),
types.NewRule(types.KindKubeService, services.RW()),
types.NewRule(types.KindDatabaseServer, services.RO()),
types.NewRule(types.KindLock, services.RO()),
types.NewRule(types.KindToken, []string{types.VerbRead, types.VerbDelete}),
types.NewRule(types.KindWindowsDesktopService, services.RO()),
types.NewRule(types.KindDatabaseCertificate, []string{types.VerbCreate}),
types.NewRule(types.KindWindowsDesktop, services.RO()),
// this rule allows local proxy to update the remote cluster's host certificate authorities
// during certificates renewal
{
Resources: []string{types.KindCertAuthority},
Verbs: []string{types.VerbCreate, types.VerbRead, types.VerbUpdate},
// allow administrative access to the host certificate authorities
// matching any cluster name except local
Where: builder.And(
builder.Equals(services.CertAuthorityTypeExpr, builder.String(string(types.HostCA))),
builder.Not(
builder.Equals(
services.ResourceNameExpr,
builder.String(clusterName),
),
),
).String(),
},
},
},
}
}

// RoleSetForBuiltinRole returns RoleSet for embedded builtin role
func RoleSetForBuiltinRoles(clusterName string, recConfig types.SessionRecordingConfig, roles ...types.SystemRole) (services.RoleSet, error) {
var definitions []types.Role
@@ -488,136 +562,13 @@ func definitionForBuiltinRole(clusterName string, recConfig types.SessionRecordi
if services.IsRecordAtProxy(recConfig.GetMode()) {
return services.RoleFromSpec(
role.String(),
types.RoleSpecV5{
Allow: types.RoleConditions{
Namespaces: []string{types.Wildcard},
ClusterLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
NodeLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
AppLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
DatabaseLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
KubernetesLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
Rules: []types.Rule{
types.NewRule(types.KindProxy, services.RW()),
types.NewRule(types.KindOIDCRequest, services.RW()),
types.NewRule(types.KindSSHSession, services.RW()),
types.NewRule(types.KindSession, services.RO()),
types.NewRule(types.KindEvent, services.RW()),
types.NewRule(types.KindSAMLRequest, services.RW()),
types.NewRule(types.KindOIDC, services.ReadNoSecrets()),
types.NewRule(types.KindSAML, services.ReadNoSecrets()),
types.NewRule(types.KindGithub, services.ReadNoSecrets()),
types.NewRule(types.KindGithubRequest, services.RW()),
types.NewRule(types.KindNamespace, services.RO()),
types.NewRule(types.KindNode, services.RO()),
types.NewRule(types.KindAuthServer, services.RO()),
types.NewRule(types.KindReverseTunnel, services.RO()),
types.NewRule(types.KindCertAuthority, services.ReadNoSecrets()),
types.NewRule(types.KindUser, services.RO()),
types.NewRule(types.KindRole, services.RO()),
types.NewRule(types.KindClusterAuthPreference, services.RO()),
types.NewRule(types.KindClusterName, services.RO()),
types.NewRule(types.KindClusterAuditConfig, services.RO()),
types.NewRule(types.KindClusterNetworkingConfig, services.RO()),
types.NewRule(types.KindSessionRecordingConfig, services.RO()),
types.NewRule(types.KindStaticTokens, services.RO()),
types.NewRule(types.KindTunnelConnection, services.RW()),
types.NewRule(types.KindHostCert, services.RW()),
types.NewRule(types.KindRemoteCluster, services.RO()),
types.NewRule(types.KindSemaphore, services.RW()),
types.NewRule(types.KindAppServer, services.RO()),
types.NewRule(types.KindWebSession, services.RW()),
types.NewRule(types.KindWebToken, services.RW()),
types.NewRule(types.KindKubeService, services.RW()),
types.NewRule(types.KindDatabaseServer, services.RO()),
types.NewRule(types.KindLock, services.RO()),
types.NewRule(types.KindWindowsDesktopService, services.RO()),
types.NewRule(types.KindWindowsDesktop, services.RO()),
// this rule allows local proxy to update the remote cluster's host certificate authorities
// during certificates renewal
{
Resources: []string{types.KindCertAuthority},
Verbs: []string{types.VerbCreate, types.VerbRead, types.VerbUpdate},
// allow administrative access to the host certificate authorities
// matching any cluster name except local
Where: builder.And(
builder.Equals(services.CertAuthorityTypeExpr, builder.String(string(types.HostCA))),
builder.Not(
builder.Equals(
services.ResourceNameExpr,
builder.String(clusterName),
),
),
).String(),
},
},
},
})
roleSpecForProxyWithRecordAtProxy(clusterName),
)
}
return services.RoleFromSpec(
role.String(),
types.RoleSpecV5{
Allow: types.RoleConditions{
Namespaces: []string{types.Wildcard},
ClusterLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
NodeLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
AppLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
DatabaseLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
KubernetesLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
Rules: []types.Rule{
types.NewRule(types.KindProxy, services.RW()),
types.NewRule(types.KindOIDCRequest, services.RW()),
types.NewRule(types.KindSSHSession, services.RW()),
types.NewRule(types.KindSession, services.RO()),
types.NewRule(types.KindEvent, services.RW()),
types.NewRule(types.KindSAMLRequest, services.RW()),
types.NewRule(types.KindOIDC, services.ReadNoSecrets()),
types.NewRule(types.KindSAML, services.ReadNoSecrets()),
types.NewRule(types.KindGithub, services.ReadNoSecrets()),
types.NewRule(types.KindGithubRequest, services.RW()),
types.NewRule(types.KindNamespace, services.RO()),
types.NewRule(types.KindNode, services.RO()),
types.NewRule(types.KindAuthServer, services.RO()),
types.NewRule(types.KindReverseTunnel, services.RO()),
types.NewRule(types.KindCertAuthority, services.ReadNoSecrets()),
types.NewRule(types.KindUser, services.RO()),
types.NewRule(types.KindRole, services.RO()),
types.NewRule(types.KindClusterAuthPreference, services.RO()),
types.NewRule(types.KindClusterName, services.RO()),
types.NewRule(types.KindClusterAuditConfig, services.RO()),
types.NewRule(types.KindClusterNetworkingConfig, services.RO()),
types.NewRule(types.KindSessionRecordingConfig, services.RO()),
types.NewRule(types.KindStaticTokens, services.RO()),
types.NewRule(types.KindTunnelConnection, services.RW()),
types.NewRule(types.KindRemoteCluster, services.RO()),
types.NewRule(types.KindSemaphore, services.RW()),
types.NewRule(types.KindAppServer, services.RO()),
types.NewRule(types.KindWebSession, services.RW()),
types.NewRule(types.KindWebToken, services.RW()),
types.NewRule(types.KindKubeService, services.RW()),
types.NewRule(types.KindDatabaseServer, services.RO()),
types.NewRule(types.KindLock, services.RO()),
types.NewRule(types.KindWindowsDesktopService, services.RO()),
types.NewRule(types.KindWindowsDesktop, services.RO()),
// this rule allows local proxy to update the remote cluster's host certificate authorities
// during certificates renewal
{
Resources: []string{types.KindCertAuthority},
Verbs: []string{types.VerbCreate, types.VerbRead, types.VerbUpdate},
// allow administrative access to the certificate authority names
// matching any cluster name except local
Where: builder.And(
builder.Equals(services.CertAuthorityTypeExpr, builder.String(string(types.HostCA))),
builder.Not(
builder.Equals(
services.ResourceNameExpr,
builder.String(clusterName),
),
),
).String(),
},
},
},
})
roleSpecForProxy(clusterName),
)
case types.RoleSignup:
return services.RoleFromSpec(
role.String(),
Loading

0 comments on commit a60d1c0

Please sign in to comment.