├── .gitignore ├── .gitleaks.toml ├── .github ├── FUNDING.yml ├── renovate.json5 ├── mergify.yml ├── semver.yaml └── workflows │ ├── security.yml │ └── pipeline.yml ├── SECURITY.md ├── keychain_default.go ├── go.mod ├── gopass_test.go ├── LICENSE ├── 1password_test.go ├── gopass.go ├── keychain_mac_test.go ├── keychain_mac.go ├── 1password.go ├── app_test.go ├── .krew.yaml ├── .golangci.yml ├── CODE_OF_CONDUCT.md ├── app.go ├── README.md └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | kubectl-passman 2 | .vscode -------------------------------------------------------------------------------- /.gitleaks.toml: -------------------------------------------------------------------------------- 1 | [whitelist] 2 | files = [ 3 | "(.*?)README.md$" 4 | ] -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [chrisns] 2 | custom: ['https://www.paypal.me/cns'] 3 | -------------------------------------------------------------------------------- /.github/renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "github>chrisns/.github:renovate" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | Please contact [chris@cns.me.uk](mailto:chris@cns.me.uk) [pgp/gpg key](https://github.com/chrisns.gpg) 6 | -------------------------------------------------------------------------------- /.github/mergify.yml: -------------------------------------------------------------------------------- 1 | pull_request_rules: 2 | - name: automatic merge repomanager 3 | conditions: 4 | - author=the-repository-manager[bot] 5 | actions: 6 | merge: 7 | method: rebase 8 | -------------------------------------------------------------------------------- /.github/semver.yaml: -------------------------------------------------------------------------------- 1 | version: 1 2 | force: 3 | major: 1 4 | existing: true 5 | wording: 6 | patch: 7 | - bump 8 | - update 9 | - initial 10 | - tweak 11 | minor: 12 | - change 13 | - improve 14 | - implement 15 | - fix 16 | major: 17 | - breaking 18 | release: 19 | - release-candidate 20 | - add-rc 21 | -------------------------------------------------------------------------------- /.github/workflows/security.yml: -------------------------------------------------------------------------------- 1 | name: "Security Scanning" 2 | on: 3 | push: 4 | branches: [main, master] 5 | pull_request: 6 | branches: [main, master] 7 | jobs: 8 | scan: 9 | name: Security Scan 10 | uses: chrisns/.github/.github/workflows/security-scan.yml@main 11 | permissions: 12 | security-events: write 13 | statuses: write 14 | -------------------------------------------------------------------------------- /keychain_default.go: -------------------------------------------------------------------------------- 1 | // +build !darwin AND !amd64 2 | 3 | package main 4 | 5 | import ( 6 | "github.com/zalando/go-keyring" 7 | ) 8 | 9 | func keychainFetcher(serviceLabel string) (string, error) { 10 | return keyring.Get(serviceLabel, serviceLabel) 11 | } 12 | 13 | func keychainWriter(serviceLabel, secret string) error { 14 | return keyring.Set(serviceLabel, serviceLabel, secret) 15 | } 16 | 17 | // func keychainDeleter(serviceLabel string) { 18 | // err := keyring.Delete(serviceLabel, serviceLabel) 19 | // if err != nil { 20 | // panic(err) 21 | // } 22 | // } 23 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/chrisns/kubectl-passman 2 | 3 | go 1.22.3 4 | 5 | require github.com/stretchr/testify v1.11.1 6 | 7 | require ( 8 | github.com/creasty/defaults v1.8.0 9 | github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 10 | github.com/urfave/cli v1.22.17 11 | github.com/urfave/cli/v3 v3.6.1 12 | github.com/zalando/go-keyring v0.2.6 13 | ) 14 | 15 | require ( 16 | al.essio.dev/pkg/shellescape v1.5.1 // indirect 17 | github.com/alessio/shellescape v1.4.2 // indirect 18 | github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect 19 | github.com/danieljoos/wincred v1.2.2 // indirect 20 | github.com/davecgh/go-spew v1.1.1 // indirect 21 | github.com/godbus/dbus/v5 v5.1.0 // indirect 22 | github.com/pmezard/go-difflib v1.0.0 // indirect 23 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 24 | golang.org/x/sys v0.26.0 // indirect 25 | gopkg.in/yaml.v3 v3.0.1 // indirect 26 | ) 27 | -------------------------------------------------------------------------------- /gopass_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func Test_gopassGetterOK(t *testing.T) { 11 | defaultGopassGet = func(itemName string) (string, error) { 12 | return "foo", nil 13 | } 14 | v, e := gopassGetter("something") 15 | require.Equal(t, "foo", v) 16 | require.Nil(t, e) 17 | } 18 | 19 | func Test_gopassGetterERROR(t *testing.T) { 20 | defaultGopassGet = func(itemName string) (string, error) { 21 | return "bar", errors.New("foobar") 22 | } 23 | v, e := gopassGetter("something") 24 | require.Equal(t, "foobar", e.Error()) 25 | require.Equal(t, "bar", v) 26 | } 27 | 28 | func Test_gopassSetterOK(t *testing.T) { 29 | defaultGopassSet = func(itemName, secret string) error { 30 | return nil 31 | } 32 | e := gopassSetter("foo", "bar") 33 | require.Nil(t, e) 34 | } 35 | 36 | func Test_gopassSetterERROR(t *testing.T) { 37 | defaultGopassSet = func(itemName, secret string) error { 38 | return errors.New("foofoo") 39 | } 40 | e := gopassSetter("foo", "bar") 41 | require.Equal(t, "foofoo", e.Error()) 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Chris Nesbitt-Smith 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /1password_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func Test_opgetter_happy(t *testing.T) { 11 | var expected = "RSA" 12 | defaultOpGet = func(itemName string) (*opResponse, error) { 13 | return &opResponse{ 14 | Fields: []opResponseField{{ 15 | Id: "password", 16 | Label: "credential", 17 | Value: expected, 18 | }}, 19 | }, nil 20 | } 21 | actual, err := opgetter("fakecred") 22 | require.Contains(t, actual, expected) 23 | require.Nil(t, err) 24 | } 25 | 26 | func Test_opgetter_op_fail(t *testing.T) { 27 | expected := errors.New("test") 28 | defaultOpGet = func(itemName string) (*opResponse, error) { 29 | return nil, expected 30 | } 31 | actual, err := opgetter("foo") 32 | require.Equal(t, actual, "") 33 | require.Equal(t, expected, err) 34 | } 35 | 36 | func Test_opgetter_password_not_found(t *testing.T) { 37 | var expected = "RSA" 38 | defaultOpGet = func(itemName string) (*opResponse, error) { 39 | return &opResponse{ 40 | 41 | Fields: []opResponseField{{ 42 | Id: "notpassword", 43 | Label: "notpassword", 44 | Value: expected, 45 | }}, 46 | }, nil 47 | } 48 | actual, err := opgetter("test") 49 | require.Equal(t, err.Error(), "unable to find credential") 50 | require.Equal(t, actual, "") 51 | } 52 | -------------------------------------------------------------------------------- /gopass.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "os/exec" 7 | ) 8 | 9 | var defaultGopassGet = func(itemName string) (string, error) { 10 | out, err := exec.Command("gopass", "show", "--password", itemName).Output() 11 | return string(out), err 12 | } 13 | 14 | func gopassGetter(itemName string) (string, error) { 15 | return defaultGopassGet(itemName) 16 | } 17 | 18 | var gopassWriteCmd = func(itemName string) *exec.Cmd { 19 | return exec.Command("gopass", "insert", "--force", itemName) 20 | } 21 | 22 | var defaultGopassSet = func(itemName, secret string) error { 23 | var stdin io.WriteCloser 24 | var err error 25 | var out []byte 26 | 27 | cmd := gopassWriteCmd(itemName) 28 | 29 | stdin, err = cmd.StdinPipe() 30 | 31 | if err != nil { 32 | log.Fatal(err) 33 | return err 34 | } 35 | 36 | err = gopassWriteSecret(stdin, secret) 37 | 38 | if err != nil { 39 | return err 40 | } 41 | 42 | out, err = cmd.CombinedOutput() 43 | 44 | if err != nil { 45 | return err 46 | } 47 | log.Print(string(out)) 48 | return nil 49 | } 50 | 51 | var gopassWriteSecret = func(stdin io.WriteCloser, secret string) error { 52 | defer stdin.Close() 53 | _, err := io.WriteString(stdin, secret) 54 | return err 55 | } 56 | 57 | func gopassSetter(itemName, secret string) error { 58 | return defaultGopassSet(itemName, secret) 59 | } 60 | -------------------------------------------------------------------------------- /keychain_mac_test.go: -------------------------------------------------------------------------------- 1 | // +build darwin,amd64 2 | 3 | package main 4 | 5 | import ( 6 | "errors" 7 | "testing" 8 | 9 | "github.com/keybase/go-keychain" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func Test_keychainFetcher_NoKeychainError(t *testing.T) { 14 | defaultKeychain = func(serviceLabel string) ([]keychain.QueryResult, error) { 15 | return nil, errors.New("error") 16 | } 17 | _, err := keychainFetcher("ff") 18 | require.Equal(t, "unable to connect to keychain", err.Error()) 19 | } 20 | 21 | func Test_keychainFetcher_NoItemFoundError(t *testing.T) { 22 | defaultKeychain = func(serviceLabel string) ([]keychain.QueryResult, error) { 23 | return nil, nil 24 | } 25 | _, err := keychainFetcher("ff") 26 | require.Equal(t, "item doesn't exist", err.Error()) 27 | } 28 | 29 | func Test_keychainFetcher_ToManyMatching(t *testing.T) { 30 | defaultKeychain = func(serviceLabel string) ([]keychain.QueryResult, error) { 31 | return []keychain.QueryResult{{Data: []byte("foo")}, {Data: []byte("foo")}}, nil 32 | } 33 | _, err := keychainFetcher("ff") 34 | require.Equal(t, "too many matching items", err.Error()) 35 | } 36 | 37 | func Test_keychainFetcher_ItemFound(t *testing.T) { 38 | var expected = "RSA" 39 | defaultKeychain = func(serviceLabel string) ([]keychain.QueryResult, error) { 40 | return []keychain.QueryResult{{Data: []byte(expected)}}, nil 41 | } 42 | actual, err := keychainFetcher("ff") 43 | 44 | require.Contains(t, expected, actual) 45 | require.Nil(t, err) 46 | } 47 | -------------------------------------------------------------------------------- /keychain_mac.go: -------------------------------------------------------------------------------- 1 | // +build darwin,amd64 2 | 3 | package main 4 | 5 | import ( 6 | "errors" 7 | "log" 8 | 9 | "github.com/keybase/go-keychain" 10 | ) 11 | 12 | var defaultKeychain = func(serviceLabel string) ([]keychain.QueryResult, error) { 13 | query := getKeychain(serviceLabel) 14 | query.SetReturnData(true) 15 | return keychain.QueryItem(query) 16 | } 17 | 18 | func getKeychain(serviceLabel string) keychain.Item { 19 | query := keychain.NewItem() 20 | query.SetSecClass(keychain.SecClassGenericPassword) 21 | query.SetService(serviceLabel) 22 | return query 23 | } 24 | 25 | func keychainFetcher(serviceLabel string) (string, error) { 26 | results, err := defaultKeychain(serviceLabel) 27 | if err != nil { 28 | return "", errors.New("unable to connect to keychain") 29 | } else if len(results) > 1 { 30 | return "", errors.New("too many matching items") 31 | } else if len(results) != 1 { 32 | return "", errors.New("item doesn't exist") 33 | } 34 | return string(results[0].Data), nil 35 | } 36 | 37 | func keychainWriter(serviceLabel string, secret string) error { 38 | query := getKeychain(serviceLabel) 39 | query.SetData([]byte(secret)) 40 | err := keychain.AddItem(query) 41 | 42 | if err == keychain.ErrorDuplicateItem { 43 | log.Print("Item already exists, deleting it first") 44 | err = keychainDeleter(serviceLabel) 45 | if err != nil { 46 | return err 47 | } 48 | return keychainWriter(serviceLabel, secret) 49 | } 50 | return err 51 | } 52 | 53 | func keychainDeleter(serviceLabel string) error { 54 | query := getKeychain(serviceLabel) 55 | query.SetMatchLimit(keychain.MatchLimitOne) 56 | return keychain.DeleteItem(query) 57 | } 58 | -------------------------------------------------------------------------------- /1password.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "os/exec" 9 | ) 10 | 11 | type opResponse struct { 12 | UUID string `json:"uuid"` 13 | Title string `json:"title"` 14 | Category string `json:"category"` 15 | Fields []opResponseField `json:"fields"` 16 | } 17 | 18 | type opResponseField struct { 19 | Id string `json:"id"` 20 | Value string `json:"value"` 21 | Type string `json:"type"` 22 | Label string `json:"label"` 23 | } 24 | 25 | var defaultOpGet = func(itemName string) (*opResponse, error) { 26 | cmd := exec.Command("op", "item", "get", itemName, "--format=json", "--debug") 27 | var outb, errb bytes.Buffer 28 | cmd.Stdout = &outb 29 | cmd.Stderr = &errb 30 | err := cmd.Run() 31 | if err != nil { 32 | return nil, err 33 | } 34 | var resp opResponse 35 | err = json.Unmarshal(outb.Bytes(), &resp) 36 | if err != nil { 37 | fmt.Println("Error unmarshaling data") 38 | return nil, err 39 | } 40 | return &resp, nil 41 | } 42 | 43 | func opgetter(itemName string) (string, error) { 44 | resp, err := defaultOpGet(itemName) 45 | if err != nil { 46 | return "", err 47 | } 48 | for _, v := range resp.Fields { 49 | if v.Label == "credential" { 50 | return v.Value, nil 51 | } 52 | } 53 | return "", errors.New("unable to find credential") 54 | } 55 | 56 | func opsetter(itemName, secret string) error { 57 | 58 | // create a string that will be passed to the op command to store our secret 59 | credString := "credential[concealed]=" + secret 60 | 61 | stdoutStderr, err := exec.Command("op", "item", "create", "--category", 62 | "API Credential", "--title", itemName, credString).CombinedOutput() 63 | 64 | if err != nil { 65 | fmt.Printf("%s\n", stdoutStderr) 66 | } 67 | 68 | return err 69 | } 70 | -------------------------------------------------------------------------------- /app_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | var ( 11 | jsonCertBase64 = `{"client-certificate-data":"MDAwMDA=","client-key-data":"MDAwMDA="}` 12 | jsonCert = `{"clientCertificateData":"00000","clientKeyData":"00000"}` 13 | jsonToken = `{"token":"00000"}` 14 | ) 15 | 16 | func Test_formatValidatorCertBase64(t *testing.T) { 17 | actual, err := formatValidator(jsonCertBase64) 18 | require.Equal(t, jsonCert, actual) 19 | require.Nil(t, err) 20 | } 21 | 22 | func Test_formatValidatorCertBase64ErrorDecode(t *testing.T) { 23 | actual, err := formatValidator(`{"client-certificate-data":"BAD-DATA","client-key-data":"MDAwMDA="}`) 24 | require.Equal(t, "", actual) 25 | require.Equal(t, "illegal base64 data at input byte 3", err.Error()) 26 | } 27 | 28 | func Test_formatValidatorCertBase64ErrorMisKey(t *testing.T) { 29 | actual, err := formatValidator(`{"clientCertificateData":"BAD-DATA","client-certificate-data":"MDAwMDA="}`) 30 | require.Equal(t, "", actual) 31 | require.Equal(t, "cannot define valid secret format", err.Error()) 32 | } 33 | 34 | func Test_formatValidatorCertRaw(t *testing.T) { 35 | actual, err := formatValidator(jsonCert) 36 | require.Equal(t, jsonCert, actual) 37 | require.Nil(t, err) 38 | } 39 | 40 | func Test_formatValidatorCertRawError(t *testing.T) { 41 | actual, err := formatValidator(`{"clientCertificateData":"00000"}`) 42 | require.Equal(t, "", actual) 43 | require.Equal(t, "cannot define valid secret format", err.Error()) 44 | } 45 | 46 | func Test_formatValidatorCertRawErrorMisKey(t *testing.T) { 47 | actual, err := formatValidator(`{"clientCertificateData":"00000"}`) 48 | require.Equal(t, "", actual) 49 | require.Equal(t, "cannot define valid secret format", err.Error()) 50 | } 51 | 52 | func Test_formatValidatorToken(t *testing.T) { 53 | actual, err := formatValidator(jsonToken) 54 | require.Equal(t, jsonToken, actual) 55 | require.Nil(t, err) 56 | } 57 | 58 | func Test_formatResponse(t *testing.T) { 59 | fixture := `{"apiVersion":"client.authentication.k8s.io/v1beta1","kind":"ExecCredential","status":{}}` 60 | actual, err := formatResponse(&response{}) 61 | require.Equal(t, fixture, actual) 62 | require.Nil(t, err) 63 | } 64 | 65 | func Test_formatResponse_is_json(t *testing.T) { 66 | actual, err := formatResponse(&response{}) 67 | require.True(t, json.Valid([]byte(actual))) 68 | require.Nil(t, err) 69 | } 70 | 71 | func Test_formatResponse_populate_defaults(t *testing.T) { 72 | actual, err := formatResponse(&response{}) 73 | require.Contains(t, actual, "apiVersion") 74 | require.Nil(t, err) 75 | } 76 | func Test_formatResponse_override_defaults(t *testing.T) { 77 | actual, err := formatResponse(&response{Kind: "foo"}) 78 | require.Contains(t, actual, `"kind":"foo"`) 79 | require.Nil(t, err) 80 | } 81 | 82 | func Test_cli_info(t *testing.T) { 83 | cliInfo() 84 | require.Equal(t, "kubectl-passman", app.Name) 85 | require.Equal(t, "0.0.0", app.Version) 86 | } 87 | 88 | func Test_commands(t *testing.T) { 89 | cliCommands() 90 | require.Equal(t, "keychain", app.Commands[0].Name) 91 | require.Equal(t, "1password", app.Commands[1].Name) 92 | require.Equal(t, "gopass", app.Commands[2].Name) 93 | } 94 | -------------------------------------------------------------------------------- /.krew.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: krew.googlecontainertools.github.com/v1alpha2 2 | kind: Plugin 3 | metadata: 4 | name: passman 5 | spec: 6 | version: {{ .TagName }} 7 | platforms: 8 | 9 | - selector: 10 | matchLabels: 11 | os: darwin 12 | arch: arm64 13 | {{addURIAndSha "https://github.com/chrisns/kubectl-passman/releases/download/{{ .TagName }}/kubectl-passman-darwin-arm64.zip" .TagName }} 14 | bin: "./kubectl-passman" 15 | files: 16 | - from: kubectl-passman-darwin-arm64 17 | to: kubectl-passman 18 | - from: LICENSE 19 | to: . 20 | 21 | - selector: 22 | matchLabels: 23 | os: linux 24 | arch: arm 25 | {{addURIAndSha "https://github.com/chrisns/kubectl-passman/releases/download/{{ .TagName }}/kubectl-passman-linux-arm.zip" .TagName }} 26 | bin: "./kubectl-passman" 27 | files: 28 | - from: kubectl-passman-linux-arm 29 | to: kubectl-passman 30 | - from: LICENSE 31 | to: . 32 | 33 | - selector: 34 | matchLabels: 35 | os: linux 36 | arch: arm64 37 | {{addURIAndSha "https://github.com/chrisns/kubectl-passman/releases/download/{{ .TagName }}/kubectl-passman-linux-arm64.zip" .TagName }} 38 | bin: "./kubectl-passman" 39 | files: 40 | - from: kubectl-passman-linux-arm64 41 | to: kubectl-passman 42 | - from: LICENSE 43 | to: . 44 | 45 | - selector: 46 | matchLabels: 47 | os: linux 48 | arch: 386 49 | {{addURIAndSha "https://github.com/chrisns/kubectl-passman/releases/download/{{ .TagName }}/kubectl-passman-linux-386.zip" .TagName }} 50 | bin: "./kubectl-passman" 51 | files: 52 | - from: kubectl-passman-linux-386 53 | to: kubectl-passman 54 | - from: LICENSE 55 | to: . 56 | 57 | - selector: 58 | matchLabels: 59 | os: linux 60 | arch: amd64 61 | {{addURIAndSha "https://github.com/chrisns/kubectl-passman/releases/download/{{ .TagName }}/kubectl-passman-linux-amd64.zip" .TagName }} 62 | bin: "./kubectl-passman" 63 | files: 64 | - from: kubectl-passman-linux-amd64 65 | to: kubectl-passman 66 | - from: LICENSE 67 | to: . 68 | 69 | - selector: 70 | matchLabels: 71 | os: windows 72 | arch: amd64 73 | {{addURIAndSha "https://github.com/chrisns/kubectl-passman/releases/download/{{ .TagName }}/kubectl-passman-windows-amd64.zip" .TagName }} 74 | bin: "./kubectl-passman.exe" 75 | files: 76 | - from: kubectl-passman-windows-amd64.exe 77 | to: kubectl-passman.exe 78 | - from: LICENSE 79 | to: . 80 | 81 | - selector: 82 | matchLabels: 83 | os: windows 84 | arch: 386 85 | {{addURIAndSha "https://github.com/chrisns/kubectl-passman/releases/download/{{ .TagName }}/kubectl-passman-windows-386.zip" .TagName }} 86 | bin: "./kubectl-passman.exe" 87 | files: 88 | - from: kubectl-passman-windows-386.exe 89 | to: kubectl-passman.exe 90 | - from: LICENSE 91 | to: . 92 | 93 | 94 | shortDescription: Store kubeconfig credentials in keychains or password managers 95 | homepage: https://github.com/chrisns/kubectl-passman 96 | caveats: | 97 | This plugin needs a usable keychain or password manager 98 | See usage docs https://github.com/chrisns/kubectl-passman 99 | description: | 100 | An effective way to keep your credentials somewhere better than in plain text 101 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters-settings: 2 | errcheck: 3 | check-type-assertions: true 4 | check-blank: true 5 | funlen: 6 | lines: 60 7 | statements: 40 8 | 9 | govet: 10 | check-shadowing: true 11 | 12 | enable-all: false 13 | gocyclo: 14 | min-complexity: 12 15 | maligned: 16 | suggest-new: true 17 | depguard: 18 | misspell: 19 | locale: US 20 | lll: 21 | line-length: 120 22 | unused: 23 | check-exported: true 24 | unparam: 25 | check-exported: true 26 | nakedret: 27 | max-func-lines: 30 28 | gocritic: 29 | # See https://go-critic.github.io/overview#checks-overview 30 | # To check which checks are enabled run `GL_DEBUG=gocritic golangci-lint run` 31 | # enabled-checks: 32 | # - rangeValCopy 33 | # - argOrder 34 | # - badCall 35 | # - badCond 36 | # - boolExprSimplify 37 | # - builtinShadow 38 | # - codegenComment 39 | # - commentFormatting 40 | # - commentedOutCode 41 | # - commentedOutImport 42 | # - deprecatedComment 43 | # - docStub 44 | # - dupImport 45 | # - emptyFallthrough 46 | # - emptyStringTest 47 | # - evalOrder 48 | # - exitAfterDefer 49 | # - flagName 50 | # - hexLiteral 51 | # - importShadow 52 | # - initClause 53 | # - methodExprCall 54 | # - nestingReduce 55 | # - newDeref 56 | # - nilValReturn 57 | # - octalLiteral 58 | # - offBy1 59 | # - paramTypeCombine 60 | # - ptrToRefParam 61 | # - regexpPattern 62 | # - sloppyReassign 63 | # - stringXbytes 64 | # - typeAssertChain 65 | # - typeUnparen 66 | # - unlabelStmt 67 | # - unnamedResult 68 | # - unnecessaryBlock 69 | # - valSwap 70 | # - weakCond 71 | # - wrapperFunc 72 | # - yodaStyleExpr 73 | 74 | # Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint` run to see all tags and checks. 75 | # Empty list by default. See https://github.com/go-critic/go-critic#usage -> section "Tags". 76 | enabled-tags: 77 | - performance 78 | - opinionated 79 | - diagnostic 80 | - style 81 | 82 | whitespace: 83 | multi-if: false 84 | 85 | linters: 86 | # enable: 87 | # - bodyclose 88 | # - deadcode 89 | # - depguard 90 | # - dogsled 91 | # - dupl 92 | # - errcheck 93 | # - funlen 94 | # - gochecknoinits 95 | # - goconst 96 | # - gocritic 97 | # - gocyclo 98 | # - gofmt 99 | # - goimports 100 | # - golint 101 | # - gosec 102 | # - gosimple 103 | # - govet 104 | # - ineffassign 105 | # - interfacer 106 | # - lll 107 | # - misspell 108 | # - nakedret 109 | # - scopelint 110 | # - staticcheck 111 | # - structcheck 112 | # - stylecheck 113 | # - typecheck 114 | # - unconvert 115 | # - unparam 116 | # - unused 117 | # - varcheck 118 | # - whitespace 119 | 120 | enable-all: true 121 | disable: 122 | - gochecknoglobals 123 | # - maligned 124 | # - prealloc 125 | # disable-all: false 126 | # presets: 127 | # - bugs 128 | # - unused 129 | fast: false 130 | auto-fix: false 131 | 132 | issues: 133 | max-issues-per-linter: 0 134 | max-same-issues: 0 135 | # new: true 136 | exclude-rules: 137 | # Exclude some linters from running on tests files. 138 | - path: _test\.go 139 | linters: 140 | - unused 141 | - godox 142 | - lll 143 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at chris@cns.me.uk. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /.github/workflows/pipeline.yml: -------------------------------------------------------------------------------- 1 | name: CI Pipeline 2 | on: 3 | push: 4 | paths-ignore: 5 | - README.md 6 | branches: 7 | - "**" 8 | tags-ignore: 9 | - build-refs** 10 | pull_request: 11 | 12 | env: 13 | HUB_VERSION: 2.12.7 14 | HUB_OS: darwin 15 | HUB_ARCH: amd64 16 | 17 | jobs: 18 | release-name: 19 | name: Generate a release name to use 20 | runs-on: ubuntu-latest 21 | env: 22 | HUB_OS: linux 23 | steps: 24 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 25 | if: github.event_name == 'push' 26 | - run: echo $GITHUB_REF | sed -e 's/^refs\///g' -e 's/^tags\///g' > VERSION 27 | - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 28 | with: 29 | name: VERSION 30 | path: VERSION 31 | - name: fetch hub 32 | if: github.event_name == 'push' 33 | run: wget -q -c https://github.com/github/hub/releases/download/v${HUB_VERSION}/hub-${HUB_OS}-${HUB_ARCH}-${HUB_VERSION}.tgz -O - | tar -xz 34 | - name: create pre-release if it doesn't exist 35 | if: github.event_name == 'push' 36 | run: hub-${HUB_OS}-${HUB_ARCH}-${HUB_VERSION}/bin/hub release create -m "$(cat VERSION)" "$(cat VERSION)" || echo release already exists 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | 40 | # golangci-lint: 41 | # name: GolangCI 42 | # runs-on: ubuntu-latest 43 | # steps: 44 | # - uses: actions/checkout@v2.4.0 45 | # - uses: docker://golangci/golangci-lint 46 | # with: 47 | # args: golangci-lint run --color=always 48 | 49 | test-build-publish: 50 | name: Test and Build 51 | runs-on: macOS-latest 52 | needs: release-name 53 | strategy: 54 | fail-fast: false 55 | max-parallel: 8 56 | matrix: 57 | OS: 58 | - darwin 59 | - linux 60 | - windows 61 | - netbsd 62 | - freebsd 63 | - openbsd 64 | - plan9 65 | - solaris 66 | ARCH: 67 | - amd64 68 | - 386 69 | - arm64 70 | - arm 71 | include: 72 | - OS: windows 73 | EXT: .exe 74 | exclude: 75 | - OS: darwin 76 | ARCH: 386 77 | - OS: darwin 78 | ARCH: arm 79 | - OS: darwin 80 | ARCH: amd64 81 | - OS: windows 82 | ARCH: arm64 83 | - OS: freebsd 84 | ARCH: arm64 85 | - OS: plan9 86 | ARCH: arm64 87 | - OS: solaris 88 | ARCH: arm64 89 | - OS: solaris 90 | ARCH: arm 91 | - OS: solaris 92 | ARCH: 386 93 | env: 94 | BUILD_FILENAME: kubectl-passman-${{matrix.OS}}-${{matrix.ARCH}}${{matrix.EXT}} 95 | ZIP_FILENAME: kubectl-passman-${{matrix.OS}}-${{matrix.ARCH}}.zip 96 | steps: 97 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 98 | - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6 99 | with: 100 | go-version-file: './go.mod' 101 | - run: go test -v 102 | - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 103 | with: 104 | name: VERSION 105 | path: VERSION 106 | - run: go build -a -ldflags "-X main.VERSION=$(cat VERSION/VERSION)" -o ${BUILD_FILENAME} 107 | env: 108 | GOOS: ${{matrix.OS}} 109 | GOARCH: ${{matrix.ARCH}} 110 | - run: chmod +x ${BUILD_FILENAME} 111 | env: 112 | GOOS: ${{matrix.OS}} 113 | GOARCH: ${{matrix.ARCH}} 114 | - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 115 | with: 116 | name: kubectl-passman-${{matrix.OS}}-${{matrix.ARCH}}${{matrix.EXT}} 117 | path: kubectl-passman-${{matrix.OS}}-${{matrix.ARCH}}${{matrix.EXT}} 118 | - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 119 | with: 120 | name: VERSION 121 | path: VERSION 122 | - run: zip kubectl-passman-${{matrix.OS}}-${{matrix.ARCH}}.zip ${BUILD_FILENAME} LICENSE 123 | - name: fetch hub 124 | if: github.event_name == 'push' 125 | run: wget -q -c https://github.com/github/hub/releases/download/v${HUB_VERSION}/hub-${HUB_OS}-${HUB_ARCH}-${HUB_VERSION}.tgz -O - | tar -xz 126 | - name: Publish to release 127 | if: github.event_name == 'push' 128 | run: | 129 | hub-${HUB_OS}-${HUB_ARCH}-${HUB_VERSION}/bin/hub release edit -a ${ZIP_FILENAME} -m "Latest build of ${{github.ref}}" "$(cat VERSION/VERSION)" 130 | env: 131 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 132 | 133 | write-krew: 134 | name: Write krew manifest file 135 | runs-on: ubuntu-latest 136 | if: github.ref_type == 'tag' 137 | env: 138 | HUB_OS: linux 139 | needs: 140 | - test-build-publish 141 | steps: 142 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 143 | - name: Update new version in krew-index 144 | uses: rajatjindal/krew-release-bot@3d9faef30a82761d610544f62afddca00993eef9 # v0.0.47 145 | -------------------------------------------------------------------------------- /app.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "log" 9 | "os" 10 | 11 | "github.com/urfave/cli" 12 | 13 | "github.com/creasty/defaults" 14 | ) 15 | 16 | // VERSION populated at build time 17 | var VERSION = "0.0.0" 18 | 19 | var app = cli.NewApp() 20 | 21 | func cliCommands() { 22 | app.Commands = []cli.Command{ 23 | { 24 | Name: "keychain", 25 | Usage: "Use keychain/keyring", 26 | Aliases: []string{"keyring"}, 27 | ArgsUsage: "[item-name]", 28 | Action: cliHandler, 29 | }, 30 | { 31 | Name: "1password", 32 | Usage: "Use 1Password", 33 | Aliases: []string{"1pass", "op"}, 34 | ArgsUsage: "[item-name]", 35 | Action: cliHandler, 36 | }, 37 | { 38 | Name: "gopass", 39 | Usage: "Use gopass", 40 | ArgsUsage: "[item-name]", 41 | Action: cliHandler, 42 | }, 43 | } 44 | } 45 | 46 | func cliHandler(c *cli.Context) error { 47 | var handler string = c.Command.Name 48 | var itemName = c.Args().Get(0) 49 | var secret = c.Args().Get(1) 50 | if itemName == "" { 51 | return cli.NewExitError("Please provide [item-name]", 1) 52 | } 53 | if secret != "" { 54 | return write(handler, itemName, secret) 55 | } 56 | return read(handler, itemName) 57 | } 58 | 59 | func cliInfo() { 60 | app.Name = "kubectl-passman" 61 | app.Usage = "Store kubeconfig credentials in keychains or password managers" 62 | app.Authors = []cli.Author{ 63 | { 64 | Name: "Chris Nesbitt-Smith", 65 | Email: "chris@cns.me.uk", 66 | }, 67 | } 68 | app.Copyright = "(c) 2019 Chris Nesbitt-Smith" 69 | app.UsageText = `kubectl-passman [command] [item-name] [new-value(optional)] 70 | If new-value is provided it will write to the item, otherwise it will read` 71 | app.Version = VERSION 72 | } 73 | 74 | func main() { 75 | cliInfo() 76 | cliCommands() 77 | err := app.Run(os.Args) 78 | if err != nil { 79 | log.Fatal(err) 80 | } 81 | } 82 | 83 | func write(handler, itemName, secret string) error { 84 | 85 | validSecret, err := formatValidator(secret) 86 | if err != nil { 87 | return err 88 | } 89 | 90 | switch handler { 91 | case "keychain": 92 | return keychainWriter(itemName, validSecret) 93 | case "1password": 94 | return opsetter(itemName, validSecret) 95 | case "gopass": 96 | return gopassSetter(itemName, validSecret) 97 | } 98 | return nil 99 | } 100 | 101 | func read(handler, itemName string) error { 102 | var secret string 103 | var err error 104 | var out string 105 | 106 | switch handler { 107 | case "keychain": 108 | secret, err = keychainFetcher(itemName) 109 | case "1password": 110 | secret, err = opgetter(itemName) 111 | case "gopass": 112 | secret, err = gopassGetter(itemName) 113 | } 114 | 115 | if err != nil { 116 | return err 117 | } 118 | res := &response{} 119 | err = json.Unmarshal([]byte(secret), &res.Status) 120 | if err != nil { 121 | return err 122 | } 123 | out, err = formatResponse(res) 124 | fmt.Println(out) 125 | return err 126 | } 127 | 128 | type responseStatus struct { 129 | Token string `json:"token,omitempty"` 130 | ClientCertificateData string `json:"clientCertificateData,omitempty"` 131 | ClientCertificateDataD string `json:"client-certificate-data,omitempty"` 132 | ClientKeyData string `json:"clientKeyData,omitempty"` 133 | ClientKeyDataD string `json:"client-key-data,omitempty"` 134 | } 135 | type response struct { 136 | APIVersion string `default:"client.authentication.k8s.io/v1beta1" json:"apiVersion"` 137 | Kind string `default:"ExecCredential" json:"kind"` 138 | Status responseStatus `json:"status"` 139 | } 140 | 141 | func formatValidator(secret string) (string, error) { 142 | s := &responseStatus{} 143 | data := []byte(secret) 144 | 145 | err := json.Unmarshal(data, s) 146 | if err != nil { 147 | return "", err 148 | } 149 | 150 | switch { 151 | case len(s.ClientCertificateDataD) > 0 && len(s.ClientKeyDataD) > 0: 152 | dataCrt, errCrt := base64.StdEncoding.DecodeString(s.ClientCertificateDataD) 153 | dataKey, errKey := base64.StdEncoding.DecodeString(s.ClientKeyDataD) 154 | 155 | switch { 156 | case errCrt != nil: 157 | return "", errCrt 158 | case errKey != nil: 159 | return "", errKey 160 | default: 161 | s.ClientCertificateData = string(dataCrt) 162 | s.ClientKeyData = string(dataKey) 163 | } 164 | 165 | s.ClientCertificateDataD = "" 166 | s.ClientKeyDataD = "" 167 | s.Token = "" 168 | case len(s.ClientCertificateData) > 0 && len(s.ClientKeyData) > 0: 169 | s.ClientCertificateDataD = "" 170 | s.ClientKeyDataD = "" 171 | s.Token = "" 172 | case len(s.Token) > 0: 173 | s.ClientCertificateDataD = "" 174 | s.ClientKeyDataD = "" 175 | s.ClientCertificateData = "" 176 | s.ClientKeyData = "" 177 | default: 178 | return "", errors.New("cannot define valid secret format") 179 | } 180 | 181 | secretByte, err := json.Marshal(s) 182 | if err != nil { 183 | return "", err 184 | } 185 | 186 | return string(secretByte), nil 187 | } 188 | 189 | func formatResponse(res *response) (string, error) { 190 | err := defaults.Set(res) 191 | if err != nil { 192 | return "", err 193 | } 194 | jsonResponse, err := json.Marshal(res) 195 | if err != nil { 196 | return "", err 197 | } 198 | return string(jsonResponse), nil 199 | } 200 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kubectl user password manager glue 2 | 3 | ![CI status badge](https://github.com/chrisns/kubectl-passman/workflows/CI%20Pipeline/badge.svg) 4 | ![LICENSE](https://img.shields.io/github/license/chrisns/kubectl-passman) 5 | ![GitHub watchers](https://img.shields.io/github/watchers/chrisns/kubectl-passman?style) 6 | ![GitHub stars](https://img.shields.io/github/stars/chrisns/kubectl-passman) 7 | ![GitHub forks](https://img.shields.io/github/forks/chrisns/kubectl-passman) 8 | ![GitHub issues](https://img.shields.io/github/issues-raw/chrisns/kubectl-passman) 9 | ![GitHub closed issues](https://img.shields.io/github/issues-closed-raw/chrisns/kubectl-passman) 10 | ![GitHub pull requests](https://img.shields.io/github/issues-pr-raw/chrisns/kubectl-passman) 11 | ![GitHub closed pull requests](https://img.shields.io/github/issues-pr-closed-raw/chrisns/kubectl-passman) 12 | ![GitHub repo size](https://img.shields.io/github/repo-size/chrisns/kubectl-passman) 13 | ![GitHub contributors](https://img.shields.io/github/contributors/chrisns/kubectl-passman) 14 | ![GitHub last commit](https://img.shields.io/github/last-commit/chrisns/kubectl-passman) 15 | [![Go Report Card](https://goreportcard.com/badge/github.com/chrisns/kubectl-passman)](https://goreportcard.com/report/github.com/chrisns/kubectl-passman) 16 | 17 | > :heavy_exclamation_mark: An easy way to store your kubernetes credentials in a keychain or password manager 18 | 19 | ### Does your `~/.kube/config` look like this: 20 | 21 | ```yaml 22 | apiVersion: v1 23 | kind: Config 24 | users: 25 | - name: my-prod-user 26 | user: 27 | token: 28 | - name: docker-desktop 29 | user: 30 | client-certificate-data: 31 | client-key-data: 32 | ``` 33 | 34 | ## :scream: :scream: :scream: :scream:

Do you scold your parents :man_teacher:/:woman_teacher: for maintaining a `passwords.doc` on their desktop?

Then you need kubectl-passman! 35 | 36 | ## Works with (more coming) 37 | 38 | Provider | Supports | Example command 39 | --- | --- | --- 40 | keychain | [Mac OS Keychain](https://support.apple.com/en-gb/guide/keychain-access/kyca1083/mac)
[GNOME Keyring](https://wiki.gnome.org/Projects/GnomeKeyring)
[Windows Credential Manager](http://blogs.msdn.com/b/visualstudioalm/archive/2015/12/08/announcing-the-git-credential-manager-for-windows-1-0.aspx) | `kubectl passman keychain [item] [token]` 41 | 1password | [1password](https://1password.com/)
requires [1password cli](https://1password.com/downloads/command-line/) | `kubectl passman 1password [item] [token]` 42 | gopass | [gopass](https://www.gopass.pw/) | `kubectl passman gopass [item] [token]` 43 | 44 | ## Installation 45 | 46 | ```bash 47 | # with krew (recommended) 48 | kubectl krew install passman 49 | 50 | # get a binary from https://github.com/chrisns/kubectl-passman/releases/latest 51 | # place it in PATH and make sure it's called kubectl-passman 52 | 53 | # use go to get the most recent 54 | go install github.com/chrisns/kubectl-passman 55 | ``` 56 | 57 | ## Usage 58 | 59 | You need to JSON encode the credentials so that should look something like: 60 | 61 | ```json 62 | {"token":"00000000-0000-0000-0000-000000000000"} 63 | ``` 64 | 65 | or for a key pair: 66 | 67 | ```json 68 | { 69 | "clientCertificateData":"-----BEGIN REAL CERTIFICATE-----\nMIIC9DCCA.......-----END CERTIFICATE-----", 70 | "clientKeyData":"-----BEGIN REAL RSA PRIVATE KEY-----\nMIIE......-----END REAL RSA PRIVATE KEY-----" 71 | } 72 | ``` 73 | 74 | or for a key pair from your kube config: 75 | 76 | ```json 77 | { 78 | "client-certificate-data":"LS0tLS1CRU...LS0tCg==", 79 | "client-key-data":"LS0tLS1CRU...LS0tLS0K" 80 | } 81 | ``` 82 | 83 | If they are already in your kube config, you could retrieve them with something like: 84 | 85 | ```bash 86 | kubectl config view --raw -o json | jq '.users[] | select(.name=="kubectl-prod-user") | .user' -c 87 | ``` 88 | 89 | ### Write it to the password manager 90 | 91 | ```bash 92 | kubectl passman keychain kubectl-prod-user '[token]' 93 | # or 94 | kubectl passman 1password kubectl-prod-user '[token]' 95 | 96 | ## so should look like: 97 | kubectl passman 1password kubectl-prod-user '{"token":"00000000-0000-0000-0000-000000000000"}' 98 | # or 99 | kubectl passman 1password kubectl-prod-user '{"client-certificate-data":"...BASE64_ENCODE...","client-key-data":"...BASE64_ENCODE..."}' 100 | ``` 101 | 102 | Then add it to the `~/.kube/config`: 103 | 104 | ```bash 105 | kubectl config set-credentials \ 106 | kubectl-prod-user \ 107 | --exec-api-version=client.authentication.k8s.io/v1beta1 \ 108 | --exec-command=kubectl-passman \ 109 | --exec-arg=keychain \ # or 1password 110 | --exec-arg=kubectl-prod-user # name of [item-name] you used when you wrote to the password manager 111 | ``` 112 | 113 | ## Build 114 | 115 | ``` bash 116 | go build 117 | ``` 118 | > Note: kubectl-passman will build slightly differently on Darwin (Mac OS) to other operation systems because it uses the [go-keychain](https://github.com/keybase/go-keychain) library that needs libraries that only exist on a mac so that it can natively talk to the keychain. When compiling for other operating systems you'll get [go-keyring](https://github.com/zalando/go-keyring) instead but I've abstracted to make the interactions the same. 119 | 120 | ## Contributing 121 | 122 | I :heart: contributions, it'd be great if you could add support for your favourite password manager, work on something from the [TODO](#TODO) or any open issues as a priority, but anything else that takes your fancy too is great, though best to raise an issue to discuss before investing time into it. 123 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho= 2 | al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= 3 | github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 4 | github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 5 | github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 6 | github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4uEoM0= 7 | github.com/alessio/shellescape v1.4.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= 8 | github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= 9 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 10 | github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= 11 | github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 12 | github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= 13 | github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 14 | github.com/creasty/defaults v1.7.0 h1:eNdqZvc5B509z18lD8yc212CAqJNvfT1Jq6L8WowdBA= 15 | github.com/creasty/defaults v1.7.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM= 16 | github.com/creasty/defaults v1.8.0 h1:z27FJxCAa0JKt3utc0sCImAEb+spPucmKoOdLHvHYKk= 17 | github.com/creasty/defaults v1.8.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM= 18 | github.com/danieljoos/wincred v1.2.1 h1:dl9cBrupW8+r5250DYkYxocLeZ1Y4vB1kxgtjxw8GQs= 19 | github.com/danieljoos/wincred v1.2.1/go.mod h1:uGaFL9fDn3OLTvzCGulzE+SzjEe5NGlh5FdCcyfPwps= 20 | github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0= 21 | github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8= 22 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 23 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 24 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 25 | github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= 26 | github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 27 | github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs= 28 | github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw= 29 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 30 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 31 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 32 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 33 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 34 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 35 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 36 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 37 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 38 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 39 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 40 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 41 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 42 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 43 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 44 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 45 | github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8= 46 | github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 47 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 48 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 49 | github.com/urfave/cli v1.22.15 h1:nuqt+pdC/KqswQKhETJjo7pvn/k4xMUxgW6liI7XpnM= 50 | github.com/urfave/cli v1.22.15/go.mod h1:wSan1hmo5zeyLGBjRJbzRTNk8gwoYa2B9n4q9dmRIc0= 51 | github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ= 52 | github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po= 53 | github.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ= 54 | github.com/urfave/cli v1.22.17/go.mod h1:b0ht0aqgH/6pBYzzxURyrM4xXNgsoT/n2ZzwQiEhNVo= 55 | github.com/urfave/cli/v3 v3.1.0/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= 56 | github.com/urfave/cli/v3 v3.1.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= 57 | github.com/urfave/cli/v3 v3.2.0/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= 58 | github.com/urfave/cli/v3 v3.3.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= 59 | github.com/urfave/cli/v3 v3.3.2/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= 60 | github.com/urfave/cli/v3 v3.3.3/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= 61 | github.com/urfave/cli/v3 v3.3.8/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= 62 | github.com/urfave/cli/v3 v3.3.9/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= 63 | github.com/urfave/cli/v3 v3.4.0/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= 64 | github.com/urfave/cli/v3 v3.4.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= 65 | github.com/urfave/cli/v3 v3.5.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= 66 | github.com/urfave/cli/v3 v3.6.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= 67 | github.com/urfave/cli/v3 v3.6.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= 68 | github.com/zalando/go-keyring v0.2.4 h1:wi2xxTqdiwMKbM6TWwi+uJCG/Tum2UV0jqaQhCa9/68= 69 | github.com/zalando/go-keyring v0.2.4/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= 70 | github.com/zalando/go-keyring v0.2.5 h1:Bc2HHpjALryKD62ppdEzaFG6VxL6Bc+5v0LYpN8Lba8= 71 | github.com/zalando/go-keyring v0.2.5/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= 72 | github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s= 73 | github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI= 74 | golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= 75 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 76 | golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= 77 | golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 78 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 79 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 80 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 81 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 82 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 83 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 84 | --------------------------------------------------------------------------------