As we reevaluate how to best support and maintain Staging Ref in the future, we encourage development teams using this environment to highlight their use cases in the following issue: https://gitlab.com/gitlab-com/gl-infra/software-delivery/framework/software-delivery-framework-issue-tracker/-/issues/36.

Skip to content
Snippets Groups Projects
Commit 91fa2312 authored by Ash McKenzie's avatar Ash McKenzie
Browse files

Merge branch 'id-ssh-certificates' into 'main'

parents b9daf6bf f3c80161
No related branches found
No related tags found
No related merge requests found
Loading
Loading
@@ -58,6 +58,20 @@ func NewWithKrb5Principal(gitlabKrb5Principal string, env sshenv.Env, config *co
return nil, disallowedcommand.Error
}
func NewWithUsername(gitlabUsername string, env sshenv.Env, config *config.Config, readWriter *readwriter.ReadWriter) (command.Command, error) {
args, err := Parse(nil, env)
if err != nil {
return nil, err
}
args.GitlabUsername = gitlabUsername
if cmd := Build(args, config, readWriter); cmd != nil {
return cmd, nil
}
return nil, disallowedcommand.Error
}
func Parse(arguments []string, env sshenv.Env) (*commandargs.Shell, error) {
args := &commandargs.Shell{Arguments: arguments, Env: env}
Loading
Loading
Loading
Loading
@@ -300,3 +300,16 @@ func TestParseFailure(t *testing.T) {
})
}
}
func TestNewWithUsername(t *testing.T) {
env := sshenv.Env{IsSSHConnection: true, OriginalCommand: "git-receive-pack 'group/repo'"}
c, err := cmd.NewWithUsername("username", env, nil, nil)
require.NoError(t, err)
require.IsType(t, &receivepack.Command{}, c)
require.Equal(t, c.(*receivepack.Command).Args.GitlabUsername, "username")
env = sshenv.Env{IsSSHConnection: true, OriginalCommand: "invalid"}
c, err = cmd.NewWithUsername("username", env, nil, nil)
require.Error(t, err)
require.Nil(t, c)
}
Loading
Loading
@@ -30,6 +30,9 @@ type Request struct {
Username string `json:"username,omitempty"`
Krb5Principal string `json:"krb5principal,omitempty"`
CheckIp string `json:"check_ip,omitempty"`
// NamespacePath is the full path of the namespace in which the authenticated
// user is allowed to perform operation.
NamespacePath string `json:"namespace_path,omitempty"`
}
type Gitaly struct {
Loading
Loading
@@ -81,7 +84,13 @@ func NewClient(config *config.Config) (*Client, error) {
}
func (c *Client) Verify(ctx context.Context, args *commandargs.Shell, action commandargs.CommandType, repo string) (*Response, error) {
request := &Request{Action: action, Repo: repo, Protocol: protocol, Changes: anyChanges}
request := &Request{
Action: action,
Repo: repo,
Protocol: protocol,
Changes: anyChanges,
NamespacePath: args.Env.NamespacePath,
}
if args.GitlabUsername != "" {
request.Username = args.GitlabUsername
Loading
Loading
Loading
Loading
@@ -19,9 +19,11 @@ import (
)
var (
namespace = "group"
repo = "group/private"
receivePackAction = commandargs.ReceivePack
uploadPackAction = commandargs.UploadPack
defaultEnv = sshenv.Env{NamespacePath: namespace}
)
func buildExpectedResponse(who string) *Response {
Loading
Loading
@@ -71,7 +73,7 @@ func TestSuccessfulResponses(t *testing.T) {
who: "key-1",
}, {
desc: "Provide username within the request",
args: &commandargs.Shell{GitlabUsername: "first"},
args: &commandargs.Shell{GitlabUsername: "first", Env: defaultEnv},
who: "user-1",
}, {
desc: "Provide krb5principal within the request",
Loading
Loading
@@ -99,7 +101,7 @@ func TestGeoPushGetCustomAction(t *testing.T) {
},
}, nil)
args := &commandargs.Shell{GitlabUsername: "custom"}
args := &commandargs.Shell{GitlabUsername: "custom", Env: defaultEnv}
result, err := client.Verify(context.Background(), args, receivePackAction, repo)
require.NoError(t, err)
Loading
Loading
@@ -128,7 +130,7 @@ func TestGeoPullGetCustomAction(t *testing.T) {
},
}, nil)
args := &commandargs.Shell{GitlabUsername: "custom"}
args := &commandargs.Shell{GitlabUsername: "custom", Env: defaultEnv}
result, err := client.Verify(context.Background(), args, uploadPackAction, repo)
require.NoError(t, err)
Loading
Loading
@@ -263,6 +265,7 @@ func setup(t *testing.T, userResponses, keyResponses map[string]testResponse) *C
w.WriteHeader(tr.status)
_, err := w.Write(tr.body)
require.NoError(t, err)
require.Equal(t, namespace, requestBody.NamespacePath)
} else if tr, ok := userResponses[requestBody.Krb5Principal]; ok {
w.WriteHeader(tr.status)
_, err := w.Write(tr.body)
Loading
Loading
package authorizedcerts
import (
"context"
"fmt"
"net/url"
"gitlab.com/gitlab-org/gitlab-shell/v14/client"
"gitlab.com/gitlab-org/gitlab-shell/v14/internal/config"
"gitlab.com/gitlab-org/gitlab-shell/v14/internal/gitlabnet"
)
const (
AuthorizedCertsPath = "/authorized_certs"
)
type Client struct {
config *config.Config
client *client.GitlabNetClient
}
type Response struct {
Username string `json:"username"`
Namespace string `json:"namespace"`
}
func NewClient(config *config.Config) (*Client, error) {
client, err := gitlabnet.GetClient(config)
if err != nil {
return nil, fmt.Errorf("Error creating http client: %v", err)
}
return &Client{config: config, client: client}, nil
}
func (c *Client) GetByKey(ctx context.Context, userId, fingerprint string) (*Response, error) {
path, err := pathWithKey(userId, fingerprint)
if err != nil {
return nil, err
}
response, err := c.client.Get(ctx, path)
if err != nil {
return nil, err
}
defer response.Body.Close()
parsedResponse := &Response{}
if err := gitlabnet.ParseJSON(response, parsedResponse); err != nil {
return nil, err
}
return parsedResponse, nil
}
func pathWithKey(userId, fingerprint string) (string, error) {
u, err := url.Parse(AuthorizedCertsPath)
if err != nil {
return "", err
}
params := u.Query()
params.Set("key", fingerprint)
params.Set("user_identifier", userId)
u.RawQuery = params.Encode()
return u.String(), nil
}
package authorizedcerts
import (
"context"
"encoding/json"
"net/http"
"testing"
"github.com/stretchr/testify/require"
"gitlab.com/gitlab-org/gitlab-shell/v14/client"
"gitlab.com/gitlab-org/gitlab-shell/v14/client/testserver"
"gitlab.com/gitlab-org/gitlab-shell/v14/internal/config"
)
var (
requests []testserver.TestRequestHandler
)
func init() {
requests = []testserver.TestRequestHandler{
{
Path: "/api/v4/internal/authorized_certs",
Handler: func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("key") == "key" {
body := &Response{
Namespace: "group",
Username: r.URL.Query().Get("user_identifier"),
}
json.NewEncoder(w).Encode(body)
} else if r.URL.Query().Get("key") == "broken-message" {
w.WriteHeader(http.StatusForbidden)
body := &client.ErrorResponse{
Message: "Not allowed!",
}
json.NewEncoder(w).Encode(body)
} else if r.URL.Query().Get("key") == "broken-json" {
w.Write([]byte("{ \"message\": \"broken json!\""))
} else if r.URL.Query().Get("key") == "broken-empty" {
w.WriteHeader(http.StatusForbidden)
} else {
w.WriteHeader(http.StatusNotFound)
}
},
},
}
}
func TestGetByKey(t *testing.T) {
client := setup(t)
result, err := client.GetByKey(context.Background(), "user-id", "key")
require.NoError(t, err)
require.Equal(t, &Response{Namespace: "group", Username: "user-id"}, result)
}
func TestGetByKeyErrorResponses(t *testing.T) {
client := setup(t)
testCases := []struct {
desc string
key string
expectedError string
}{
{
desc: "A response with an error message",
key: "broken-message",
expectedError: "Not allowed!",
},
{
desc: "A response with bad JSON",
key: "broken-json",
expectedError: "Parsing failed",
},
{
desc: "A forbidden (403) response without message",
key: "broken-empty",
expectedError: "Internal API error (403)",
},
{
desc: "A not found (404) response without message",
key: "not-found",
expectedError: "Internal API error (404)",
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
resp, err := client.GetByKey(context.Background(), "user-id", tc.key)
require.EqualError(t, err, tc.expectedError)
require.Nil(t, resp)
})
}
}
func setup(t *testing.T) *Client {
url := testserver.StartSocketHttpServer(t, requests)
client, err := NewClient(&config.Config{GitlabUrl: url})
require.NoError(t, err)
return client
}
Loading
Loading
@@ -6,11 +6,13 @@ import (
"fmt"
"os"
"strconv"
"strings"
"time"
"golang.org/x/crypto/ssh"
"gitlab.com/gitlab-org/gitlab-shell/v14/internal/config"
"gitlab.com/gitlab-org/gitlab-shell/v14/internal/gitlabnet/authorizedcerts"
"gitlab.com/gitlab-org/gitlab-shell/v14/internal/gitlabnet/authorizedkeys"
"gitlab.com/gitlab-org/labkit/log"
Loading
Loading
@@ -37,10 +39,11 @@ var (
)
type serverConfig struct {
cfg *config.Config
hostKeys []ssh.Signer
hostKeyToCertMap map[string]*ssh.Certificate
authorizedKeysClient *authorizedkeys.Client
cfg *config.Config
hostKeys []ssh.Signer
hostKeyToCertMap map[string]*ssh.Certificate
authorizedKeysClient *authorizedkeys.Client
authorizedCertsClient *authorizedcerts.Client
}
func parseHostKeys(keyFiles []string) []ssh.Signer {
Loading
Loading
@@ -113,7 +116,12 @@ func parseHostCerts(hostKeys []ssh.Signer, certFiles []string) map[string]*ssh.C
func newServerConfig(cfg *config.Config) (*serverConfig, error) {
authorizedKeysClient, err := authorizedkeys.NewClient(cfg)
if err != nil {
return nil, fmt.Errorf("failed to initialize GitLab client: %w", err)
return nil, fmt.Errorf("failed to initialize authorized keys client: %w", err)
}
authorizedCertsClient, err := authorizedcerts.NewClient(cfg)
if err != nil {
return nil, fmt.Errorf("failed to initialize authorized certs client: %w", err)
}
hostKeys := parseHostKeys(cfg.Server.HostKeyFiles)
Loading
Loading
@@ -123,10 +131,16 @@ func newServerConfig(cfg *config.Config) (*serverConfig, error) {
hostKeyToCertMap := parseHostCerts(hostKeys, cfg.Server.HostCertFiles)
return &serverConfig{cfg: cfg, authorizedKeysClient: authorizedKeysClient, hostKeys: hostKeys, hostKeyToCertMap: hostKeyToCertMap}, nil
return &serverConfig{
cfg: cfg,
authorizedKeysClient: authorizedKeysClient,
authorizedCertsClient: authorizedCertsClient,
hostKeys: hostKeys,
hostKeyToCertMap: hostKeyToCertMap,
}, nil
}
func (s *serverConfig) getAuthKey(ctx context.Context, user string, key ssh.PublicKey) (*authorizedkeys.Response, error) {
func (s *serverConfig) handleUserKey(ctx context.Context, user string, key ssh.PublicKey) (*ssh.Permissions, error) {
if user != s.cfg.User {
return nil, fmt.Errorf("unknown user")
}
Loading
Loading
@@ -134,15 +148,64 @@ func (s *serverConfig) getAuthKey(ctx context.Context, user string, key ssh.Publ
return nil, fmt.Errorf("DSA is prohibited")
}
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
res, err := s.authorizedKeysClient.GetByKey(ctx, base64.RawStdEncoding.EncodeToString(key.Marshal()))
if err != nil {
return nil, err
}
return res, nil
return &ssh.Permissions{
// Record the public key used for authentication.
Extensions: map[string]string{
"key-id": strconv.FormatInt(res.Id, 10),
},
}, nil
}
func (s *serverConfig) handleUserCertificate(ctx context.Context, user string, cert *ssh.Certificate) (*ssh.Permissions, error) {
if os.Getenv("FF_GITLAB_SHELL_SSH_CERTIFICATES") != "1" {
return nil, fmt.Errorf("handleUserCertificate: feature is disabled")
}
fingerprint := ssh.FingerprintSHA256(cert.SignatureKey)
if cert.CertType != ssh.UserCert {
return nil, fmt.Errorf("handleUserCertificate: cert has type %d", cert.CertType)
}
certChecker := &ssh.CertChecker{}
if err := certChecker.CheckCert(user, cert); err != nil {
return nil, err
}
logger := log.WithContextFields(ctx,
log.Fields{
"ssh_user": user,
"public_key_fingerprint": ssh.FingerprintSHA256(cert),
"signing_ca_fingerprint": fingerprint,
"certificate_identity": cert.KeyId,
},
)
res, err := s.authorizedCertsClient.GetByKey(ctx, cert.KeyId, strings.TrimPrefix(fingerprint, "SHA256:"))
if err != nil {
logger.WithError(err).Warn("user certificate is not signed by a trusted key")
return nil, err
}
logger.WithFields(
log.Fields{
"certificate_username": res.Username,
"certificate_namespace": res.Namespace,
},
).Info("user certificate is signed by a trusted key")
return &ssh.Permissions{
Extensions: map[string]string{
"username": res.Username,
"namespace": res.Namespace,
},
}, nil
}
func (s *serverConfig) get(ctx context.Context) *ssh.ServerConfig {
Loading
Loading
@@ -166,19 +229,18 @@ func (s *serverConfig) get(ctx context.Context) *ssh.ServerConfig {
},
}
}
sshCfg := &ssh.ServerConfig{
PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
res, err := s.getAuthKey(ctx, conn.User(), key)
if err != nil {
return nil, err
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
cert, ok := key.(*ssh.Certificate)
if ok {
return s.handleUserCertificate(ctx, conn.User(), cert)
}
return &ssh.Permissions{
// Record the public key used for authentication.
Extensions: map[string]string{
"key-id": strconv.FormatInt(res.Id, 10),
},
}, nil
return s.handleUserKey(ctx, conn.User(), key)
},
GSSAPIWithMICConfig: gssapiWithMICConfig,
ServerVersion: "SSH-2.0-GitLab-SSHD",
Loading
Loading
Loading
Loading
@@ -5,13 +5,20 @@ import (
"crypto/dsa"
"crypto/rand"
"crypto/rsa"
"encoding/base64"
"errors"
"net/http"
"os"
"path"
"strings"
"testing"
"time"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/ssh"
"gitlab.com/gitlab-org/gitlab-shell/v14/client"
"gitlab.com/gitlab-org/gitlab-shell/v14/client/testserver"
"gitlab.com/gitlab-org/gitlab-shell/v14/internal/config"
"gitlab.com/gitlab-org/gitlab-shell/v14/internal/testhelper"
)
Loading
Loading
@@ -66,12 +73,30 @@ func TestFailedAuthorizedKeysClient(t *testing.T) {
_, err := newServerConfig(&config.Config{GitlabUrl: "ftp://localhost"})
require.Error(t, err)
require.Equal(t, "failed to initialize GitLab client: Error creating http client: unknown GitLab URL prefix", err.Error())
require.Equal(t, "failed to initialize authorized keys client: Error creating http client: unknown GitLab URL prefix", err.Error())
}
func TestFailedGetAuthKey(t *testing.T) {
func TestUserKeyHandling(t *testing.T) {
testhelper.PrepareTestRootDir(t)
validRSAKey := rsaPublicKey(t)
requests := []testserver.TestRequestHandler{
{
Path: "/api/v4/internal/authorized_keys",
Handler: func(w http.ResponseWriter, r *http.Request) {
key := base64.RawStdEncoding.EncodeToString(validRSAKey.Marshal())
if key == r.URL.Query().Get("key") {
w.Write([]byte(`{ "id": 1, "key": "key" }`))
} else {
w.WriteHeader(http.StatusInternalServerError)
}
},
},
}
url := testserver.StartSocketHttpServer(t, requests)
srvCfg := config.ServerConfig{
Listen: "127.0.0.1",
ConcurrentSessionsLimit: 1,
Loading
Loading
@@ -83,39 +108,139 @@ func TestFailedGetAuthKey(t *testing.T) {
}
cfg, err := newServerConfig(
&config.Config{GitlabUrl: "http://localhost", User: "user", Server: srvCfg},
&config.Config{GitlabUrl: url, User: "user", Server: srvCfg},
)
require.NoError(t, err)
testCases := []struct {
desc string
user string
key ssh.PublicKey
expectedError string
desc string
user string
key ssh.PublicKey
expectedErr error
expectedPermissions *ssh.Permissions
}{
{
desc: "wrong user",
user: "wrong-user",
key: rsaPublicKey(t),
expectedError: "unknown user",
desc: "wrong user",
user: "wrong-user",
key: rsaPublicKey(t),
expectedErr: errors.New("unknown user"),
}, {
desc: "prohibited dsa key",
user: "user",
key: dsaPublicKey(t),
expectedError: "DSA is prohibited",
desc: "prohibited dsa key",
user: "user",
key: dsaPublicKey(t),
expectedErr: errors.New("DSA is prohibited"),
}, {
desc: "API error",
user: "user",
key: rsaPublicKey(t),
expectedError: "Internal API unreachable",
desc: "API error",
user: "user",
key: rsaPublicKey(t),
expectedErr: &client.ApiError{Msg: "Internal API unreachable"},
}, {
desc: "successful request",
user: "user",
key: validRSAKey,
expectedPermissions: &ssh.Permissions{
Extensions: map[string]string{"key-id": "1"},
},
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
_, err = cfg.getAuthKey(context.Background(), tc.user, tc.key)
require.Error(t, err)
require.Equal(t, tc.expectedError, err.Error())
permissions, err := cfg.handleUserKey(context.Background(), tc.user, tc.key)
require.Equal(t, tc.expectedErr, err)
require.Equal(t, tc.expectedPermissions, permissions)
})
}
}
func TestUserCertificateHandling(t *testing.T) {
testhelper.PrepareTestRootDir(t)
validUserCert := userCert(t, ssh.UserCert, time.Now().Add(time.Hour))
requests := []testserver.TestRequestHandler{
{
Path: "/api/v4/internal/authorized_certs",
Handler: func(w http.ResponseWriter, r *http.Request) {
key := strings.TrimPrefix(ssh.FingerprintSHA256(validUserCert.SignatureKey), "SHA256:")
if key == r.URL.Query().Get("key") && r.URL.Query().Get("user_identifier") == "root@example.com" {
w.Write([]byte(`{ "username": "root", "namespace": "namespace" }`))
} else {
w.WriteHeader(http.StatusInternalServerError)
}
},
},
}
url := testserver.StartSocketHttpServer(t, requests)
srvCfg := config.ServerConfig{
Listen: "127.0.0.1",
ConcurrentSessionsLimit: 1,
HostKeyFiles: []string{
path.Join(testhelper.TestRoot, "certs/valid/server.key"),
path.Join(testhelper.TestRoot, "certs/invalid-path.key"),
path.Join(testhelper.TestRoot, "certs/invalid/server.crt"),
},
}
cfg, err := newServerConfig(
&config.Config{GitlabUrl: url, User: "user", Server: srvCfg},
)
require.NoError(t, err)
testCases := []struct {
desc string
cert *ssh.Certificate
featureFlagValue string
expectedErr error
expectedPermissions *ssh.Permissions
}{
{
desc: "wrong cert type",
cert: userCert(t, ssh.HostCert, time.Now().Add(time.Hour)),
featureFlagValue: "1",
expectedErr: errors.New("handleUserCertificate: cert has type 2"),
}, {
desc: "expired cert",
cert: userCert(t, ssh.UserCert, time.Now().Add(-time.Hour)),
featureFlagValue: "1",
expectedErr: errors.New("ssh: cert has expired"),
}, {
desc: "API error",
cert: userCert(t, ssh.UserCert, time.Now().Add(time.Hour)),
featureFlagValue: "1",
expectedErr: &client.ApiError{Msg: "Internal API unreachable"},
}, {
desc: "successful request",
cert: validUserCert,
featureFlagValue: "1",
expectedPermissions: &ssh.Permissions{
Extensions: map[string]string{
"username": "root",
"namespace": "namespace",
},
},
}, {
desc: "feature flag is not enabled",
cert: validUserCert,
expectedErr: errors.New("handleUserCertificate: feature is disabled"),
expectedPermissions: nil,
}, {
desc: "feature flag is disabled",
cert: validUserCert,
featureFlagValue: "0",
expectedErr: errors.New("handleUserCertificate: feature is disabled"),
expectedPermissions: nil,
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
t.Setenv("FF_GITLAB_SHELL_SSH_CERTIFICATES", tc.featureFlagValue)
permissions, err := cfg.handleUserCertificate(context.Background(), "user", tc.cert)
require.Equal(t, tc.expectedErr, err)
require.Equal(t, tc.expectedPermissions, permissions)
})
}
}
Loading
Loading
@@ -240,3 +365,24 @@ func dsaPublicKey(t *testing.T) ssh.PublicKey {
return publicKey
}
func userCert(t *testing.T, certType uint32, validBefore time.Time) *ssh.Certificate {
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
signer, err := ssh.NewSignerFromKey(privateKey)
require.NoError(t, err)
pubKey, err := ssh.NewPublicKey(&privateKey.PublicKey)
require.NoError(t, err)
cert := &ssh.Certificate{
CertType: certType,
Key: pubKey,
KeyId: "root@example.com",
ValidBefore: uint64(validBefore.Unix()),
}
require.NoError(t, cert.SignCert(rand.Reader, signer))
return cert
}
Loading
Loading
@@ -28,6 +28,8 @@ type session struct {
channel ssh.Channel
gitlabKeyId string
gitlabKrb5Principal string
gitlabUsername string
namespace string
remoteAddr string
// State managed by the session
Loading
Loading
@@ -162,6 +164,7 @@ func (s *session) handleShell(ctx context.Context, req *ssh.Request) (context.Co
OriginalCommand: s.execCmd,
GitProtocolVersion: s.gitProtocolVersion,
RemoteAddr: s.remoteAddr,
NamespacePath: s.namespace,
}
rw := &readwriter.ReadWriter{
Loading
Loading
@@ -175,6 +178,8 @@ func (s *session) handleShell(ctx context.Context, req *ssh.Request) (context.Co
if s.gitlabKrb5Principal != "" {
cmd, err = shellCmd.NewWithKrb5Principal(s.gitlabKrb5Principal, env, s.cfg, rw)
} else if s.gitlabUsername != "" {
cmd, err = shellCmd.NewWithUsername(s.gitlabUsername, env, s.cfg, rw)
} else {
cmd, err = shellCmd.NewWithKey(s.gitlabKeyId, env, s.cfg, rw)
}
Loading
Loading
Loading
Loading
@@ -132,9 +132,13 @@ func TestHandleExec(t *testing.T) {
t.Run(tc.desc, func(t *testing.T) {
sessions := []*session{
{
gitlabKeyId: "root",
gitlabKeyId: "id",
cfg: &config.Config{GitlabUrl: url},
},
{
gitlabUsername: "root",
cfg: &config.Config{GitlabUrl: url},
},
{
gitlabKrb5Principal: "test@TEST.TEST",
cfg: &config.Config{GitlabUrl: url},
Loading
Loading
Loading
Loading
@@ -202,6 +202,8 @@ func (s *Server) handleConn(ctx context.Context, nconn net.Conn) {
channel: channel,
gitlabKeyId: sconn.Permissions.Extensions["key-id"],
gitlabKrb5Principal: sconn.Permissions.Extensions["krb5principal"],
gitlabUsername: sconn.Permissions.Extensions["username"],
namespace: sconn.Permissions.Extensions["namespace"],
remoteAddr: remoteAddr,
started: time.Now(),
}
Loading
Loading
Loading
Loading
@@ -19,6 +19,7 @@ type Env struct {
IsSSHConnection bool
OriginalCommand string
RemoteAddr string
NamespacePath string
}
func NewFromEnv() Env {
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment