Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ RUN apk add tzdata && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& apk del tzdata

COPY conf/app.yml /etc/kubepi/app.yml
RUN chmod 600 /etc/kubepi/app.yml

COPY vimrc.local /etc/vim

Expand Down
18 changes: 8 additions & 10 deletions cmd/server/script/darwin/init-kube.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,33 @@ start_shell() {
exec /bin/bash
}

fail_and_shell() {
echo "$1"
start_shell
fail_and_exit() {
echo "$1" >&2
exit 1
}

arg1=${1:-}
if [[ -z "${arg1}" ]]; then
fail_and_shell "missing terminal session token"
fail_and_exit "missing terminal session token"
fi

terminal_home=$(mktemp -d "${TMPDIR:-/tmp}/kubepi-terminal.XXXXXX") || fail_and_shell "can not create terminal home"
terminal_home=$(mktemp -d "${TMPDIR:-/tmp}/kubepi-terminal.XXXXXX") || fail_and_exit "can not create terminal home"
export HOME="${terminal_home}"
mkdir -p ~/.kube || fail_and_shell "can not create kubeconfig directory"
mkdir -p ~/.kube || fail_and_exit "can not create kubeconfig directory"

session_url=${KUBEPI_WEBKUBECTL_SESSION_URL:-http://127.0.0.1/kubepi/api/v1/webkubectl/session}
code=$(curl --connect-timeout 5 --max-time 20 -w "%{http_code}" -s -o ~/.kube/config "${session_url}?token=${arg1}")
curl_status=$?

if [[ $curl_status -ne 0 || "${code}" != "200" ]];then
echo "download kubeconfig failed (curl: ${curl_status}, http: ${code:-000})"
rm -f ~/.kube/config
start_shell
fail_and_exit "download kubeconfig failed (curl: ${curl_status}, http: ${code:-000})"
fi

current_context=$(kubectl config current-context 2>/tmp/kubepi-kubectl.err)
if [[ $? -ne 0 || -z "${current_context}" ]]; then
echo "load kubeconfig failed"
cat /tmp/kubepi-kubectl.err
start_shell
fail_and_exit "load kubeconfig failed"
fi
cluster=${current_context%@*}
username=${current_context#*@}
Expand Down
44 changes: 20 additions & 24 deletions cmd/server/script/linux/init-kube.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,26 @@ set -u

start_shell() {
export TMPDIR=/nonexistent
if command -v su >/dev/null 2>&1 && id nobody >/dev/null 2>&1; then
su -s /bin/bash nobody
status=$?
if [[ $status -eq 0 ]]; then
exit 0
fi
echo "warning: can not start shell as nobody, fallback to bash"
if ! command -v su >/dev/null 2>&1 || ! id nobody >/dev/null 2>&1; then
echo "can not start terminal as nobody" >&2
exit 1
fi
exec /bin/bash
exec su -s /bin/bash nobody
echo "can not start terminal as nobody" >&2
exit 1
}

fail_and_shell() {
echo "$1"
start_shell
fail_and_exit() {
echo "$1" >&2
exit 1
}

arg1=${1:-}

if [[ -z "${arg1}" ]]; then
fail_and_exit "missing terminal session token"
fi

echo "export TERM=xterm-256color" >> /root/.bashrc 2>/dev/null || true
echo "source /usr/share/bash-completion/bash_completion" >> /root/.bashrc 2>/dev/null || true
echo 'source <(kubectl completion bash)' >> /root/.bashrc 2>/dev/null || true
Expand All @@ -28,22 +32,16 @@ if [ "${WELCOME_BANNER:-}" ]; then
echo ${WELCOME_BANNER}
fi

arg1=${1:-}

if [[ -z "${arg1}" ]]; then
fail_and_shell "missing terminal session token"
fi

mkdir -p /nonexistent || fail_and_shell "can not create terminal home"
mkdir -p /nonexistent || fail_and_exit "can not create terminal home"
mount -t tmpfs -o size=10m tmpfs /nonexistent 2>/tmp/kubepi-mount.err || {
echo "warning: can not mount tmpfs for terminal home"
cat /tmp/kubepi-mount.err
}
cd /nonexistent || fail_and_shell "can not enter terminal home"
cd /nonexistent || fail_and_exit "can not enter terminal home"
cp /root/.bashrc ./ 2>/dev/null || touch .bashrc
cp /etc/vim/vimrc.local .vimrc 2>/dev/null || true
echo 'source /opt/kubectl-aliases/.kubectl_aliases' >> .bashrc
mkdir -p .kube || fail_and_shell "can not create kubeconfig directory"
mkdir -p .kube || fail_and_exit "can not create kubeconfig directory"

export HOME=/nonexistent

Expand All @@ -52,16 +50,14 @@ code=$(curl --connect-timeout 5 --max-time 20 -w "%{http_code}" -s -o ~/.kube/co
curl_status=$?

if [[ $curl_status -ne 0 || "${code}" != "200" ]];then
echo "download kubeconfig failed (curl: ${curl_status}, http: ${code:-000})"
rm -f .kube/config
start_shell
fail_and_exit "download kubeconfig failed (curl: ${curl_status}, http: ${code:-000})"
fi

current_context=$(kubectl config current-context 2>/tmp/kubepi-kubectl.err)
if [[ $? -ne 0 || -z "${current_context}" ]]; then
echo "load kubeconfig failed"
cat /tmp/kubepi-kubectl.err
start_shell
fail_and_exit "load kubeconfig failed"
fi
cluster=${current_context%@*}
username=${current_context#*@}
Expand Down
18 changes: 8 additions & 10 deletions init-kube.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,33 @@ start_shell() {
exec /bin/bash
}

fail_and_shell() {
echo "$1"
start_shell
fail_and_exit() {
echo "$1" >&2
exit 1
}

arg1=${1:-}
if [[ -z "${arg1}" ]]; then
fail_and_shell "missing terminal session token"
fail_and_exit "missing terminal session token"
fi

terminal_home=$(mktemp -d "${TMPDIR:-/tmp}/kubepi-terminal.XXXXXX") || fail_and_shell "can not create terminal home"
terminal_home=$(mktemp -d "${TMPDIR:-/tmp}/kubepi-terminal.XXXXXX") || fail_and_exit "can not create terminal home"
export HOME="${terminal_home}"
mkdir -p ~/.kube || fail_and_shell "can not create kubeconfig directory"
mkdir -p ~/.kube || fail_and_exit "can not create kubeconfig directory"

session_url=${KUBEPI_WEBKUBECTL_SESSION_URL:-http://127.0.0.1/kubepi/api/v1/webkubectl/session}
code=$(curl --connect-timeout 5 --max-time 20 -w "%{http_code}" -s -o ~/.kube/config "${session_url}?token=${arg1}")
curl_status=$?

if [[ $curl_status -ne 0 || "${code}" != "200" ]];then
echo "download kubeconfig failed (curl: ${curl_status}, http: ${code:-000})"
rm -f ~/.kube/config
start_shell
fail_and_exit "download kubeconfig failed (curl: ${curl_status}, http: ${code:-000})"
fi

current_context=$(kubectl config current-context 2>/tmp/kubepi-kubectl.err)
if [[ $? -ne 0 || -z "${current_context}" ]]; then
echo "load kubeconfig failed"
cat /tmp/kubepi-kubectl.err
start_shell
fail_and_exit "load kubeconfig failed"
fi
cluster=${current_context%@*}
username=${current_context#*@}
Expand Down
3 changes: 1 addition & 2 deletions internal/api/v1/v1.go
Original file line number Diff line number Diff line change
Expand Up @@ -425,8 +425,7 @@ func resourceNameInvalidHandler() iris.Handler {
}

func WarpedJwtHandler() iris.Handler {
verifier := jwt.NewVerifier(jwt.HS256, server.Config().Spec.Jwt.Key)
verifier.WithDefaultBlocklist()
verifier := server.JWTVerifier()
verifyMiddleware := verifier.Verify(func() interface{} {
return new(session.UserProfile)
})
Expand Down
20 changes: 20 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ func ReadConfig(c *config.Config, path ...string) error {
fmt.Println(fmt.Sprintf(configNotFoundSkipErr, realDir))
continue
}
if err := secureConfigFile(configFile); err != nil {
return err
}

v.SetConfigFile(configFile)
if err := v.MergeInConfig(); err != nil {
Expand Down Expand Up @@ -79,11 +82,28 @@ func ReadConfig(c *config.Config, path ...string) error {
if err := v.WriteConfig(); err != nil {
return err
}
if err := secureConfigFile(loadedConfigFile); err != nil {
return err
}
}
}
return nil
}

func secureConfigFile(configFile string) error {
info, err := os.Stat(configFile)
if err != nil {
return fmt.Errorf("can not inspect config file %s: %w", configFile, err)
}
if info.Mode().Perm()&0077 == 0 {
return nil
}
if err := os.Chmod(configFile, 0600); err != nil {
return fmt.Errorf("can not secure config file %s: %w", configFile, err)
}
return nil
}

func resolveConfigFile(path string) (string, bool, error) {
info, err := os.Stat(path)
if err != nil {
Expand Down
69 changes: 68 additions & 1 deletion internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package server

import (
"embed"
"encoding/json"
"fmt"
"io/fs"
"net/http"
Expand All @@ -26,6 +27,7 @@ import (
"github.com/asdine/storm/v3"
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/context"
"github.com/kataras/iris/v12/middleware/jwt"
"github.com/kataras/iris/v12/sessions"
"github.com/kataras/iris/v12/view"
"github.com/sirupsen/logrus"
Expand All @@ -34,6 +36,11 @@ import (
const SessionCookieName = "SESS_COOKIE_KUBEPI"

var SessionMgr *sessions.Sessions
var jwtVerifier *jwt.Verifier

func JWTVerifier() *jwt.Verifier {
return jwtVerifier
}

var EmbedWebKubePi embed.FS
var EmbedWebDashboard embed.FS
Expand Down Expand Up @@ -168,6 +175,8 @@ func (e *KubePiServer) setUpStaticFile() {

func (e *KubePiServer) setUpSession() {
SessionMgr = sessions.New(sessions.Config{Cookie: SessionCookieName, AllowReclaim: true, Expires: time.Duration(e.config.Spec.Session.Expires) * time.Hour})
jwtVerifier = jwt.NewVerifier(jwt.HS256, e.config.Spec.Jwt.Key)
jwtVerifier.WithDefaultBlocklist()
e.rootRoute.Use(SessionMgr.Handler())
}

Expand Down Expand Up @@ -266,14 +275,72 @@ func (e *KubePiServer) setUpErrHandler() {
func (e *KubePiServer) runMigrations() {
migrate.RunMigrate(e.db, e.logger)
}

type webkubectlJWTClaims struct {
Name string `json:"name"`
ForceChangePassword bool `json:"forceChangePassword"`
Mfa struct {
Enable bool `json:"enable"`
Approved bool `json:"approved"`
} `json:"mfa"`
}

func isActiveWebkubectlProfile(profile interface{}) bool {
payload, err := json.Marshal(profile)
if err != nil {
return false
}
return isActiveWebkubectlPayload(payload)
}

func isActiveWebkubectlPayload(payload []byte) bool {
var claims webkubectlJWTClaims
if err := json.Unmarshal(payload, &claims); err != nil {
return false
}
return claims.Name != "" && (!claims.Mfa.Enable || claims.Mfa.Approved) && !claims.ForceChangePassword
}

func hasValidWebkubectlJWT(ctx *context.Context) bool {
if jwtVerifier == nil {
return false
}
token := jwt.FromHeader(ctx)
if token == "" {
return false
}
verifiedToken, err := jwtVerifier.VerifyToken([]byte(token), jwtVerifier.Blocklist)
if err != nil {
return false
}
return isActiveWebkubectlPayload(verifiedToken.Payload)
}

func requireWebkubectlSession(ctx *context.Context) bool {
if SessionMgr != nil {
if profile := SessionMgr.Start(ctx).Get("profile"); profile != nil && isActiveWebkubectlProfile(profile) {
return true
}
}
if hasValidWebkubectlJWT(ctx) {
return true
}
ctx.Values().Set("message", "please login")
ctx.StopWithStatus(iris.StatusUnauthorized)
return false
}

func (e *KubePiServer) setWebkubectlProxy() {
handler := func(ctx *context.Context) {
if !requireWebkubectlSession(ctx) {
return
}
p := ctx.Params().Get("p")
if strings.Contains(p, "root") {
ctx.Request().URL.Path = strings.ReplaceAll(ctx.Request().URL.Path, "root", "")
ctx.Request().RequestURI = strings.ReplaceAll(ctx.Request().RequestURI, "root", "")
}
u, _ := url.Parse("http://localhost:8080")
u, _ := url.Parse(webkubectlBackendURL)
proxy := httputil.NewSingleHostReverseProxy(u)
proxy.ModifyResponse = func(resp *http.Response) error {
if resp.StatusCode == iris.StatusMovedPermanently {
Expand Down
5 changes: 5 additions & 0 deletions internal/server/tty.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import (
"strings"
)

const (
webkubectlBindAddress = "127.0.0.1"
webkubectlBackendURL = "http://127.0.0.1:8080"
)

func (e *KubePiServer) localWebkubectlSessionURL() string {
host := strings.TrimSpace(e.config.Spec.Server.Bind.Host)
switch host {
Expand Down
2 changes: 1 addition & 1 deletion internal/server/tty_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (

func (e *KubePiServer) startTty() {
cmd := "gotty"
params := []string{"--permit-write", "bash", "init-kube.sh"}
params := []string{"--address", webkubectlBindAddress, "--permit-write", "bash", "init-kube.sh"}
go func() {
c := exec.Command(cmd, params...)
c.Env = append(os.Environ(), "KUBEPI_WEBKUBECTL_SESSION_URL="+e.localWebkubectlSessionURL())
Expand Down
2 changes: 1 addition & 1 deletion internal/server/tty_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (

func (e *KubePiServer) startTty() {
cmd := "gotty"
params := []string{"--permit-write", "unshare", "--fork", "--pid", "--mount-proc", "--mount", "bash", "init-kube.sh"}
params := []string{"--address", webkubectlBindAddress, "--permit-write", "unshare", "--fork", "--pid", "--mount-proc", "--mount", "bash", "init-kube.sh"}
go func() {
c := exec.Command(cmd, params...)
c.Env = append(os.Environ(), "KUBEPI_WEBKUBECTL_SESSION_URL="+e.localWebkubectlSessionURL())
Expand Down
6 changes: 3 additions & 3 deletions thirdparty/gotty/server/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
)

type Options struct {
Address string `hcl:"address" flagName:"address" flagSName:"a" flagDescribe:"IP address to listen" default:"0.0.0.0"`
Address string `hcl:"address" flagName:"address" flagSName:"a" flagDescribe:"IP address to listen" default:"127.0.0.1"`
Port string `hcl:"port" flagName:"port" flagSName:"p" flagDescribe:"Port number to liten" default:"8080"`
PermitWrite bool `hcl:"permit_write" flagName:"permit-write" flagSName:"w" flagDescribe:"Permit clients to write to the TTY (BE CAREFUL)" default:"false"`
EnableBasicAuth bool `hcl:"enable_basic_auth" default:"false"`
Expand Down Expand Up @@ -102,7 +102,7 @@ type HtermPrefernces struct {
UserCss string `hcl:"user_css" json:"user-css,omitempty"`
}

//RedisOptions contain whether use redis and redis connection options
// RedisOptions contain whether use redis and redis connection options
type RedisOptions struct {
UseRedisTokenCache string `hcl:"use_redis_token_cache" flagName:"use-redis-token-cache" flagDescribe:"if true,will use redis cache token;if false,will use memory cache token." default:"false"`
// host:port address.
Expand Down Expand Up @@ -131,7 +131,7 @@ type RedisOptions struct {
MaxConnAge time.Duration `hcl:"redis_max_conn_age" flagName:"redis-max-conn-age" flagDescribe:"redis connection age at which client retires (closes) the connection" default:"0"`
}

//Convert RedisOptions to redis-go Options
// Convert RedisOptions to redis-go Options
func (ro *RedisOptions) Convert() *redis.Options {
return &redis.Options{
Addr: ro.Addr,
Expand Down