Merge branch 'master' into ev/docker

This commit is contained in:
Ev Kontsevoy 2017-02-22 12:02:40 -08:00 committed by GitHub
commit bc85683b51
27 changed files with 1552 additions and 252 deletions

View file

@ -72,18 +72,24 @@ const (
// pining each other with it:
KeepAliveReqType = "keepalive@openssh.com"
// OTP means One-time Password Algorithm.
// OTP means One-time Password Algorithm for Two-Factor Authentication.
OTP = "otp"
// TOTP means Time-based One-time Password Algorithm.
// TOTP means Time-based One-time Password Algorithm. for Two-Factor Authentication.
TOTP = "totp"
// HOTP means HMAC-based One-time Password Algorithm.
// HOTP means HMAC-based One-time Password Algorithm.for Two-Factor Authentication.
HOTP = "hotp"
// U2F means Universal 2nd Factor.
// U2F means Universal 2nd Factor.for Two-Factor Authentication.
U2F = "u2f"
// OIDC means OpenID Connect.
// OFF means no second factor.for Two-Factor Authentication.
OFF = "off"
// Local means authentication will happen locally within the Teleport cluster.
Local = "local"
// OIDC means authentication will happen remotly using an OIDC connector.
OIDC = "oidc"
)

View file

@ -197,6 +197,13 @@ auth_service:
# Turns 'auth' role on. Default is 'yes'
enabled: yes
# defines the types and second factors the auth server supports
authentication:
# type can be local or oidc
type: local
# second_factor can be off, otp, or u2f
second_factor: otp
# IP and the port to bind to. Other Teleport nodes will be connecting to
# this port (AKA "Auth API" or "Cluster API") to validate client
# certificates
@ -747,14 +754,20 @@ OIDC integration with applications like Teleport.
```
auth_service:
enabled: true
cluster_name: magadan
oidc_connectors:
- id: google
redirect_url: https://localhost:3080/v1/webapi/oidc/callback
client_id: id-from-google.apps.googleusercontent.com
client_secret: secret-key-from-google
issuer_url: https://accounts.google.com
authentication:
type: oidc
oidc:
id: google
redirect_url: https://localhost:3080/v1/webapi/oidc/callback
client_id: id-from-google.apps.googleusercontent.com
client_secret: secret-key-from-google
issuer_url: https://accounts.google.com
display: "whaterver"
scope: ["ssh_permissions", "roles"]
claims_to_roles:
- claim: role
value: admin
roles: ["dba", "backup", "root"]
```
Now you should be able to create Teleport users whose identity is managed by Google.
@ -817,17 +830,17 @@ service configuration in `teleport.yaml`:
```bash
auth_service:
u2f:
# Must be set to 'yes' to enable and 'no' to disable:
enabled: yes
authentication:
type: local
second_factor: u2f
u2f:
# Only matters when multiple proxy servers are used:
app_id: https://mycorp.com/appid.js
# Only matters when multiple proxy servers are used:
app_id: https://mycorp.com/appid.js
# U2F facets must be set to Teleport proxy servers:
facets:
- https://proxy1.mycorp.com:3080
- https://proxy2.mycorp.com:3080
# U2F facets must be set to Teleport proxy servers:
facets:
- https://proxy1.mycorp.com:3080
- https://proxy2.mycorp.com:3080
```
If your Teleport is deployed with the same `teleport` process running as a proxy

View file

