Repository: openpubkey/openpubkey
Pull request #248 (2025-02-18 23:09:30)

Adds installer to OPKSSH to get servers setup quickly
This PR does two things:
- It adds an install command to opkssh so that opkssh can be easily setup on a server
- It reorganizes some of the policy files

![evidence it works](https://github.com/user-attachments/assets/7ddbd319-a626-4de1-9183-e77d68ddb110)

# Tests Run

Only tested against `ubuntu 53-Ubuntu SMP PREEMPT_DYNAMIC Sat Jan 11 00:06:25 UTC 2025`

## Fresh install

Delete the Authorized Key Commands from /etc/ssh/sshd_config
```
rm -rf /etc/opk
rm -rf ~/.opk
```
./opkssh install

Works

## Existing Authorized Key Commands

Ensure there is a configure Authorized Key Command set

```
rm -rf /etc/opk
rm -rf ~/.opk
```
./opkssh install

Works

# Testing with Azure

`./opkssh e0 owlvendor@hotmail.com azure`



Patch:
diff --git a/go.mod b/go.mod
index e921d64..bf1cd5d 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,6 @@ go 1.22
 require (
 	filippo.io/bigmod v0.0.3
 	github.com/awnumar/memguard v0.22.3
-	github.com/coreos/go-oidc/v3 v3.12.0
 	github.com/docker/go-connections v0.5.0
 	github.com/go-webauthn/webauthn v0.8.6
 	github.com/google/uuid v1.6.0
@@ -96,5 +95,5 @@ require (
 	golang.org/x/oauth2 v0.25.0 // indirect
 	golang.org/x/sys v0.29.0 // indirect
 	golang.org/x/text v0.21.0 // indirect
-	gopkg.in/yaml.v3 v3.0.1
+	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/go.sum b/go.sum
index 5968071..73a0c12 100644
--- a/go.sum
+++ b/go.sum
@@ -22,8 +22,6 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
 github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
 github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
 github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
-github.com/coreos/go-oidc/v3 v3.12.0 h1:sJk+8G2qq94rDI6ehZ71Bol3oUHy63qNYmkiSjrc/Jo=
-github.com/coreos/go-oidc/v3 v3.12.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
 github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
 github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
diff --git a/opkssh/commands/install.go b/opkssh/commands/install.go
new file mode 100644
index 0000000..bd9d5aa
--- /dev/null
+++ b/opkssh/commands/install.go
@@ -0,0 +1,87 @@
+// Copyright 2025 OpenPubkey
+//
+// 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.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package commands
+
+import (
+	"fmt"
+	"os"
+	"os/exec"
+)
+
+func Install() error {
+	if os.Geteuid() != 0 {
+		return fmt.Errorf("this command must be run as root or sudo")
+	}
+
+	opksshExePath, err := os.Executable()
+	if err != nil {
+		return fmt.Errorf("failed to get executable path: %v", err)
+	}
+
+	installScript := `
+#!/bin/sh
+
+PROVIDER_GOOGLE="https://accounts.google.com 992028499768-ce9juclb3vvckh23r83fjkmvf1lvjq18.apps.googleusercontent.com 24h"
+PROVIDER_MICROSOFT="https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0 bd345b9c-6902-400d-9e18-45abdf0f698f 24h"
+
+sudo mkdir -p /etc/opk
+sudo touch /etc/opk/auth_id
+sudo chown root /etc/opk/auth_id
+sudo chmod 600 /etc/opk/auth_id
+
+sudo touch /etc/opk/providers
+sudo chown root /etc/opk/providers
+sudo chmod 600 /etc/opk/providers
+
+if [ -s /etc/opk/providers ]; then
+	read -p "The providers policy file (/etc/opk/providers) is not empty. Do you wish to override the original values? (y/n): " choice
+	case "$choice" in 
+		y|Y ) sudo truncate -s 0 /etc/opk/providers
+			echo "$PROVIDER_GOOGLE" >> /etc/opk/providers
+			echo "$PROVIDER_MICROSOFT" >> /etc/opk/providers
+		;;
+		n|N ) echo "Keeping original providers values.";;
+		* ) echo "Invalid choice. Exiting." && exit 1;;
+	esac
+else
+	echo "$PROVIDER_GOOGLE" >> /etc/opk/providers
+	echo "$PROVIDER_MICROSOFT" >> /etc/opk/providers
+fi
+
+sed -i '/^AuthorizedKeysCommand /s/^/#/' /etc/ssh/sshd_config
+sed -i '/^AuthorizedKeysCommandUser /s/^/#/' /etc/ssh/sshd_config
+echo "AuthorizedKeysCommand /etc/opk/opkssh verify %u %k %t\nAuthorizedKeysCommandUser root" >> /etc/ssh/sshd_config
+sudo systemctl restart ssh
+
+`
+	installScript += fmt.Sprintf("sudo cp %s /etc/opk/opkssh\n", opksshExePath)
+
+	// removeScript :=`
+	// rm -rf /etc/opk
+	// rm -rf ~/.opk
+	// `
+
+	cmd := exec.Command("sh", "-c", installScript)
+	cmd.Stdout = os.Stdout
+	cmd.Stderr = os.Stderr
+	err = cmd.Run()
+	if err != nil {
+		return fmt.Errorf("failed to run script: %v", err)
+	}
+
+	return nil
+}
diff --git a/opkssh/docs/config.md b/opkssh/docs/config.md
index 5d4dc5a..9887d66 100644
--- a/opkssh/docs/config.md
+++ b/opkssh/docs/config.md
@@ -111,9 +111,9 @@ sudo chown root /etc/opk/providers
 sudo chmod 600 /etc/opk/providers
 
 sudo su
-RUN sed -i '/^AuthorizedKeysCommand /s/^/#/' /etc/ssh/sshd_config
-RUN sed -i '/^AuthorizedKeysCommandUser /s/^/#/' /etc/ssh/sshd_config
-RUN echo "AuthorizedKeysCommand /etc/opk/opkssh verify %u %k %t\nAuthorizedKeysCommandUser root" >> /etc/ssh/sshd_config
+sed -i '/^AuthorizedKeysCommand /s/^/#/' /etc/ssh/sshd_config
+sed -i '/^AuthorizedKeysCommandUser /s/^/#/' /etc/ssh/sshd_config
+echo "AuthorizedKeysCommand /etc/opk/opkssh verify %u %k %t\nAuthorizedKeysCommandUser root" >> /etc/ssh/sshd_config
 sudo systemctl restart ssh
 
 echo "https://accounts.google.com 992028499768-ce9juclb3vvckh23r83fjkmvf1lvjq18.apps.googleusercontent.com 24h" >> /etc/opk/providers
diff --git a/opkssh/main.go b/opkssh/main.go
index 15f9005..7cce435 100644
--- a/opkssh/main.go
+++ b/opkssh/main.go
@@ -33,8 +33,8 @@ import (
 
 	"github.com/openpubkey/openpubkey/client/choosers"
 	"github.com/openpubkey/openpubkey/opkssh/commands"
-	"github.com/openpubkey/openpubkey/opkssh/config"
 	"github.com/openpubkey/openpubkey/opkssh/policy"
+	"github.com/openpubkey/openpubkey/opkssh/policy/files"
 	"github.com/openpubkey/openpubkey/providers"
 )
 
@@ -54,7 +54,7 @@ func main() {
 
 func run() int {
 	if len(os.Args) < 2 {
-		fmt.Println("Example SSH key generator using OpenPubkey: command choices are: login, verify, and add")
+		fmt.Println("Example SSH key generator using OpenPubkey: command choices are: login, verify, add, and install")
 		return 1
 	}
 	command := os.Args[1]
@@ -238,6 +238,20 @@ func run() int {
 		} else {
 			log.Println("Successfully added new policy to", policyFilePath)
 		}
+	case "install":
+		// The "install" command is sets up and installs opkssh on a server.
+		// It currently has only been tested on Ubuntu. It must be run as root.
+		//
+		// Example line:
+		// 		./opkssh install
+		err := commands.Install()
+		if err != nil {
+			log.Println("failed to install opkssh:", err)
+			return 1
+		} else {
+			log.Println("successfully installed opkssh")
+			return 0
+		}
 	default:
 		log.Println("ERROR! Unrecognized command:", command)
 		return 1
@@ -247,7 +261,7 @@ func run() int {
 }
 
 func printConfigProblems() {
-	problems := config.ConfigProblems().GetProblems()
+	problems := files.ConfigProblems().GetProblems()
 	if len(problems) > 0 {
 		log.Println("Warning: Encountered the following configuration problems:")
 		for _, problem := range problems {
diff --git a/opkssh/config/configlog.go b/opkssh/policy/files/configlog.go
similarity index 99%
rename from opkssh/config/configlog.go
rename to opkssh/policy/files/configlog.go
index 3247789..b5541f9 100644
--- a/opkssh/config/configlog.go
+++ b/opkssh/policy/files/configlog.go
@@ -14,7 +14,7 @@
 //
 // SPDX-License-Identifier: Apache-2.0
 
-package config
+package files
 
 import (
 	"fmt"
diff --git a/opkssh/config/table.go b/opkssh/policy/files/table.go
similarity index 99%
rename from opkssh/config/table.go
rename to opkssh/policy/files/table.go
index 961e89f..a9a0514 100644
--- a/opkssh/config/table.go
+++ b/opkssh/policy/files/table.go
@@ -14,7 +14,7 @@
 //
 // SPDX-License-Identifier: Apache-2.0
 
-package config
+package files
 
 import "strings"
 
diff --git a/opkssh/policy/providerpolicy.go b/opkssh/policy/providerpolicy.go
index 7a87c32..c0c49f7 100644
--- a/opkssh/policy/providerpolicy.go
+++ b/opkssh/policy/providerpolicy.go
@@ -20,7 +20,7 @@ import (
 	"fmt"
 	"strings"
 
-	"github.com/openpubkey/openpubkey/opkssh/config"
+	"github.com/openpubkey/openpubkey/opkssh/policy/files"
 	"github.com/openpubkey/openpubkey/providers"
 	"github.com/openpubkey/openpubkey/verifier"
 	"github.com/spf13/afero"
@@ -143,8 +143,8 @@ func (o *ProvidersFileLoader) LoadProviderPolicy(path string) (*ProviderPolicy,
 }
 
 // FromTable decodes whitespace delimited input into policy.Policy
-func (o ProvidersFileLoader) ToTable(opPolicies ProviderPolicy) config.Table {
-	table := config.Table{}
+func (o ProvidersFileLoader) ToTable(opPolicies ProviderPolicy) files.Table {
+	table := files.Table{}
 	for _, opPolicy := range opPolicies.rows {
 		table.AddRow(opPolicy.Issuer, opPolicy.ClientID, opPolicy.ExpirationPolicy)
 	}
@@ -154,21 +154,21 @@ func (o ProvidersFileLoader) ToTable(opPolicies ProviderPolicy) config.Table {
 // FromTable decodes whitespace delimited input into policy.Policy
 // Path is passed only for logging purposes
 func (o *ProvidersFileLoader) FromTable(input []byte, path string) *ProviderPolicy {
-	table := config.NewTable(input)
+	table := files.NewTable(input)
 	policy := &ProviderPolicy{
 		rows: []ProvidersPolicyRow{},
 	}
 	for i, row := range table.GetRows() {
 		// Error should not break everyone's ability to login, skip those rows
 		if len(row) != 3 {
-			configProblem := config.ConfigProblem{
+			configProblem := files.ConfigProblem{
 				Filepath:            path,
 				OffendingLine:       strings.Join(row, " "),
 				OffendingLineNumber: i,
 				ErrorMessage:        fmt.Sprintf("wrong number of arguments (expected=3, got=%d)", len(row)),
 				Source:              "providers policy file",
 			}
-			config.ConfigProblems().RecordProblem(configProblem)
+			files.ConfigProblems().RecordProblem(configProblem)
 			continue
 		}
 		policyRow := ProvidersPolicyRow{
diff --git a/opkssh/policy/userpolicy.go b/opkssh/policy/userpolicy.go
index 8bef0b3..d3a1788 100644
--- a/opkssh/policy/userpolicy.go
+++ b/opkssh/policy/userpolicy.go
@@ -21,7 +21,7 @@ import (
 	"log"
 	"strings"
 
-	"github.com/openpubkey/openpubkey/opkssh/config"
+	"github.com/openpubkey/openpubkey/opkssh/policy/files"
 )
 
 // User is an opkssh policy user entry
@@ -44,19 +44,19 @@ type Policy struct {
 // FromTable decodes whitespace delimited input into policy.Policy
 func FromTable(input []byte, path string) *Policy {
 
-	table := config.NewTable(input)
+	table := files.NewTable(input)
 	policy := &Policy{}
 	for i, row := range table.GetRows() {
 		// Error should not break everyone's ability to login, skip those rows
 		if len(row) != 3 {
-			configProblem := config.ConfigProblem{
+			configProblem := files.ConfigProblem{
 				Filepath:            path,
 				OffendingLine:       strings.Join(row, " "),
 				OffendingLineNumber: i,
 				ErrorMessage:        fmt.Sprintf("wrong number of arguments (expected=3, got=%d)", len(row)),
 				Source:              "user policy file",
 			}
-			config.ConfigProblems().RecordProblem(configProblem)
+			files.ConfigProblems().RecordProblem(configProblem)
 			continue
 		}
 		user := User{
@@ -116,7 +116,7 @@ func (p *Policy) AddAllowedPrincipal(principal string, userEmail string, issuer
 
 // ToTable encodes the policy into a whitespace delimited table
 func (p *Policy) ToTable() ([]byte, error) {
-	table := config.Table{}
+	table := files.Table{}
 	for _, user := range p.Users {
 		for _, principal := range user.Principals {
 			table.AddRow(principal, user.Email, user.Issuer)


Test patch:
diff --git a/opkssh/config/configlog_test.go b/opkssh/policy/files/configlog_test.go
similarity index 99%
rename from opkssh/config/configlog_test.go
rename to opkssh/policy/files/configlog_test.go
index b6d6e17..2a7f63b 100644
--- a/opkssh/config/configlog_test.go
+++ b/opkssh/policy/files/configlog_test.go
@@ -14,7 +14,7 @@
 //
 // SPDX-License-Identifier: Apache-2.0
 
-package config
+package files
 
 import (
 	"testing"
diff --git a/opkssh/config/table_test.go b/opkssh/policy/files/table_test.go
similarity index 99%
rename from opkssh/config/table_test.go
rename to opkssh/policy/files/table_test.go
index e9ec35f..61c2c69 100644
--- a/opkssh/config/table_test.go
+++ b/opkssh/policy/files/table_test.go
@@ -14,7 +14,7 @@
 //
 // SPDX-License-Identifier: Apache-2.0
 
-package config
+package files
 
 import (
 	"testing"
diff --git a/opkssh/policy/userfileloader_test.go b/opkssh/policy/userfileloader_test.go
index ea8b062..450d362 100644
--- a/opkssh/policy/userfileloader_test.go
+++ b/opkssh/policy/userfileloader_test.go
@@ -23,8 +23,8 @@ import (
 	"path"
 	"testing"
 
-	"github.com/openpubkey/openpubkey/opkssh/config"
 	"github.com/openpubkey/openpubkey/opkssh/policy"
+	"github.com/openpubkey/openpubkey/opkssh/policy/files"
 	"github.com/spf13/afero"
 	"github.com/stretchr/testify/require"
 )
@@ -126,8 +126,8 @@ func TestLoadUserPolicy_ErrorFile(t *testing.T) {
 
 	policy, path, err := policyLoader.LoadUserPolicy(ValidUser.Username, false)
 	require.NoError(t, err)
-	require.False(t, config.ConfigProblems().NoProblems())
-	config.ConfigProblems().Clear()
+	require.False(t, files.ConfigProblems().NoProblems())
+	files.ConfigProblems().Clear()
 
 	require.NotNil(t, policy, "should return policy even if error")
 	require.NotEmpty(t, path, "should return path even if error")
@@ -296,8 +296,8 @@ func TestLoadSystemDefaultPolicy_ErrorFile(t *testing.T) {
 	require.NoError(t, err)
 	policy, err := policyLoader.LoadSystemDefaultPolicy()
 	require.NoError(t, err)
-	require.False(t, config.ConfigProblems().NoProblems())
-	config.ConfigProblems().Clear()
+	require.False(t, files.ConfigProblems().NoProblems())
+	files.ConfigProblems().Clear()
 
 	require.NotNil(t, policy, "should return policy even if problems encountered")
 }
diff --git a/opkssh/test/integration/add_test.go b/opkssh/test/integration/add_test.go
index ff8480f..dc69902 100644
--- a/opkssh/test/integration/add_test.go
+++ b/opkssh/test/integration/add_test.go
@@ -25,8 +25,8 @@ import (
 	"strings"
 	"testing"
 
-	"github.com/openpubkey/openpubkey/opkssh/config"
 	"github.com/openpubkey/openpubkey/opkssh/policy"
+	"github.com/openpubkey/openpubkey/opkssh/policy/files"
 	"github.com/openpubkey/openpubkey/opkssh/test/integration/ssh_server"
 	"github.com/testcontainers/testcontainers-go"
 
@@ -149,7 +149,7 @@ func TestAdd(t *testing.T) {
 				code, policyContents := executeCommandAsUser(t, container.Container, []string{"cat", expectedPolicyFilepath}, RootUser)
 				require.Equal(t, 0, code, "failed to read policy file")
 				gotPolicy := policy.FromTable([]byte(policyContents), "test-path")
-				require.True(t, config.ConfigProblems().NoProblems())
+				require.True(t, files.ConfigProblems().NoProblems())
 
 				expectedPolicy := &policy.Policy{
 					Users: []policy.User{