Compare commits
2 Commits
1cf7f50bfb
...
3a860914d5
| Author | SHA1 | Date | |
|---|---|---|---|
| 3a860914d5 | |||
| dd4af5b25c |
@@ -59,6 +59,8 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
build-args: |
|
||||
VERSION=${{ steps.version.outputs.tag }}
|
||||
tags: |
|
||||
${{ steps.version.outputs.registry }}/${{ secrets.REGISTRY_USER }}/${{ env.IMAGE_NAME }}:latest
|
||||
${{ steps.version.outputs.registry }}/${{ secrets.REGISTRY_USER }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.tag }}
|
||||
|
||||
@@ -12,8 +12,12 @@ RUN go mod download
|
||||
|
||||
COPY . .
|
||||
|
||||
ARG VERSION=dev
|
||||
RUN CGO_ENABLED=1 GOOS=linux go build -o keywarden -ldflags="-s -w -X git.techniverse.net/scriptos/keywarden/internal/version.Version=${VERSION}" ./cmd/keywarden/
|
||||
ARG VERSION=""
|
||||
RUN set -e; \
|
||||
if [ -z "$VERSION" ]; then \
|
||||
VERSION=$(grep 'var Version' internal/version/version.go | sed 's/.*"\(.*\)".*/\1/'); \
|
||||
fi; \
|
||||
CGO_ENABLED=1 GOOS=linux go build -o keywarden -ldflags="-s -w -X git.techniverse.net/scriptos/keywarden/internal/version.Version=${VERSION}" ./cmd/keywarden/
|
||||
|
||||
# Stage 2: Runtime
|
||||
FROM alpine:3.21
|
||||
|
||||
@@ -59,9 +59,12 @@ Server groups are used as targets for:
|
||||
1. Navigate to **Deploy**
|
||||
2. Select an **SSH key** from the dropdown (shows all keys from all users)
|
||||
3. Select a **target server**
|
||||
4. Click **Deploy**
|
||||
4. Choose an authentication method (password or existing key)
|
||||
5. Click **Deploy**
|
||||
|
||||
Keywarden connects to the target server using the system master key and appends the selected public key to the server user's `~/.ssh/authorized_keys`.
|
||||
Keywarden connects to the target server and appends the selected public key to the server user's `~/.ssh/authorized_keys`.
|
||||
|
||||
> **Owner only:** The SSH key dropdown includes the **[MASTER] System Master Key** as the first option. This allows the owner to deploy the system master key directly to servers from the Deploy page — useful for initial server setup or re-deployment after master key regeneration.
|
||||
|
||||
### Group Deployment
|
||||
|
||||
@@ -220,7 +223,7 @@ Navigate to **System** to view runtime information:
|
||||
|
||||
Keywarden automatically checks for new releases in the background by querying the Gitea releases API. If a newer version is available, a yellow update badge is displayed in the top header for **Admin** and **Owner** users. The badge links directly to the release page on Gitea.
|
||||
|
||||
The update checker is only active when the application was built with a version tag (via `--build-arg VERSION=...`). Development builds (`dev`) skip the check entirely.
|
||||
The update checker is only active when the application was built with a proper version tag. Development builds without a version skip the check entirely.
|
||||
|
||||
## Admin Settings (Owner Only)
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ The Dockerfile uses a two-stage build:
|
||||
|
||||
The runtime container runs as a non-root user (`keywarden`).
|
||||
|
||||
The build accepts an optional `VERSION` build arg (e.g. `--build-arg VERSION=v1.0.0`) which is injected into the binary via `-ldflags`. This enables the built-in update checker to compare the running version against the latest Gitea release. If omitted, the version defaults to `dev` and the update checker is disabled.
|
||||
The build accepts an optional `VERSION` build arg (e.g. `--build-arg VERSION=v1.0.0`) which is injected into the binary via `-ldflags`. If omitted, the version is automatically extracted from `internal/version/version.go`. The CI release pipeline passes the Git tag as `VERSION` automatically.
|
||||
|
||||
### Docker Compose
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ Owner → Admin → User
|
||||
| Test server connectivity | ❌ | ✅ | ✅ |
|
||||
| **Deployments** | | | |
|
||||
| Manual key deployment | ❌ | ✅ | ✅ |
|
||||
| Deploy system master key | ❌ | ❌ | ✅ |
|
||||
| Group deployment | ❌ | ✅ | ✅ |
|
||||
| **Access Assignments** | | | |
|
||||
| Create/edit/delete assignments | ❌ | ✅ | ✅ |
|
||||
@@ -88,6 +89,7 @@ Admins **cannot** access the Admin Settings page, regenerate the master key, man
|
||||
|
||||
The **Owner** role has unrestricted access. In addition to all Admin permissions, the owner can:
|
||||
|
||||
- Deploy the system master key to servers (via the Deploy page)
|
||||
- Access the Admin Settings page
|
||||
- Configure application settings (app name, session timeout, default key type)
|
||||
- Configure security settings (password policy, account lockout, MFA enforcement)
|
||||
|
||||
@@ -1434,6 +1434,36 @@ func (h *Handler) handleServerTestAuth(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// masterKeyForDeploy returns the system master key as a virtual SSHKey entry for deployment.
|
||||
// Returns nil if the master key is not available.
|
||||
func (h *Handler) masterKeyForDeploy() *models.SSHKey {
|
||||
pub, err := h.keys.GetSystemMasterKeyPublic()
|
||||
if err != nil || pub == "" {
|
||||
return nil
|
||||
}
|
||||
fp, _ := h.keys.GetSystemMasterKeyFingerprint()
|
||||
return &models.SSHKey{
|
||||
ID: -1,
|
||||
UserID: 0,
|
||||
Name: "[MASTER] System Master Key",
|
||||
KeyType: "ed25519",
|
||||
PublicKey: pub,
|
||||
Fingerprint: fp,
|
||||
}
|
||||
}
|
||||
|
||||
// prependMasterKey adds the system master key to the key list if the user is an owner.
|
||||
func (h *Handler) prependMasterKey(keyList []models.SSHKey, role string) []models.SSHKey {
|
||||
if !isOwner(role) {
|
||||
return keyList
|
||||
}
|
||||
mk := h.masterKeyForDeploy()
|
||||
if mk == nil {
|
||||
return keyList
|
||||
}
|
||||
return append([]models.SSHKey{*mk}, keyList...)
|
||||
}
|
||||
|
||||
func (h *Handler) handleDeploy(w http.ResponseWriter, r *http.Request) {
|
||||
userID := h.getUserID(r)
|
||||
user, _ := h.auth.GetUserByID(userID)
|
||||
@@ -1442,6 +1472,9 @@ func (h *Handler) handleDeploy(w http.ResponseWriter, r *http.Request) {
|
||||
groups, _ := h.servers.GetAllGroups()
|
||||
deployments, _ := h.deploy.GetDeployments(userID)
|
||||
|
||||
// Owner can deploy the system master key
|
||||
keyList = h.prependMasterKey(keyList, user.Role)
|
||||
|
||||
if r.Method == http.MethodGet {
|
||||
data := &PageData{
|
||||
Title: "Deploy Keys",
|
||||
@@ -1461,14 +1494,26 @@ func (h *Handler) handleDeploy(w http.ResponseWriter, r *http.Request) {
|
||||
serverID, _ := strconv.ParseInt(r.FormValue("server_id"), 10, 64)
|
||||
authMethod := r.FormValue("auth_method")
|
||||
|
||||
key, err := h.keys.GetKeyByID(keyID, userID)
|
||||
if err != nil {
|
||||
// Try global access for admin/owner deploying other users' keys
|
||||
key, err = h.keys.GetKeyByIDGlobal(keyID)
|
||||
if err != nil {
|
||||
var key *models.SSHKey
|
||||
var err error
|
||||
|
||||
if keyID == -1 && isOwner(user.Role) {
|
||||
// Owner deploying the system master key
|
||||
key = h.masterKeyForDeploy()
|
||||
if key == nil {
|
||||
http.Redirect(w, r, "/deploy", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
key, err = h.keys.GetKeyByID(keyID, userID)
|
||||
if err != nil {
|
||||
// Try global access for admin/owner deploying other users' keys
|
||||
key, err = h.keys.GetKeyByIDGlobal(keyID)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/deploy", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
server, err := h.servers.GetByIDGlobal(serverID)
|
||||
@@ -1527,14 +1572,26 @@ func (h *Handler) handleDeployGroup(w http.ResponseWriter, r *http.Request) {
|
||||
groupID, _ := strconv.ParseInt(r.FormValue("group_id"), 10, 64)
|
||||
authMethod := r.FormValue("auth_method")
|
||||
|
||||
key, err := h.keys.GetKeyByID(keyID, userID)
|
||||
if err != nil {
|
||||
// Try global access for admin/owner deploying other users' keys
|
||||
key, err = h.keys.GetKeyByIDGlobal(keyID)
|
||||
if err != nil {
|
||||
var key *models.SSHKey
|
||||
var keyErr error
|
||||
|
||||
if keyID == -1 && isOwner(user.Role) {
|
||||
// Owner deploying the system master key
|
||||
key = h.masterKeyForDeploy()
|
||||
if key == nil {
|
||||
http.Redirect(w, r, "/deploy", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
key, keyErr = h.keys.GetKeyByID(keyID, userID)
|
||||
if keyErr != nil {
|
||||
// Try global access for admin/owner deploying other users' keys
|
||||
key, keyErr = h.keys.GetKeyByIDGlobal(keyID)
|
||||
if keyErr != nil {
|
||||
http.Redirect(w, r, "/deploy", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
group, err := h.servers.GetGroupByIDGlobal(groupID)
|
||||
@@ -1546,6 +1603,7 @@ func (h *Handler) handleDeployGroup(w http.ResponseWriter, r *http.Request) {
|
||||
members, err := h.servers.GetGroupMembersGlobal(groupID)
|
||||
if err != nil || len(members) == 0 {
|
||||
keyList, _ := h.keys.GetAllKeys()
|
||||
keyList = h.prependMasterKey(keyList, user.Role)
|
||||
serverList, _ := h.servers.GetAllServers()
|
||||
groups, _ := h.servers.GetAllGroups()
|
||||
deployments, _ := h.deploy.GetDeployments(userID)
|
||||
@@ -1601,6 +1659,7 @@ func (h *Handler) handleDeployGroup(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
keyList, _ := h.keys.GetAllKeys()
|
||||
keyList = h.prependMasterKey(keyList, user.Role)
|
||||
serverList, _ := h.servers.GetAllServers()
|
||||
groups, _ := h.servers.GetAllGroups()
|
||||
deployments, _ := h.deploy.GetDeployments(userID)
|
||||
|
||||
Reference in New Issue
Block a user