@ -117,14 +117,6 @@ func NewAPIServer(config *APIConfig) http.Handler {
srv.GET("/:version/namespaces/:namespace/sessions/:id/stream", srv.withAuth(srv.getSessionChunk))
srv.GET("/:version/namespaces/:namespace/sessions/:id/events", srv.withAuth(srv.getSessionEvents))
// OIDC
srv.POST("/:version/oidc/connectors", srv.withAuth(srv.upsertOIDCConnector))
srv.GET("/:version/oidc/connectors", srv.withAuth(srv.getOIDCConnectors))
srv.GET("/:version/oidc/connectors/:id", srv.withAuth(srv.getOIDCConnector))
srv.DELETE("/:version/oidc/connectors/:id", srv.withAuth(srv.deleteOIDCConnector))
srv.POST("/:version/oidc/requests/create", srv.withAuth(srv.createOIDCAuthRequest))
srv.POST("/:version/oidc/requests/validate", srv.withAuth(srv.validateOIDCAuthCallback))
// Namespaces
srv.POST("/:version/namespaces", srv.withAuth(srv.upsertNamespace))
srv.GET("/:version/namespaces", srv.withAuth(srv.getNamespaces))
@ -137,6 +129,20 @@ func NewAPIServer(config *APIConfig) http.Handler {
srv.GET("/:version/roles/:role", srv.withAuth(srv.getRole))
srv.DELETE("/:version/roles/:role", srv.withAuth(srv.deleteRole))
// cluster authentication preferences
srv.GET("/:version/authentication/preference", srv.withAuth(srv.getClusterAuthPreference))
srv.POST("/:version/authentication/preference", srv.withAuth(srv.setClusterAuthPreference))
srv.GET("/:version/authentication/preference/u2f", srv.withAuth(srv.getUniversalSecondFactor))
srv.POST("/:version/authentication/preference/u2f", srv.withAuth(srv.setUniversalSecondFactor))
// OIDC
srv.POST("/:version/oidc/connectors", srv.withAuth(srv.upsertOIDCConnector))
srv.GET("/:version/oidc/connectors", srv.withAuth(srv.getOIDCConnectors))
srv.GET("/:version/oidc/connectors/:id", srv.withAuth(srv.getOIDCConnector))
srv.DELETE("/:version/oidc/connectors/:id", srv.withAuth(srv.deleteOIDCConnector))
srv.POST("/:version/oidc/requests/create", srv.withAuth(srv.createOIDCAuthRequest))
srv.POST("/:version/oidc/requests/validate", srv.withAuth(srv.validateOIDCAuthCallback))
// U2F
srv.GET("/:version/u2f/signuptokens/:token", srv.withAuth(srv.getSignupU2FRegisterRequest))
srv.POST("/:version/u2f/users", srv.withAuth(srv.createUserWithU2FToken))
@ -750,12 +756,13 @@ func (s *APIServer) getDomainName(auth ClientI, w http.ResponseWriter, r *http.R
// getU2FAppID returns the U2F AppID in the auth configuration
func (s *APIServer) getU2FAppID(auth ClientI, w http.ResponseWriter, r *http.Request, p httprouter.Params, version string) (interface{}, error) {
appID, err := auth.GetU2FAppID()
universalSecondFactor, err := auth.GetUniversalSecondFactor()
if err != nil {
return nil, trace.Wrap(err)
}
w.Header().Set("Content-Type", "application/fido.trusted-apps+json")
return appID, nil
return universalSecondFactor.GetAppID(), nil
}
func (s *APIServer) deleteCertAuthority(auth ClientI, w http.ResponseWriter, r *http.Request, p httprouter.Params, version string) (interface{}, error) {
@ -1257,6 +1264,74 @@ func (s *APIServer) deleteRole(auth ClientI, w http.ResponseWriter, r *http.Requ
return message(fmt.Sprintf("role '%v' deleted", role)), nil
}
func (s *APIServer) getClusterAuthPreference(auth ClientI, w http.ResponseWriter, r *http.Request, p httprouter.Params, version string) (interface{}, error) {
cap, err := auth.GetClusterAuthPreference()
if err != nil {
return nil, trace.Wrap(err)
}
return rawMessage(services.GetAuthPreferenceMarshaler().Marshal(cap, services.WithVersion(version)))
}
type setClusterAuthPreferenceReq struct {
ClusterAuthPreference json.RawMessage `json:"cluster_auth_prerference"`
}
func (s *APIServer) setClusterAuthPreference(auth ClientI, w http.ResponseWriter, r *http.Request, p httprouter.Params, version string) (interface{}, error) {
var req *setClusterAuthPreferenceReq
err := httplib.ReadJSON(r, &req)
if err != nil {
return nil, trace.Wrap(err)
}
cap, err := services.GetAuthPreferenceMarshaler().Unmarshal(req.ClusterAuthPreference)
if err != nil {
return nil, trace.Wrap(err)
}
err = auth.SetClusterAuthPreference(cap)
if err != nil {
return nil, trace.Wrap(err)
}
return message(fmt.Sprintf("cluster authenticaton preference set: %+v", cap)), nil
}
func (s *APIServer) getUniversalSecondFactor(auth ClientI, w http.ResponseWriter, r *http.Request, p httprouter.Params, version string) (interface{}, error) {
universalSecondFactor, err := auth.GetUniversalSecondFactor()
if err != nil {
return nil, trace.Wrap(err)
}
return rawMessage(services.GetUniversalSecondFactorMarshaler().Marshal(universalSecondFactor, services.WithVersion(version)))
}
type setUniversalSecondFactorReq struct {
UniversalSecondFactor json.RawMessage `json:"universal_second_factor"`
}
func (s *APIServer) setUniversalSecondFactor(auth ClientI, w http.ResponseWriter, r *http.Request, p httprouter.Params, version string) (interface{}, error) {
var req *setUniversalSecondFactorReq
err := httplib.ReadJSON(r, &req)
if err != nil {
return nil, trace.Wrap(err)
}
universalSecondFactor, err := services.GetUniversalSecondFactorMarshaler().Unmarshal(req.UniversalSecondFactor)
if err != nil {
return nil, trace.Wrap(err)
}
err = auth.SetUniversalSecondFactor(universalSecondFactor)
if err != nil {
return nil, trace.Wrap(err)
}
return message(fmt.Sprintf("universal second factor set: %+v", universalSecondFactor)), nil
}
func message(msg string) map[string]interface{} {
return map[string]interface{}{"message": msg}
}

View file

@ -85,19 +85,27 @@ func NewAuthServer(cfg *InitConfig, opts ...AuthServerOption) *AuthServer {
if cfg.Access == nil {
cfg.Access = local.NewAccessService(cfg.Backend)
}
if cfg.ClusterAuthPreferenceService == nil {
cfg.ClusterAuthPreferenceService = local.NewClusterAuthPreferenceService(cfg.Backend)
}
if cfg.UniversalSecondFactorService == nil {
cfg.UniversalSecondFactorService = local.NewUniversalSecondFactorService(cfg.Backend)
}
as := AuthServer{
bk: cfg.Backend,
Authority: cfg.Authority,
Trust: cfg.Trust,
Presence: cfg.Presence,
Provisioner: cfg.Provisioner,
Identity: cfg.Identity,
Access: cfg.Access,
DomainName: cfg.DomainName,
AuthServiceName: cfg.AuthServiceName,
oidcClients: make(map[string]*oidcClient),
StaticTokens: cfg.StaticTokens,
U2F: cfg.U2F,
bk: cfg.Backend,
Authority: cfg.Authority,
Trust: cfg.Trust,
Presence: cfg.Presence,
Provisioner: cfg.Provisioner,
Identity: cfg.Identity,
Access: cfg.Access,
DomainName: cfg.DomainName,
AuthServiceName: cfg.AuthServiceName,
StaticTokens: cfg.StaticTokens,
ClusterAuthPreference: cfg.ClusterAuthPreferenceService,
UniversalSecondFactorSettings: cfg.UniversalSecondFactorService,
oidcClients: make(map[string]*oidcClient),
}
for _, o := range opts {
o(&as)
@ -135,14 +143,13 @@ type AuthServer struct {
// environments where paranoid security is not needed
StaticTokens []services.ProvisionToken
// U2F is the configuration of the U2F 2 factor authentication
U2F services.U2F
services.Trust
services.Presence
services.Provisioner
services.Identity
services.Access
services.ClusterAuthPreference
services.UniversalSecondFactorSettings
}
func (a *AuthServer) Close() error {
@ -158,20 +165,6 @@ func (a *AuthServer) GetDomainName() (string, error) {
return a.DomainName, nil
}
func (a *AuthServer) GetU2FAppID() (string, error) {
if err := a.CheckU2FEnabled(); err != nil {
return "", trace.Wrap(err)
}
return a.U2F.AppID, nil
}
func (a *AuthServer) CheckU2FEnabled() error {
if !a.U2F.Enabled {
return trace.AccessDenied("U2F is disabled")
}
return nil
}
// GenerateHostCert uses the private key of the CA to sign the public key of the host
// (along with meta data like host ID, node name, roles, and ttl) to generate a host certificate.
func (s *AuthServer) GenerateHostCert(hostPublicKey []byte, hostID, nodeName, clusterName string, roles teleport.Roles, ttl time.Duration) ([]byte, error) {
@ -296,7 +289,7 @@ func (s *AuthServer) PreAuthenticatedSignIn(user string) (services.WebSession, e
}
func (s *AuthServer) U2FSignRequest(user string, password []byte) (*u2f.SignRequest, error) {
err := s.CheckU2FEnabled()
universalSecondFactor, err := s.GetUniversalSecondFactor()
if err != nil {
return nil, trace.Wrap(err)
}
@ -313,7 +306,7 @@ func (s *AuthServer) U2FSignRequest(user string, password []byte) (*u2f.SignRequ
return nil, trace.Wrap(err)
}
challenge, err := u2f.NewChallenge(s.U2F.AppID, s.U2F.Facets)
challenge, err := u2f.NewChallenge(universalSecondFactor.GetAppID(), universalSecondFactor.GetFacets())
if err != nil {
return nil, trace.Wrap(err)
}
@ -329,7 +322,8 @@ func (s *AuthServer) U2FSignRequest(user string, password []byte) (*u2f.SignRequ
}
func (s *AuthServer) CheckU2FSignResponse(user string, response *u2f.SignResponse) error {
err := s.CheckU2FEnabled()
// before trying to register a user, see U2F is actually setup on the backend
_, err := s.GetUniversalSecondFactor()
if err != nil {
return trace.Wrap(err)
}

View file

@ -122,11 +122,6 @@ func (a *AuthWithRoles) GetDomainName() (string, error) {
return a.authServer.GetDomainName()
}
func (a *AuthWithRoles) GetU2FAppID() (string, error) {
// authenticated users can all read this
return a.authServer.GetU2FAppID()
}
func (a *AuthWithRoles) DeleteCertAuthority(id services.CertAuthID) error {
if err := a.action(defaults.Namespace, services.KindCertAuthority, services.ActionWrite); err != nil {
return trace.Wrap(err)
@ -578,6 +573,42 @@ func (a *AuthWithRoles) DeleteRole(name string) error {
return a.authServer.DeleteRole(name)
}
func (a *AuthWithRoles) GetClusterAuthPreference() (services.AuthPreference, error) {
err := a.action(defaults.Namespace, services.KindClusterAuthPreference, services.ActionRead)
if err != nil {
return nil, trace.Wrap(err)
}
return a.authServer.GetClusterAuthPreference()
}
func (a *AuthWithRoles) SetClusterAuthPreference(cap services.AuthPreference) error {
err := a.action(defaults.Namespace, services.KindClusterAuthPreference, services.ActionWrite)
if err != nil {
return trace.Wrap(err)
}
return a.authServer.SetClusterAuthPreference(cap)
}
func (a *AuthWithRoles) GetUniversalSecondFactor() (services.UniversalSecondFactor, error) {
err := a.action(defaults.Namespace, services.KindUniversalSecondFactor, services.ActionRead)
if err != nil {
return nil, trace.Wrap(err)
}
return a.authServer.GetUniversalSecondFactor()
}
func (a *AuthWithRoles) SetUniversalSecondFactor(u2f services.UniversalSecondFactor) error {
err := a.action(defaults.Namespace, services.KindUniversalSecondFactor, services.ActionWrite)
if err != nil {
return trace.Wrap(err)
}
return a.authServer.SetUniversalSecondFactor(u2f)
}
// NewAuthWithRoles creates new auth server with access control
func NewAuthWithRoles(authServer *AuthServer,
checker services.AccessChecker,

View file

@ -1131,6 +1131,62 @@ func (c *Client) DeleteRole(name string) error {
return trace.Wrap(err)
}
func (c *Client) GetClusterAuthPreference() (services.AuthPreference, error) {
out, err := c.Get(c.Endpoint("authentication", "preferences"), url.Values{})
if err != nil {
return nil, trace.Wrap(err)
}
cap, err := services.GetAuthPreferenceMarshaler().Unmarshal(out.Bytes())
if err != nil {
return nil, trace.Wrap(err)
}
return cap, nil
}
func (c *Client) SetClusterAuthPreference(cap services.AuthPreference) error {
data, err := services.GetAuthPreferenceMarshaler().Marshal(cap)
if err != nil {
return trace.Wrap(err)
}
_, err = c.PostJSON(c.Endpoint("authentication", "preferences"), &setClusterAuthPreferenceReq{ClusterAuthPreference: data})
if err != nil {
return trace.Wrap(err)
}
return nil
}
func (c *Client) GetUniversalSecondFactor() (services.UniversalSecondFactor, error) {
out, err := c.Get(c.Endpoint("authentication", "preferences", "u2f"), url.Values{})
if err != nil {
return nil, trace.Wrap(err)
}
universalSecondFactor, err := services.GetUniversalSecondFactorMarshaler().Unmarshal(out.Bytes())
if err != nil {
return nil, trace.Wrap(err)
}
return universalSecondFactor, nil
}
func (c *Client) SetUniversalSecondFactor(universalSecondFactor services.UniversalSecondFactor) error {
data, err := services.GetUniversalSecondFactorMarshaler().Marshal(universalSecondFactor)
if err != nil {
return trace.Wrap(err)
}
_, err = c.PostJSON(c.Endpoint("authentication", "preferences", "u2f"), &setUniversalSecondFactorReq{UniversalSecondFactor: data})
if err != nil {
return trace.Wrap(err)
}
return nil
}
// WebService implements features used by Web UI clients
type WebService interface {
// GetWebSessionInfo checks if a web sesion is valid, returns session id in case if
@ -1145,7 +1201,7 @@ type WebService interface {
DeleteWebSession(user string, sid string) error
}
// IdentityService manages identities and userse
// IdentityService manages identities and users
type IdentityService interface {
// UpsertPassword updates web access password for the user
UpsertPassword(user string, password []byte) error
@ -1180,9 +1236,6 @@ type IdentityService interface {
// PreAuthenticatedSignIn is used get web session for a user that is already authenticated
PreAuthenticatedSignIn(user string) (services.WebSession, error)
// GetU2FAppID returns U2F settings, like App ID and Facets
GetU2FAppID() (string, error)
// GetUser returns user by name
GetUser(name string) (services.User, error)
@ -1270,6 +1323,8 @@ type ClientI interface {
services.Access
WebService
session.Service
services.ClusterAuthPreference
services.UniversalSecondFactorSettings
GetDomainName() (string, error)
}

View file

@ -87,6 +87,12 @@ type InitConfig struct {
// Access is service controlling access to resources
Access services.Access
// ClusterAuthPreferenceService is a service to get and set authentication preferences.
ClusterAuthPreferenceService services.ClusterAuthPreference
// UniversalSecondFactorService is a service to get and set universal second factor settings.
UniversalSecondFactorService services.UniversalSecondFactorSettings
// Roles is a set of roles to create
Roles []services.Role
@ -94,8 +100,12 @@ type InitConfig struct {
// environments where paranoid security is not needed
StaticTokens []services.ProvisionToken
// U2F is the configuration of the U2F 2 factor authentication
U2F services.U2F
// AuthPreference defines the authentication type (local, oidc) and second
// factor (off, otp, u2f) passed in from a configuration file.
AuthPreference services.AuthPreference
// U2F defines U2F application ID and any facets passed in from a configuration file.
U2F services.UniversalSecondFactor
}
// Init instantiates and configures an instance of AuthServer
@ -126,6 +136,43 @@ func Init(cfg InitConfig, seedConfig bool) (*AuthServer, *Identity, error) {
// and this is NOT the first time teleport starts on this machine
skipConfig := seedConfig && !firstStart
// set the cluster auth prerference, the first time read them from the config
// and create a resource on the backend, after that always read from the backend
if !skipConfig {
log.Infof("Initializing Cluster Authentication Preference: %+v", cfg.AuthPreference)
err = asrv.SetClusterAuthPreference(cfg.AuthPreference)
if err != nil {
return nil, nil, trace.Wrap(err)
}
log.Infof("Initializing Universal Second Factor Settings: %+v", cfg.U2F)
if cfg.U2F != nil {
err = asrv.SetUniversalSecondFactor(cfg.U2F)
if err != nil {
return nil, nil, trace.Wrap(err)
}
}
}
// migrate: if U2F is set, but we don't have anything set on the backend, do a migration
// because the old configuration file format is probably being used.
if cfg.U2F != nil {
// check if we already have something set on the backend
_, err = asrv.GetUniversalSecondFactor()
if err != nil {
// if we have an error other than IsNotFound return it right away
if !trace.IsNotFound(err) {
return nil, nil, trace.Wrap(err)
}
// if the error was not found, then try and set it on the backend
err = asrv.SetUniversalSecondFactor(cfg.U2F)
if err != nil {
return nil, nil, trace.Wrap(err)
}
}
}
// add trusted authorities from the configuration into the trust backend:
keepMap := make(map[string]int, 0)
if !skipConfig {

View file

@ -17,15 +17,20 @@ limitations under the License.
package auth
import (
"io/ioutil"
"time"
"golang.org/x/crypto/ssh"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/lib/auth/testauthority"
"github.com/gravitational/teleport/lib/backend"
"github.com/gravitational/teleport/lib/backend/boltbk"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/utils"
"golang.org/x/crypto/ssh"
"github.com/gravitational/trace"
. "gopkg.in/check.v1"
)
@ -137,3 +142,36 @@ func (s *AuthInitSuite) TestBadIdentity(c *C) {
_, err = ReadIdentityFromKeyPair(priv, cert)
c.Assert(trace.IsBadParameter(err), Equals, true, Commentf("%#v", err))
}
// TestAuthPreference ensures that the act of creating an AuthServer sets
// the AuthPreference (type and second factor) on the backend.
func (s *AuthInitSuite) TestAuthPreference(c *C) {
tempDir, err := ioutil.TempDir("", "auth-test-")
c.Assert(err, IsNil)
bk, err := boltbk.New(backend.Params{"path": tempDir})
c.Assert(err, IsNil)
ap, err := services.NewAuthPreference(services.AuthPreferenceSpecV2{
Type: "local",
SecondFactor: "u2f",
})
c.Assert(err, IsNil)
ac := InitConfig{
DataDir: tempDir,
HostUUID: "00000000-0000-0000-0000-000000000000",
NodeName: "foo",
Backend: bk,
Authority: testauthority.New(),
DomainName: "me.localhost",
AuthPreference: ap,
}
as, _, err := Init(ac, true)
c.Assert(err, IsNil)
cap, err := as.GetClusterAuthPreference()
c.Assert(err, IsNil)
c.Assert(cap.GetType(), Equals, "local")
c.Assert(cap.GetSecondFactor(), Equals, "u2f")
}

View file

@ -131,7 +131,7 @@ func (s *AuthServer) GetSignupTokenData(token string) (user string, qrCode []byt
}
func (s *AuthServer) CreateSignupU2FRegisterRequest(token string) (u2fRegisterRequest *u2f.RegisterRequest, e error) {
err := s.CheckU2FEnabled()
universalSecondFactor, err := s.GetUniversalSecondFactor()
if err != nil {
return nil, trace.Wrap(err)
}
@ -143,10 +143,10 @@ func (s *AuthServer) CreateSignupU2FRegisterRequest(token string) (u2fRegisterRe
_, err = s.GetPasswordHash(tokenData.User.Name)
if err == nil {
return nil, trace.AlreadyExists("can't add user %v, user already exists", tokenData.User)
return nil, trace.AlreadyExists("can't add user %q, user already exists", tokenData.User)
}
c, err := u2f.NewChallenge(s.U2F.AppID, s.U2F.Facets)
c, err := u2f.NewChallenge(universalSecondFactor.GetAppID(), universalSecondFactor.GetFacets())
if err != nil {
return nil, trace.Wrap(err)
}
@ -220,9 +220,10 @@ func (s *AuthServer) CreateUserWithToken(token string, password string, otpToken
}
func (s *AuthServer) CreateUserWithU2FToken(token string, password string, response u2f.RegisterResponse) (services.WebSession, error) {
err := s.CheckU2FEnabled()
// before trying to create a user, see U2F is actually setup on the backend
_, err := s.GetUniversalSecondFactor()
if err != nil {
return nil, trace.Wrap(err)
return nil, err
}
tokenData, err := s.GetSignupToken(token)

View file

@ -1,5 +1,5 @@
/*
Copyright 2015-16 Gravitational, Inc.
Copyright 2015 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -93,7 +93,7 @@ func ReadConfigFile(cliConfigPath string) (*FileConfig, error) {
return ReadFromFile(configFilePath)
}
// ApplyFileConfig applies confniguration from a YAML file to Teleport
// ApplyFileConfig applies configuration from a YAML file to Teleport
// runtime config
func ApplyFileConfig(fc *FileConfig, cfg *service.Config) error {
// no config file? no problem
@ -141,13 +141,6 @@ func ApplyFileConfig(fc *FileConfig, cfg *service.Config) error {
cfg.ApplyToken(fc.AuthToken)
cfg.Auth.DomainName = fc.Auth.DomainName
// U2F (universal 2nd factor auth) configuration:
u2f, err := fc.Auth.U2F.Parse()
if err != nil {
return trace.Wrap(err)
}
cfg.Auth.U2F = *u2f
if fc.Global.DataDir != "" {
cfg.DataDir = fc.Global.DataDir
cfg.Auth.StorageConfig.Params["path"] = cfg.DataDir
@ -229,15 +222,6 @@ func ApplyFileConfig(fc *FileConfig, cfg *service.Config) error {
cfg.ReverseTunnels = append(cfg.ReverseTunnels, tun)
}
// add oidc connectors supplied from configs
for _, c := range fc.Auth.OIDCConnectors {
conn, err := c.Parse()
if err != nil {
return trace.Wrap(err)
}
cfg.OIDCConnectors = append(cfg.OIDCConnectors, conn)
}
// apply "proxy_service" section
if fc.Proxy.ListenAddress != "" {
addr, err := utils.ParseHostPortAddr(fc.Proxy.ListenAddress, int(defaults.SSHProxyListenPort))
@ -273,6 +257,63 @@ func ApplyFileConfig(fc *FileConfig, cfg *service.Config) error {
cfg.Proxy.TLSCert = fc.Proxy.CertFile
}
// Deprecated: Use U2F section in Authentication section instead.
u2f, err := fc.Auth.U2F.Parse()
if err != nil {
return trace.Wrap(err)
}
if u2f.Enabled {
log.Warnf("Configuration of U2F settings under auth_server.u2f has been deprecated.")
log.Warnf("Configuration of U2F has moved under the auth_server.authentication section.")
log.Warnf("See the Admin Guide for more details.\n")
}
// If the old config format is still being used, convert it to the new format,
// and still allow it to be used.
cfg.Auth.U2F, err = services.NewUniversalSecondFactor(services.UniversalSecondFactorSpecV2{
AppID: u2f.AppID,
Facets: u2f.Facets,
})
if err != nil {
return trace.Wrap(err)
}
// Deprecated: Use OIDC section in Authentication section instead.
// If the old config format is still being used, allow it to be used.
if len(fc.Auth.OIDCConnectors) > 0 {
log.Warnf("Configuration for OIDC connectors under auth_server.oidc_connectors has been deprecated.")
log.Warnf("Configuration for OIDC connectors has moved under the auth_server.authentication section.")
log.Warnf("See the Admin Guide for more details.\n")
}
for _, c := range fc.Auth.OIDCConnectors {
conn, err := c.Parse()
if err != nil {
return trace.Wrap(err)
}
cfg.OIDCConnectors = append(cfg.OIDCConnectors, conn)
}
// if authentication section exists on the auth_server, then use it to extract
// the cluster authentication preferences and override u2f and oidc settings set
// in the above two blocks
if fc.Auth.Authentication != nil {
authPreference, oidcConnector, universalSecondFactor, err := fc.Auth.Authentication.Parse()
if err != nil {
return trace.Wrap(err)
}
cfg.Auth.Preference = authPreference
// we now only always allow a single oidc connector, note this will
// override anything set with the old format
if oidcConnector != nil {
cfg.OIDCConnectors = []services.OIDCConnector{oidcConnector}
}
// set u2f settings, note this will override anything set with the old format
if universalSecondFactor != nil {
cfg.Auth.U2F = universalSecondFactor
}
}
// apply "auth_service" section
if fc.Auth.ListenAddress != "" {
addr, err := utils.ParseHostPortAddr(fc.Auth.ListenAddress, int(defaults.AuthListenPort))

View file

@ -1,5 +1,5 @@
/*
Copyright 2016 Gravitational, Inc.
Copyright 2015 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -97,7 +97,6 @@ func (s *ConfigTestSuite) TestSampleConfig(c *check.C) {
c.Assert(fc.Limits.MaxUsers, check.Equals, defaults.LimiterMaxConcurrentUsers)
c.Assert(fc.Global.DataDir, check.Equals, defaults.DataDir)
c.Assert(fc.Logger.Severity, check.Equals, "INFO")
}
func (s *ConfigTestSuite) TestConfigReading(c *check.C) {
@ -298,6 +297,21 @@ func (s *ConfigTestSuite) TestApplyConfig(c *check.C) {
c.Assert(cfg.Proxy.ReverseTunnelListenAddr.FullAddress(), check.Equals, "tcp://tunnelhost:1001")
}
// TestLegacyU2FTransformation ensures that the legacy format for U2F gets transformed
// into the new format that we are using now for backward compatibility.
func (s *ConfigTestSuite) TestLegacyU2FTransformation(c *check.C) {
conf, err := ReadConfig(bytes.NewBufferString(LegacyAuthenticationSection))
c.Assert(err, check.IsNil)
c.Assert(conf, check.NotNil)
cfg := service.MakeDefaultConfig()
err = ApplyFileConfig(conf, cfg)
c.Assert(err, check.IsNil)
c.Assert(cfg.Auth.U2F.GetAppID(), check.Equals, "https://graviton:3080")
c.Assert(cfg.Auth.U2F.GetFacets(), check.DeepEquals, []string{"https://graviton:3080"})
}
func checkStaticConfig(c *check.C, conf *FileConfig) {
c.Assert(conf.AuthToken, check.Equals, "xxxyyy")
c.Assert(conf.SSH.Enabled(), check.Equals, false) // YAML treats 'no' as False
@ -419,117 +433,3 @@ func makeConfigFixture() string {
return conf.DebugDumpToYAML()
}
const (
StaticConfigString = `
#
# Some comments
#
teleport:
nodename: edsger.example.com
advertise_ip: 10.10.10.1
pid_file: /var/run/teleport.pid
auth_servers:
- auth0.server.example.org:3024
- auth1.server.example.org:3024
auth_token: xxxyyy
log:
output: stderr
severity: INFO
storage:
type: etcd
peers: ['one', 'two']
tls_key_file: /tls.key
tls_cert_file: /tls.cert
tls_ca_file: /tls.ca
connection_limits:
max_connections: 90
max_users: 91
rates:
- period: 1m1s
average: 70
burst: 71
- period: 10m10s
average: 170
burst: 171
keys:
- cert: node.cert
private_key: !!binary cHJpdmF0ZSBrZXk=
- cert_file: /proxy.cert.file
private_key_file: /proxy.key.file
auth_service:
enabled: yes
listen_addr: auth:3025
tokens:
- "proxy,node:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
- "auth:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
authorities:
- type: host
domain_name: example.com
checking_keys:
- checking key 1
checking_key_files:
- /ca.checking.key
signing_keys:
- !!binary c2lnbmluZyBrZXkgMQ==
signing_key_files:
- /ca.signing.key
reverse_tunnels:
- domain_name: tunnel.example.com
addresses: ["com-1", "com-2"]
- domain_name: tunnel.example.org
addresses: ["org-1"]
ssh_service:
enabled: no
listen_addr: ssh:3025
labels:
name: mongoserver
role: slave
commands:
- name: hostname
command: [/bin/hostname]
period: 10ms
- name: date
command: [/bin/date]
period: 20ms
`
SmallConfigString = `
teleport:
nodename: cat.example.com
advertise_ip: 10.10.10.1
pid_file: /var/run/teleport.pid
auth_servers:
- auth0.server.example.org:3024
- auth1.server.example.org:3024
auth_token: xxxyyy
log:
output: stderr
severity: INFO
connection_limits:
max_connections: 90
max_users: 91
rates:
- period: 1m1s
average: 70
burst: 71
- period: 10m10s
average: 170
burst: 171
auth_service:
enabled: yes
listen_addr: 10.5.5.1:3025
cluster_name: magadan
tokens:
- "proxy,node:xxx"
- "auth:yyy"
ssh_service:
enabled: no
proxy_service:
enabled: yes
web_listen_addr: webhost
tunnel_listen_addr: tunnelhost:1001
`
)

View file

@ -1,5 +1,5 @@
/*
Copyright 2015-16 Gravitational, Inc.
Copyright 2015 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -107,6 +107,12 @@ var (
"u2f": true,
"app_id": true,
"facets": true,
"authentication": true,
"second_factor": false,
"oidc": true,
"display": false,
"scope": false,
"claims_to_roles": true,
}
)
@ -236,10 +242,6 @@ func MakeSampleFileConfig() (fc *FileConfig) {
a.EnabledFlag = "yes"
a.StaticTokens = []StaticToken{"proxy,node:cluster-join-token"}
a.U2F.EnabledFlag = "yes"
a.U2F.AppID = conf.Auth.U2F.AppID
a.U2F.Facets = conf.Auth.U2F.Facets
// sample proxy config:
var p Proxy
p.EnabledFlag = "yes"
@ -341,6 +343,7 @@ func (s *Service) Disabled() bool {
// Auth is 'auth_service' section of the config file
type Auth struct {
Service `yaml:",inline"`
// DomainName is the name of the CA who manages this cluster
DomainName string `yaml:"cluster_name,omitempty"`
@ -357,9 +360,6 @@ type Auth struct {
// to 3rd party auth servers we trust)
ReverseTunnels []ReverseTunnel `yaml:"reverse_tunnels,omitempty"`
// OIDCConnectors is a list of trusted OpenID Connect Identity providers
OIDCConnectors []OIDCConnector `yaml:"oidc_connectors"`
// StaticTokens are pre-defined host provisioning tokens supplied via config file for
// environments where paranoid security is not needed
//
@ -367,7 +367,16 @@ type Auth struct {
// for exmple: "auth,proxy,node:MTIzNGlvemRmOWE4MjNoaQo"
StaticTokens []StaticToken `yaml:"tokens,omitempty"`
// Authentication holds authentication configuration information like authentication
// type, second factor type, specific connector information, etc.
Authentication *AuthenticationConfig `yaml:"authentication,omitempty"`
// OIDCConnectors is a list of trusted OpenID Connect Identity providers
// Deprecated: Use OIDC section in Authentication section instead.
OIDCConnectors []OIDCConnector `yaml:"oidc_connectors"`
// Configuration for "universal 2nd factor"
// Deprecated: Use U2F section in Authentication section instead.
U2F U2F `yaml:"u2f,omitempty"`
}
@ -384,6 +393,74 @@ type TrustedCluster struct {
type StaticToken string
// Parse is applied to a string in "role,role,role:token" format. It breaks it
// apart into a slice of roles, token and optional error
func (t StaticToken) Parse() (roles teleport.Roles, token string, err error) {
parts := strings.Split(string(t), ":")
if len(parts) != 2 {
return nil, "", trace.BadParameter("invalid static token spec: %q", t)
}
roles, err = teleport.ParseRoles(parts[0])
return roles, parts[1], trace.Wrap(err)
}
type AuthenticationConfig struct {
Type string `yaml:"type"`
SecondFactor string `yaml:"second_factor,omitempty"`
U2F *UniversalSecondFactor `yaml:"u2f,omitempty"`
OIDC *OIDCConnector `yaml:"oidc,omitempty"`
}
// Parse returns the Authentication Configuration in three parts, the AuthPreference (type and second factor) as well
// as OIDCConnector and UniversalSecondFactor that define how those two are configured (if provided).
func (a *AuthenticationConfig) Parse() (services.AuthPreference, services.OIDCConnector, services.UniversalSecondFactor, error) {
var err error
ap, err := services.NewAuthPreference(services.AuthPreferenceSpecV2{
Type: a.Type,
SecondFactor: a.SecondFactor,
})
if err != nil {
return nil, nil, nil, trace.Wrap(err)
}
// check to make sure the configuration is valid
err = ap.Check()
if err != nil {
return nil, nil, nil, trace.Wrap(err)
}
var oidcConnector services.OIDCConnector
if a.OIDC != nil {
oidcConnector, err = a.OIDC.Parse()
if err != nil {
return nil, nil, nil, trace.Wrap(err)
}
}
var universalSecondFactor services.UniversalSecondFactor
if a.U2F != nil {
universalSecondFactor, err = a.U2F.Parse()
if err != nil {
return nil, nil, nil, trace.Wrap(err)
}
}
return ap, oidcConnector, universalSecondFactor, nil
}
type UniversalSecondFactor struct {
AppID string `yaml:"app_id"`
Facets []string `yaml:"facets"`
}
func (u *UniversalSecondFactor) Parse() (services.UniversalSecondFactor, error) {
return services.NewUniversalSecondFactor(services.UniversalSecondFactorSpecV2{
AppID: u.AppID,
Facets: u.Facets,
})
}
// SSH is 'ssh_service' section of the config file
type SSH struct {
Service `yaml:",inline"`
@ -597,17 +674,6 @@ func (o *OIDCConnector) Parse() (services.OIDCConnector, error) {
return v2, nil
}
// Parse() is applied to a string in "role,role,role:token" format. It breaks it
// apart into a slice of roles, token and optional error
func (t StaticToken) Parse() (roles teleport.Roles, token string, err error) {
parts := strings.Split(string(t), ":")
if len(parts) != 2 {
return nil, "", trace.Errorf("invalid static token spec: '%s'", t)
}
roles, err = teleport.ParseRoles(parts[0])
return roles, parts[1], trace.Wrap(err)
}
type U2F struct {
EnabledFlag string `yaml:"enabled"`
AppID string `yaml:"app_id,omitempty"`

177
lib/config/fileconf_test.go Normal file
View file

@ -0,0 +1,177 @@
/*
Copyright 2015 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package config
import (
"encoding/base64"
"fmt"
"gopkg.in/check.v1"
)
type FileTestSuite struct {
}
var _ = check.Suite(&FileTestSuite{})
var _ = fmt.Printf
func (s *FileTestSuite) SetUpSuite(c *check.C) {
}
func (s *FileTestSuite) TearDownSuite(c *check.C) {
}
func (s *FileTestSuite) SetUpTest(c *check.C) {
}
func (s *FileTestSuite) TestAuthenticationSection(c *check.C) {
tests := []struct {
inConfigString string
outAuthenticationConfig *AuthenticationConfig
}{
// 0 - local with otp
{
`
auth_service:
authentication:
type: local
second_factor: otp
`,
&AuthenticationConfig{
Type: "local",
SecondFactor: "otp",
},
},
// 1 - local auth without otp
{
`
auth_service:
authentication:
type: local
second_factor: off
`,
&AuthenticationConfig{
Type: "local",
SecondFactor: "off",
},
},
// 2 - local auth with u2f
{
`
auth_service:
authentication:
type: local
second_factor: u2f
u2f:
app_id: https://graviton:3080
facets:
- https://graviton:3080
`,
&AuthenticationConfig{
Type: "local",
SecondFactor: "u2f",
U2F: &UniversalSecondFactor{
AppID: "https://graviton:3080",
Facets: []string{
"https://graviton:3080",
},
},
},
},
// 3 - oidc without second factor
{
`
auth_service:
authentication:
type: oidc
oidc:
id: google
redirect_url: "https://localhost:3080/v1/webapi/oidc/callback"
client_id: id-from-google.apps.googleusercontent.com
client_secret: secret-key-from-google
issuer_url: "https://accounts.google.com"
display: whaterver
scope: [ "ssh_permissions", "roles"]
claims_to_roles:
- claim: role
value: admin
roles: ["dba", "backup", "root"]
`,
&AuthenticationConfig{
Type: "oidc",
OIDC: &OIDCConnector{
ID: "google",
RedirectURL: "https://localhost:3080/v1/webapi/oidc/callback",
ClientID: "id-from-google.apps.googleusercontent.com",
ClientSecret: "secret-key-from-google",
IssuerURL: "https://accounts.google.com",
Display: "whaterver",
Scope: []string{
"ssh_permissions",
"roles",
},
ClaimsToRoles: []ClaimMapping{
ClaimMapping{
Claim: "role",
Value: "admin",
Roles: []string{
"dba",
"backup",
"root",
},
},
},
},
},
},
}
// run tests
for i, tt := range tests {
comment := check.Commentf("Test %v", i)
encodedConfigString := base64.StdEncoding.EncodeToString([]byte(tt.inConfigString))
fc, err := ReadFromString(encodedConfigString)
c.Assert(err, check.IsNil, comment)
c.Assert(fc.Auth.Authentication, check.DeepEquals, tt.outAuthenticationConfig, comment)
}
}
// TestLegacySection ensures we continue to parse and correctly load deprecated
// OIDC connector and U2F authentication configuration.
func (s *FileTestSuite) TestLegacyAuthenticationSection(c *check.C) {
encodedLegacyAuthenticationSection := base64.StdEncoding.EncodeToString([]byte(LegacyAuthenticationSection))
// read config into struct
fc, err := ReadFromString(encodedLegacyAuthenticationSection)
c.Assert(err, check.IsNil)
// validate oidc connector
c.Assert(fc.Auth.OIDCConnectors, check.HasLen, 1)
c.Assert(fc.Auth.OIDCConnectors[0].ID, check.Equals, "google")
c.Assert(fc.Auth.OIDCConnectors[0].RedirectURL, check.Equals, "https://localhost:3080/v1/webapi/oidc/callback")
c.Assert(fc.Auth.OIDCConnectors[0].ClientID, check.Equals, "id-from-google.apps.googleusercontent.com")
c.Assert(fc.Auth.OIDCConnectors[0].ClientSecret, check.Equals, "secret-key-from-google")
c.Assert(fc.Auth.OIDCConnectors[0].IssuerURL, check.Equals, "https://accounts.google.com")
// validate u2f
c.Assert(fc.Auth.U2F.EnabledFlag, check.Equals, "yes")
c.Assert(fc.Auth.U2F.AppID, check.Equals, "https://graviton:3080")
c.Assert(fc.Auth.U2F.Facets, check.HasLen, 1)
c.Assert(fc.Auth.U2F.Facets[0], check.Equals, "https://graviton:3080")
}

146
lib/config/testdata_test.go Normal file
View file

@ -0,0 +1,146 @@
/*
Copyright 2015 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package config
const StaticConfigString = `
#
# Some comments
#
teleport:
nodename: edsger.example.com
advertise_ip: 10.10.10.1
pid_file: /var/run/teleport.pid
auth_servers:
- auth0.server.example.org:3024
- auth1.server.example.org:3024
auth_token: xxxyyy
log:
output: stderr
severity: INFO
storage:
type: etcd
peers: ['one', 'two']
tls_key_file: /tls.key
tls_cert_file: /tls.cert
tls_ca_file: /tls.ca
connection_limits:
max_connections: 90
max_users: 91
rates:
- period: 1m1s
average: 70
burst: 71
- period: 10m10s
average: 170
burst: 171
keys:
- cert: node.cert
private_key: !!binary cHJpdmF0ZSBrZXk=
- cert_file: /proxy.cert.file
private_key_file: /proxy.key.file
auth_service:
enabled: yes
listen_addr: auth:3025
tokens:
- "proxy,node:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
- "auth:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
authorities:
- type: host
domain_name: example.com
checking_keys:
- checking key 1
checking_key_files:
- /ca.checking.key
signing_keys:
- !!binary c2lnbmluZyBrZXkgMQ==
signing_key_files:
- /ca.signing.key
reverse_tunnels:
- domain_name: tunnel.example.com
addresses: ["com-1", "com-2"]
- domain_name: tunnel.example.org
addresses: ["org-1"]
ssh_service:
enabled: no
listen_addr: ssh:3025
labels:
name: mongoserver
role: slave
commands:
- name: hostname
command: [/bin/hostname]
period: 10ms
- name: date
command: [/bin/date]
period: 20ms
`
const SmallConfigString = `
teleport:
nodename: cat.example.com
advertise_ip: 10.10.10.1
pid_file: /var/run/teleport.pid
auth_servers:
- auth0.server.example.org:3024
- auth1.server.example.org:3024
auth_token: xxxyyy
log:
output: stderr
severity: INFO
connection_limits:
max_connections: 90
max_users: 91
rates:
- period: 1m1s
average: 70
burst: 71
- period: 10m10s
average: 170
burst: 171
auth_service:
enabled: yes
listen_addr: 10.5.5.1:3025
cluster_name: magadan
tokens:
- "proxy,node:xxx"
- "auth:yyy"
ssh_service:
enabled: no
proxy_service:
enabled: yes
web_listen_addr: webhost
tunnel_listen_addr: tunnelhost:1001
`
// LegacyAuthenticationSection is the deprecated format for authentication method. We still
// need to support it until it's fully removed.
const LegacyAuthenticationSection = `
auth_service:
oidc_connectors:
- id: google
redirect_url: https://localhost:3080/v1/webapi/oidc/callback
client_id: id-from-google.apps.googleusercontent.com
client_secret: secret-key-from-google
issuer_url: https://accounts.google.com
u2f:
enabled: "yes"
app_id: https://graviton:3080
facets:
- https://graviton:3080
`

View file

@ -17,11 +17,9 @@ limitations under the License.
package service
import (
"fmt"
"io"
"net"
"os"
"strings"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/backend"
@ -209,7 +207,12 @@ type AuthConfig struct {
// NoAudit, when set to true, disables session recording and event audit
NoAudit bool
U2F services.U2F
// Preference defines the authentication preference (type and second factor) for
// the auth server.
Preference services.AuthPreference
// U2F defines is settings for Universal Second Factor (appID and facets).
U2F services.UniversalSecondFactor
}
// SSHConfig configures SSH server node role
@ -247,9 +250,6 @@ func ApplyDefaults(cfg *Config) {
// defaults for the auth service:
cfg.Auth.Enabled = true
cfg.Auth.SSHAddr = *defaults.AuthListenAddr()
cfg.Auth.U2F.Enabled = false
cfg.Auth.U2F.AppID = fmt.Sprintf("https://%s:%d", strings.ToLower(hostname), defaults.HTTPListenPort)
cfg.Auth.U2F.Facets = []string{cfg.Auth.U2F.AppID}
cfg.Auth.StorageConfig.Type = boltbk.GetName()
cfg.Auth.StorageConfig.Params = backend.Params{"path": cfg.DataDir}
defaults.ConfigureLimiter(&cfg.Auth.Limiter)

View file

@ -324,15 +324,16 @@ func (process *TeleportProcess) initAuthService(authority auth.Authority) error
NodeName: cfg.Hostname,
Authorities: cfg.Auth.Authorities,
ReverseTunnels: cfg.ReverseTunnels,
OIDCConnectors: cfg.OIDCConnectors,
Trust: cfg.Trust,
Presence: cfg.Presence,
Provisioner: cfg.Provisioner,
Identity: cfg.Identity,
Access: cfg.Access,
StaticTokens: cfg.Auth.StaticTokens,
U2F: cfg.Auth.U2F,
Roles: cfg.Auth.Roles,
AuthPreference: cfg.Auth.Preference,
OIDCConnectors: cfg.OIDCConnectors,
U2F: cfg.Auth.U2F,
}, cfg.SeedConfig)
if err != nil {
return trace.Wrap(err)

View file

@ -0,0 +1,197 @@
/*
Copyright 2017 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package services
import (
"encoding/json"
"fmt"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/trace"
)
// ClusterAuthPreference defines an interface to get and set
// authentication preferences for a cluster.
type ClusterAuthPreference interface {
// GetClusterAuthPreference returns the authentication preferences for a cluster.
GetClusterAuthPreference() (AuthPreference, error)
// SetClusterAuthPreference sets the authentication preferences for a cluster.
SetClusterAuthPreference(AuthPreference) error
}
// AuthPreference defines the authentication preferences for a specific
// cluster. It defines the type (local, oidc) and second factor (off, otp, oidc).
type AuthPreference interface {
// GetType returns the type of authentication.
GetType() string
// SetType sets the type of authentication.
SetType(string)
// GetSecondFactor returns the type of second factor.
GetSecondFactor() string
// SetSecondFactor sets the type of second factor.
SetSecondFactor(string)
// Check verifies the constraints for AuthPreference.
Check() error
}
// NewAuthPreference is a convenience method to to create AuthPreferenceV2.
func NewAuthPreference(spec AuthPreferenceSpecV2) (AuthPreference, error) {
return &AuthPreferenceV2{
Kind: KindClusterAuthPreference,
Version: V2,
Metadata: Metadata{
Name: MetaNameClusterAuthPreference,
Namespace: defaults.Namespace,
},
Spec: spec,
}, nil
}
// AuthPreferenceV2 implements AuthPreference.
type AuthPreferenceV2 struct {
// Kind is a resource kind - always resource.
Kind string `json:"kind"`
// Version is a resource version.
Version string `json:"version"`
// Metadata is metadata about the resource.
Metadata Metadata `json:"metadata"`
// Spec is the specification of the resource.
Spec AuthPreferenceSpecV2 `json:"spec"`
}
// AuthPreferenceSpecV2 is the actual data we care about for AuthPreferenceV2.
type AuthPreferenceSpecV2 struct {
// Type is the type of authentication.
Type string `json:"type"`
// SecondFactor is the type of second factor.
SecondFactor string `json:"second_factor"`
}
// GetType returns the type of authentication.
func (c *AuthPreferenceV2) GetType() string {
return c.Spec.Type
}
// SetType sets the type of authentication.
func (c *AuthPreferenceV2) SetType(s string) {
c.Spec.Type = s
}
// GetSecondFactor returns the type of second factor.
func (c *AuthPreferenceV2) GetSecondFactor() string {
return c.Spec.SecondFactor
}
// SetSecondFactor sets the type of second factor.
func (c *AuthPreferenceV2) SetSecondFactor(s string) {
c.Spec.SecondFactor = s
}
// Check verifies the constraints for AuthPreference.
func (c *AuthPreferenceV2) Check() error {
switch c.Spec.Type {
case teleport.Local:
if c.Spec.SecondFactor != teleport.OFF && c.Spec.SecondFactor != teleport.OTP && c.Spec.SecondFactor != teleport.U2F {
return trace.BadParameter("second factor type %q not supported", c.Spec.SecondFactor)
}
case teleport.OIDC:
if c.Spec.SecondFactor != "" {
return trace.BadParameter("second factor not supported with oidc connector")
}
default:
return trace.BadParameter("unsupported type %q", c.Spec.Type)
}
return nil
}
const AuthPreferenceSpecSchemaTemplate = `{
"type": "object",
"additionalProperties": false,
"properties": {
"type": {"type": "string"},
"second_factor": {"type": "string"}%v
}
}`
// GetAuthPreferenceSchema returns the schema with optionally injected
// schema for extensions.
func GetAuthPreferenceSchema(extensionSchema string) string {
var authPreferenceSchema string
if authPreferenceSchema == "" {
authPreferenceSchema = fmt.Sprintf(AuthPreferenceSpecSchemaTemplate, "")
} else {
authPreferenceSchema = fmt.Sprintf(AuthPreferenceSpecSchemaTemplate, ","+extensionSchema)
}
return fmt.Sprintf(V2SchemaTemplate, MetadataSchema, authPreferenceSchema)
}
// AuthPreferenceMarshaler implements marshal/unmarshal of AuthPreference implementations
// mostly adds support for extended versions.
type AuthPreferenceMarshaler interface {
Marshal(c AuthPreference, opts ...MarshalOption) ([]byte, error)
Unmarshal(bytes []byte) (AuthPreference, error)
}
var authPreferenceMarshaler AuthPreferenceMarshaler = &TeleportAuthPreferenceMarshaler{}
func SetAuthPreferenceMarshaler(m AuthPreferenceMarshaler) {
marshalerMutex.Lock()
defer marshalerMutex.Unlock()
authPreferenceMarshaler = m
}
func GetAuthPreferenceMarshaler() AuthPreferenceMarshaler {
marshalerMutex.Lock()
defer marshalerMutex.Unlock()
return authPreferenceMarshaler
}
type TeleportAuthPreferenceMarshaler struct{}
// Unmarshal unmarshals role from JSON or YAML.
func (t *TeleportAuthPreferenceMarshaler) Unmarshal(bytes []byte) (AuthPreference, error) {
var authPreference AuthPreferenceV2
if len(bytes) == 0 {
return nil, trace.BadParameter("missing resource data")
}
err := utils.UnmarshalWithSchema(GetAuthPreferenceSchema(""), &authPreference, bytes)
if err != nil {
return nil, trace.BadParameter(err.Error())
}
return &authPreference, nil
}
// Marshal marshals role to JSON or YAML.
func (t *TeleportAuthPreferenceMarshaler) Marshal(c AuthPreference, opts ...MarshalOption) ([]byte, error) {
return json.Marshal(c)
}

View file

@ -0,0 +1,67 @@
/*
Copyright 2017 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package services
import (
"fmt"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/utils"
"gopkg.in/check.v1"
)
type ClusterAuthPreferenceSuite struct{}
var _ = check.Suite(&ClusterAuthPreferenceSuite{})
var _ = fmt.Printf
func (s *ClusterAuthPreferenceSuite) SetUpSuite(c *check.C) {
utils.InitLoggerForTests()
}
func (s *ClusterAuthPreferenceSuite) TestUnmarshal(c *check.C) {
input := `
{
"kind": "cluster_auth_preference",
"metadata": {
"name": "cluster-auth-preference"
},
"spec": {
"type": "local",
"second_factor": "otp"
}
}
`
output := AuthPreferenceV2{
Kind: KindClusterAuthPreference,
Version: V2,
Metadata: Metadata{
Name: MetaNameClusterAuthPreference,
Namespace: defaults.Namespace,
},
Spec: AuthPreferenceSpecV2{
Type: "local",
SecondFactor: "otp",
},
}
ap, err := GetAuthPreferenceMarshaler().Unmarshal([]byte(input))
c.Assert(err, check.IsNil)
c.Assert(ap, check.DeepEquals, &output)
}

View file

@ -282,6 +282,7 @@ func (i *OIDCAuthRequest) Check() error {
}
// U2F is a configuration of the U2F two factor authentication
// Deprecated: Use services.UniversalSecondFactor instead.
type U2F struct {
Enabled bool
// AppID identifies the website to the U2F keys. It should not be changed once a U2F

View file

@ -0,0 +1,65 @@
/*
Copyright 2017 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package local
import (
"github.com/gravitational/teleport/lib/backend"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/trace"
)
// ClusterAuthPreferenceService is responsible for managing cluster authentication preferences.
type ClusterAuthPreferenceService struct {
backend.Backend
}
// NewClusterAuthPreferenceService returns a new ClusterAuthPreferenceService.
func NewClusterAuthPreferenceService(backend backend.Backend) *ClusterAuthPreferenceService {
return &ClusterAuthPreferenceService{
Backend: backend,
}
}
// GetClusterAuthPreference fetches the cluster authentication preferences
// from the backend and return them.
func (s *ClusterAuthPreferenceService) GetClusterAuthPreference() (services.AuthPreference, error) {
data, err := s.GetVal([]string{"authentication"}, "preference")
if err != nil {
if trace.IsNotFound(err) {
return nil, trace.NotFound("authentication preference not found")
}
return nil, trace.Wrap(err)
}
return services.GetAuthPreferenceMarshaler().Unmarshal(data)
}
// SetClusterAuthPreference sets the cluster authentication preferences
// on the backend.
func (s *ClusterAuthPreferenceService) SetClusterAuthPreference(preferences services.AuthPreference) error {
data, err := services.GetAuthPreferenceMarshaler().Marshal(preferences)
if err != nil {
return trace.Wrap(err)
}
err = s.UpsertVal([]string{"authentication"}, "preference", []byte(data), backend.Forever)
if err != nil {
return trace.Wrap(err)
}
return nil
}

View file

@ -0,0 +1,83 @@
/*
Copyright 2017 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package local
import (
"fmt"
"io/ioutil"
"os"
"github.com/gravitational/teleport/lib/backend"
"github.com/gravitational/teleport/lib/backend/boltbk"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/utils"
"gopkg.in/check.v1"
)
type ClusterAuthPreferenceSuite struct {
bk backend.Backend
tempDir string
}
var _ = check.Suite(&ClusterAuthPreferenceSuite{})
var _ = fmt.Printf
func (s *ClusterAuthPreferenceSuite) SetUpSuite(c *check.C) {
utils.InitLoggerForTests()
}
func (s *ClusterAuthPreferenceSuite) TearDownSuite(c *check.C) {
}
func (s *ClusterAuthPreferenceSuite) SetUpTest(c *check.C) {
var err error
s.tempDir, err = ioutil.TempDir("", "preference-test-")
c.Assert(err, check.IsNil)
s.bk, err = boltbk.New(backend.Params{"path": s.tempDir})
c.Assert(err, check.IsNil)
}
func (s *ClusterAuthPreferenceSuite) TearDownTest(c *check.C) {
var err error
c.Assert(s.bk.Close(), check.IsNil)
err = os.RemoveAll(s.tempDir)
c.Assert(err, check.IsNil)
}
func (s *ClusterAuthPreferenceSuite) TestCycle(c *check.C) {
caps := NewClusterAuthPreferenceService(s.bk)
ap, err := services.NewAuthPreference(services.AuthPreferenceSpecV2{
Type: "local",
SecondFactor: "otp",
})
c.Assert(err, check.IsNil)
err = caps.SetClusterAuthPreference(ap)
c.Assert(err, check.IsNil)
gotAP, err := caps.GetClusterAuthPreference()
c.Assert(err, check.IsNil)
c.Assert(gotAP.GetType(), check.Equals, "local")
c.Assert(gotAP.GetSecondFactor(), check.Equals, "otp")
}

65
lib/services/local/u2f.go Normal file
View file

@ -0,0 +1,65 @@
/*
Copyright 2017 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package local
import (
"github.com/gravitational/teleport/lib/backend"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/trace"
)
// UniversalSecondFactorService is responsible for managing universal second factor settings.
type UniversalSecondFactorService struct {
backend.Backend
}
// NewUniversalSecondFactorService returns a new UniversalSecondFactorService.
func NewUniversalSecondFactorService(backend backend.Backend) *UniversalSecondFactorService {
return &UniversalSecondFactorService{
Backend: backend,
}
}
// GetUniversalSecondFactor fetches the universal second factor settings
// from the backend and returns them.
func (s *UniversalSecondFactorService) GetUniversalSecondFactor() (services.UniversalSecondFactor, error) {
data, err := s.GetVal([]string{"authentication", "preference"}, "u2f")
if err != nil {
if trace.IsNotFound(err) {
return nil, trace.NotFound("no universal second factor settings")
}
return nil, trace.Wrap(err)
}
return services.GetUniversalSecondFactorMarshaler().Unmarshal(data)
}
// GetUniversalSecondFactor sets the universal second factor settings
// on the backend.
func (s *UniversalSecondFactorService) SetUniversalSecondFactor(settings services.UniversalSecondFactor) error {
data, err := services.GetUniversalSecondFactorMarshaler().Marshal(settings)
if err != nil {
return trace.Wrap(err)
}
err = s.UpsertVal([]string{"authentication", "preference"}, "u2f", []byte(data), backend.Forever)
if err != nil {
return trace.Wrap(err)
}
return nil
}

View file

@ -1,3 +1,19 @@
/*
Copyright 2015 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package services
import (

View file

@ -1,3 +1,19 @@
/*
Copyright 2015 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package services
import (
@ -74,6 +90,18 @@ const (
// KindOIDCConnector is a OIDC connector resource
KindOIDCConnector = "oidc"
// KindAuthPreference is the type of authentication for this cluster.
KindClusterAuthPreference = "cluster_auth_preference"
// KindAuthPreference is the type of authentication for this cluster.
MetaNameClusterAuthPreference = "cluster-auth-preference"
// KindUniversalSecondFactor is a type of second factor authentication.
KindUniversalSecondFactor = "universal_second_factor"
// MetaNameUniversalSecondFactor is a type of second factor authentication.
MetaNameUniversalSecondFactor = "universal-second-factor"
// V2 is our current version
V2 = "v2"

179
lib/services/u2f.go Normal file
View file

@ -0,0 +1,179 @@
/*
Copyright 2017 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package services
import (
"encoding/json"
"fmt"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/trace"
)
// UniversalSecondFactorSettings defines the interface to get and set
// Universal Second Factor settings.
type UniversalSecondFactorSettings interface {
// GetUniversalSecondFactor returns universal second factor settings.
GetUniversalSecondFactor() (UniversalSecondFactor, error)
// SetUniversalSecondFactor sets universal second factor settings.
SetUniversalSecondFactor(UniversalSecondFactor) error
}
// UniversalSecondFactor defines settings for Universal Second Factor
// like the AppID and Facets.
type UniversalSecondFactor interface {
// GetAppID returns the application ID for universal second factor.
GetAppID() string
// SetAppID sets the application ID for universal second factor.
SetAppID(string)
// GetFacets returns the facets for universal second factor.
GetFacets() []string
// SetFacets sets the facets for universal second factor.
SetFacets([]string)
}
// NewUniversalSecondFactor is a convenience method to to create UniversalSecondFactorV2.
func NewUniversalSecondFactor(spec UniversalSecondFactorSpecV2) (UniversalSecondFactor, error) {
return &UniversalSecondFactorV2{
Kind: KindUniversalSecondFactor,
Version: V2,
Metadata: Metadata{
Name: MetaNameUniversalSecondFactor,
Namespace: defaults.Namespace,
},
Spec: spec,
}, nil
}
// UniversalSecondFactorV2 implements UniversalSecondFactor.
type UniversalSecondFactorV2 struct {
// Kind is a resource kind - always resource.
Kind string `json:"kind"`
// Version is a resource version.
Version string `json:"version"`
// Metadata is metadata about the resource.
Metadata Metadata `json:"metadata"`
// Spec is the specification of the resource.
Spec UniversalSecondFactorSpecV2 `json:"spec"`
}
// UniversalSecondFactorSpecV2 is the actual data we care about for UniversalSecondFactorV2.
type UniversalSecondFactorSpecV2 struct {
// AppID is the application ID for universal second factor.
AppID string `json:"app_id"`
// Facets are the facets for universal second factor.
Facets []string `json:"facets"`
}
// GetAppID returns the application ID for universal second factor.
func (c *UniversalSecondFactorV2) GetAppID() string {
return c.Spec.AppID
}
// SetAppID sets the application ID for universal second factor.
func (c *UniversalSecondFactorV2) SetAppID(s string) {
c.Spec.AppID = s
}
// GetFacets returns the facets for universal second factor.
func (c *UniversalSecondFactorV2) GetFacets() []string {
return c.Spec.Facets
}
// SetFacets sets the facets for universal second factor.
func (c *UniversalSecondFactorV2) SetFacets(s []string) {
c.Spec.Facets = s
}
const UniversalSecondFactorSpecSchemaTemplate = `{
"type": "object",
"additionalProperties": false,
"properties": {
"app_id": {"type": "string"},
"facets": {
"type": "array",
"items": {
"type": "string"
}
}%v
}
}`
// GetUniversalSecondFactorSchema returns the schema with optionally injected
// schema for extensions.
func GetUniversalSecondFactorSchema(extensionSchema string) string {
var authPreferenceSchema string
if authPreferenceSchema == "" {
authPreferenceSchema = fmt.Sprintf(UniversalSecondFactorSpecSchemaTemplate, "")
} else {
authPreferenceSchema = fmt.Sprintf(UniversalSecondFactorSpecSchemaTemplate, ","+extensionSchema)
}
return fmt.Sprintf(V2SchemaTemplate, MetadataSchema, authPreferenceSchema)
}
// UniversalSecondFactorMarshaler implements marshal/unmarshal of UniversalSecondFactor implementations
// mostly adds support for extended versions.
type UniversalSecondFactorMarshaler interface {
Marshal(c UniversalSecondFactor, opts ...MarshalOption) ([]byte, error)
Unmarshal(bytes []byte) (UniversalSecondFactor, error)
}
var universalSecondFactorMarshaler UniversalSecondFactorMarshaler = &TeleportUniversalSecondFactorMarshaler{}
func SetUniversalSecondFactorMarshaler(m UniversalSecondFactorMarshaler) {
marshalerMutex.Lock()
defer marshalerMutex.Unlock()
universalSecondFactorMarshaler = m
}
func GetUniversalSecondFactorMarshaler() UniversalSecondFactorMarshaler {
marshalerMutex.Lock()
defer marshalerMutex.Unlock()
return universalSecondFactorMarshaler
}
type TeleportUniversalSecondFactorMarshaler struct{}
// Unmarshal unmarshals role from JSON or YAML.
func (t *TeleportUniversalSecondFactorMarshaler) Unmarshal(bytes []byte) (UniversalSecondFactor, error) {
var authPreference UniversalSecondFactorV2
if len(bytes) == 0 {
return nil, trace.BadParameter("missing resource data")
}
err := utils.UnmarshalWithSchema(GetUniversalSecondFactorSchema(""), &authPreference, bytes)
if err != nil {
return nil, trace.BadParameter(err.Error())
}
return &authPreference, nil
}
// Marshal marshals role to JSON or YAML.
func (t *TeleportUniversalSecondFactorMarshaler) Marshal(c UniversalSecondFactor, opts ...MarshalOption) ([]byte, error) {
return json.Marshal(c)
}

View file

@ -312,11 +312,15 @@ func (m *Handler) getSettings(w http.ResponseWriter, r *http.Request) (interface
if len(settings.Auth.OIDCConnectors) == 0 {
settings.Auth.OIDCConnectors = make([]oidcConnector, 0)
}
u2fAppID, err := m.cfg.ProxyClient.GetU2FAppID()
universalSecondFactor, err := m.cfg.ProxyClient.GetUniversalSecondFactor()
if err != nil {
return nil, trace.Wrap(err)
}
if err != nil {
settings.Auth.U2FAppID = ""
} else {
settings.Auth.U2FAppID = u2fAppID
settings.Auth.U2FAppID = universalSecondFactor.GetAppID()
}
out, err := json.Marshal(settings)
if err != nil {

View file

@ -145,15 +145,19 @@ func (s *WebSuite) SetUpTest(c *C) {
Backend: s.bk,
Authority: authority.New(),
DomainName: s.domainName,
U2F: services.U2F{
Enabled: true,
AppID: "https://" + s.domainName,
Facets: []string{"https://" + s.domainName},
},
Identity: identity,
Access: access,
Identity: identity,
Access: access,
})
// configure u2f
universalSecondFactor, err := services.NewUniversalSecondFactor(services.UniversalSecondFactorSpecV2{
AppID: "https://" + s.domainName,
Facets: []string{"https://" + s.domainName},
})
c.Assert(err, IsNil)
err = authServer.SetUniversalSecondFactor(universalSecondFactor)
c.Assert(err, IsNil)
teleUser, err := services.NewUser(s.user)
c.Assert(err, IsNil)
role := services.RoleForUser(teleUser)