├── api ├── kubeseal_webgui_api │ ├── __init__.py │ ├── routers │ │ ├── __init__.py │ │ ├── config.py │ │ ├── kubernetes_namespace_resolver.py │ │ ├── kubernetes.py │ │ ├── models.py │ │ ├── mock_namespace_resolver.py │ │ └── kubeseal.py │ ├── main.py │ ├── app.py │ └── app_config.py ├── README.md ├── bin │ └── docker-entrypoint.sh ├── config │ └── logging_config.yaml ├── tests │ ├── test_kubernetes_service.py │ ├── test_kubeseal_service.py │ └── test_run_kubeseal.py ├── pyproject.toml └── .pre-commit-config.yaml ├── ui ├── .env.development ├── public │ ├── favicon.ico │ └── config.json ├── src │ ├── assets │ │ ├── logo.png │ │ ├── logo-2025.png │ │ ├── kubeseal-webgui-logo.png │ │ ├── kubeseal-webgui-logo-2025.png │ │ └── styles.css │ ├── plugins │ │ └── vuetify.js │ ├── components │ │ ├── AppLogo.vue │ │ ├── SecretKeyCard.vue │ │ ├── DarkMode.vue │ │ ├── SealedSecretCard.vue │ │ ├── SecretInput.vue │ │ ├── AppConfig.vue │ │ ├── SecretsList.vue │ │ ├── SecretsResults.vue │ │ ├── SecretFormInputs.vue │ │ └── Secrets.vue │ ├── App.vue │ ├── utils │ │ └── mockData.js │ ├── composables │ │ ├── useConfig.js │ │ └── useSecrets.js │ └── main.js ├── .prettierrc.json ├── .gitignore ├── README.md ├── vite.config.mjs ├── eslint.config.js ├── nginx-default.conf ├── package.json ├── hooks │ └── 50-envsubst-default-conf.sh ├── index.html └── tests │ └── test_ui.py ├── demo ├── kubeseal-webgui-logo.jpg ├── kubseal-demo-1.0.0.gif ├── kubseal-demo-1.0.2.gif ├── kubseal-demo-1.1.0.gif ├── kubseal-demo-2.0.0.gif ├── kubseal-demo-3.0.0.gif ├── kubseal-demo-4.0.0.gif ├── kubseal-demo-4.0.2.gif ├── kubseal-demo-4.4.0.gif ├── kubeseal-webgui-logo-2025.png └── kubeseal-webgui-demo-4.6.0.gif ├── .typos.toml ├── .dockerignore ├── chart └── kubeseal-webgui │ ├── ci │ ├── fetch.yaml │ └── environment.yaml │ ├── Chart.yaml │ ├── templates │ ├── serviceaccount.yaml │ ├── service.yaml │ ├── clusterrole.yaml │ ├── clusterrolebinding.yaml │ ├── configmap.yaml │ ├── route-ui.yaml │ ├── ingress.yaml │ ├── _helpers.tpl │ └── deployment.yaml │ ├── .helmignore │ ├── files │ └── logging_config.yaml │ ├── values.yaml │ └── README.md ├── SECURITY.md ├── .gitignore ├── .deepsource.toml ├── .github ├── workflows │ ├── typo.yml │ ├── semantic-release.yml │ ├── frontend-tests.yml │ ├── main.yml │ ├── ghcr-build.yml │ ├── codeql-analysis.yml │ ├── helm-release.yml │ └── kind.yaml └── ISSUE_TEMPLATE │ └── bug_report.md ├── kind-config.yaml ├── NOTICE ├── Dockerfile.ui ├── dev └── bump_version.sh ├── .releaserc ├── .pre-commit-config.yaml ├── Dockerfile.api ├── README.md ├── kind-setup.sh └── LICENSE /api/kubeseal_webgui_api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /api/kubeseal_webgui_api/routers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/.env.development: -------------------------------------------------------------------------------- 1 | VITE_MOCK_NAMESPACES=true 2 | -------------------------------------------------------------------------------- /ui/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jaydee94/kubeseal-webgui/HEAD/ui/public/favicon.ico -------------------------------------------------------------------------------- /ui/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jaydee94/kubeseal-webgui/HEAD/ui/src/assets/logo.png -------------------------------------------------------------------------------- /api/README.md: -------------------------------------------------------------------------------- 1 | # API of kubeseal-webgui 2 | 3 | This backend is used to encrypt secrets with the kubeseal binary. 4 | -------------------------------------------------------------------------------- /demo/kubeseal-webgui-logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jaydee94/kubeseal-webgui/HEAD/demo/kubeseal-webgui-logo.jpg -------------------------------------------------------------------------------- /demo/kubseal-demo-1.0.0.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jaydee94/kubeseal-webgui/HEAD/demo/kubseal-demo-1.0.0.gif -------------------------------------------------------------------------------- /demo/kubseal-demo-1.0.2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jaydee94/kubeseal-webgui/HEAD/demo/kubseal-demo-1.0.2.gif -------------------------------------------------------------------------------- /demo/kubseal-demo-1.1.0.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jaydee94/kubeseal-webgui/HEAD/demo/kubseal-demo-1.1.0.gif -------------------------------------------------------------------------------- /demo/kubseal-demo-2.0.0.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jaydee94/kubeseal-webgui/HEAD/demo/kubseal-demo-2.0.0.gif -------------------------------------------------------------------------------- /demo/kubseal-demo-3.0.0.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jaydee94/kubeseal-webgui/HEAD/demo/kubseal-demo-3.0.0.gif -------------------------------------------------------------------------------- /demo/kubseal-demo-4.0.0.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jaydee94/kubeseal-webgui/HEAD/demo/kubseal-demo-4.0.0.gif -------------------------------------------------------------------------------- /demo/kubseal-demo-4.0.2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jaydee94/kubeseal-webgui/HEAD/demo/kubseal-demo-4.0.2.gif -------------------------------------------------------------------------------- /demo/kubseal-demo-4.4.0.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jaydee94/kubeseal-webgui/HEAD/demo/kubseal-demo-4.4.0.gif -------------------------------------------------------------------------------- /ui/src/assets/logo-2025.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jaydee94/kubeseal-webgui/HEAD/ui/src/assets/logo-2025.png -------------------------------------------------------------------------------- /.typos.toml: -------------------------------------------------------------------------------- 1 | [files] 2 | # Ignore files or directories 3 | extend-exclude = [ 4 | "chart/kubeseal-webgui/values.yaml" 5 | ] -------------------------------------------------------------------------------- /demo/kubeseal-webgui-logo-2025.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jaydee94/kubeseal-webgui/HEAD/demo/kubeseal-webgui-logo-2025.png -------------------------------------------------------------------------------- /demo/kubeseal-webgui-demo-4.6.0.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jaydee94/kubeseal-webgui/HEAD/demo/kubeseal-webgui-demo-4.6.0.gif -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | .* 3 | !api/pyproject.toml 4 | !api/README.md 5 | !api/bin 6 | !api/config 7 | !api/kubeseal_webgui_api 8 | !ui/* 9 | -------------------------------------------------------------------------------- /ui/src/assets/kubeseal-webgui-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jaydee94/kubeseal-webgui/HEAD/ui/src/assets/kubeseal-webgui-logo.png -------------------------------------------------------------------------------- /ui/src/assets/kubeseal-webgui-logo-2025.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jaydee94/kubeseal-webgui/HEAD/ui/src/assets/kubeseal-webgui-logo-2025.png -------------------------------------------------------------------------------- /chart/kubeseal-webgui/ci/fetch.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | sealedSecrets: 4 | autoFetchCert: true 5 | controllerName: seal 6 | controllerNamespace: secrets 7 | -------------------------------------------------------------------------------- /chart/kubeseal-webgui/ci/environment.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | api: 4 | environment: 5 | UWSGI_DISABLE_LOGGING: 1 6 | UWSGI_PROCESSES: 4 7 | UWSGI_THREADS: 2 8 | -------------------------------------------------------------------------------- /ui/public/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "api_url": "", 3 | "display_name": "Test", 4 | "kubeseal_webgui_ui_version": "2.1.0", 5 | "kubeseal_webgui_api_version": "2.1.0" 6 | } -------------------------------------------------------------------------------- /chart/kubeseal-webgui/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: kubeseal-webgui 3 | description: A Helm chart for installing kubeseal-webgui 4 | version: 6.0.5 5 | appVersion: 4.6.1 6 | -------------------------------------------------------------------------------- /ui/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/prettierrc", 3 | "semi": false, 4 | "tabWidth": 2, 5 | "singleQuote": true, 6 | "printWidth": 100, 7 | "trailingComma": "none" 8 | } -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 2.x.x | :white_check_mark: | 8 | | 1.x.x | :x: | 9 | -------------------------------------------------------------------------------- /ui/src/plugins/vuetify.js: -------------------------------------------------------------------------------- 1 | // Styles 2 | import '@mdi/font/css/materialdesignicons.css' 3 | import 'vuetify/styles' 4 | 5 | // Vuetify 6 | import { createVuetify } from 'vuetify' 7 | 8 | export default createVuetify( 9 | // https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides 10 | ) 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | .mypy_cache/ 3 | .pytest_cache/ 4 | .ruff_cache/ 5 | .vscode/ 6 | venv/ 7 | app/static/.webassets-cache/ 8 | app/static/screen.css 9 | Pipfile.lock 10 | helm 11 | kubeseal 12 | kubectl 13 | chart/kubeseal-webgui/values-local.yaml 14 | test.sh 15 | test.yaml 16 | *.pyc 17 | .coverage 18 | .venv/ 19 | .vscode/ 20 | -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /api/kubeseal_webgui_api/main.py: -------------------------------------------------------------------------------- 1 | from os import environ 2 | 3 | import uvicorn 4 | 5 | bind_address = environ.get("HOST", "127.0.0.1") 6 | listen_port = int(environ.get("PORT", "5000")) 7 | 8 | if __name__ == "__main__": 9 | uvicorn.run( 10 | "kubeseal_webgui_api.app:app", 11 | host=bind_address, 12 | port=listen_port, 13 | ) 14 | -------------------------------------------------------------------------------- /api/bin/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -eu 2 | 3 | : "${HOST:=0.0.0.0}" 4 | : "${PORT:=5000}" 5 | : "${BASE_PATH:=/}" 6 | 7 | case "${1:-}" in 8 | -*|'') : ;; 9 | uwsgi) shift ;; 10 | *) exec "$@" ;; 11 | esac 12 | 13 | set -- --http-socket "${HOST}:${PORT}" \ 14 | --mount "${BASE_PATH}=kubeseal_webgui_api.run:app" \ 15 | "$@" 16 | 17 | exec uwsgi "$@" 18 | -------------------------------------------------------------------------------- /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | test_patterns = [ 4 | "api/tests/**", 5 | "ui/tests/**", 6 | "test_*.py" 7 | ] 8 | 9 | [[analyzers]] 10 | name = "python" 11 | enabled = true 12 | 13 | [analyzers.meta] 14 | runtime_version = "3.x.x" 15 | 16 | [[analyzers]] 17 | name = "docker" 18 | enabled = true 19 | 20 | [analyzers.meta] 21 | dockerfile_paths = ["Dockerfile"] 22 | -------------------------------------------------------------------------------- /.github/workflows/typo.yml: -------------------------------------------------------------------------------- 1 | name: Check Typos 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - reopened 8 | - synchronize 9 | 10 | jobs: 11 | typos: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v4 17 | 18 | - name: Run typos check 19 | uses: crate-ci/typos@v1.40.0 20 | -------------------------------------------------------------------------------- /chart/kubeseal-webgui/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if not .Values.customServiceAccountName }} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: kubeseal-webgui 6 | labels: 7 | {{- include "kubeseal-webgui.labels" . | nindent 4 }} 8 | {{- with .Values.route.annotations }} 9 | annotations: 10 | {{ toYaml . | indent 4 }} 11 | {{- end }} 12 | {{- end }} 13 | 14 | -------------------------------------------------------------------------------- /chart/kubeseal-webgui/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "kubeseal-webgui.fullname" . }} 5 | labels: 6 | {{- include "kubeseal-webgui.labels" . | nindent 4 }} 7 | spec: 8 | selector: 9 | app: {{ template "kubeseal-webgui.name" . }} 10 | type: ClusterIP 11 | ports: 12 | - port: 8080 13 | targetPort: ui 14 | protocol: TCP 15 | name: ui 16 | -------------------------------------------------------------------------------- /kind-config.yaml: -------------------------------------------------------------------------------- 1 | kind: Cluster 2 | apiVersion: kind.x-k8s.io/v1alpha4 3 | nodes: 4 | - role: control-plane 5 | kubeadmConfigPatches: 6 | - | 7 | kind: InitConfiguration 8 | nodeRegistration: 9 | kubeletExtraArgs: 10 | node-labels: "ingress-ready=true" 11 | extraPortMappings: 12 | - containerPort: 80 13 | hostPort: 80 14 | protocol: TCP 15 | - containerPort: 443 16 | hostPort: 443 17 | protocol: TCP 18 | -------------------------------------------------------------------------------- /chart/kubeseal-webgui/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /ui/README.md: -------------------------------------------------------------------------------- 1 | # kubeseal-webgui 2 | 3 | ## Project setup 4 | 5 | ```console 6 | npm install 7 | ``` 8 | 9 | ### Compiles and hot-reloads for development 10 | 11 | ```console 12 | npm run dev 13 | ``` 14 | 15 | ### Compiles and minifies for production 16 | 17 | ```console 18 | npm run build 19 | ``` 20 | 21 | ### Lints and fixes files 22 | 23 | ```console 24 | npm run lint 25 | ``` 26 | 27 | ### Customize configuration 28 | 29 | See [Configuration Reference](https://vitejs.dev/config/). 30 | -------------------------------------------------------------------------------- /chart/kubeseal-webgui/templates/clusterrole.yaml: -------------------------------------------------------------------------------- 1 | {{- if not .Values.customServiceAccountName }} 2 | kind: ClusterRole 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | metadata: 5 | name: kubeseal-webgui-list-namespaces 6 | labels: 7 | {{- include "kubeseal-webgui.labels" . | nindent 4 }} 8 | {{- with .Values.route.annotations }} 9 | annotations: 10 | {{ toYaml . | indent 4 }} 11 | {{- end }} 12 | rules: 13 | - apiGroups: [""] 14 | resources: ["namespaces"] 15 | verbs: ["list"] 16 | {{- end }} 17 | 18 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2023 Jan Herber 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /Dockerfile.ui: -------------------------------------------------------------------------------- 1 | FROM node:lts-slim AS ui-build-stage 2 | WORKDIR /ui 3 | COPY ui/package*.json ./ 4 | RUN npm install --legacy-peer-deps 5 | COPY ui/ ./ 6 | RUN npm run build && \ 7 | npm cache clean --force 8 | 9 | FROM quay.io/nginx/nginx-unprivileged:stable-alpine AS ui-production-stage 10 | USER root 11 | RUN apk update && \ 12 | apk upgrade && \ 13 | rm -rf /var/cache/apk/* 14 | USER nginx 15 | COPY --chown=101:101 --from=ui-build-stage /ui/dist /usr/share/nginx/html/ 16 | COPY --chown=101:101 ui/nginx-default.conf /etc/nginx/conf.d/default.conf 17 | COPY --chown=101:101 ui/hooks /docker-entrypoint.d/ 18 | -------------------------------------------------------------------------------- /chart/kubeseal-webgui/templates/clusterrolebinding.yaml: -------------------------------------------------------------------------------- 1 | {{- if not .Values.customServiceAccountName }} 2 | kind: ClusterRoleBinding 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | metadata: 5 | name: kubeseal-webgui-list-namespaces 6 | labels: 7 | {{- include "kubeseal-webgui.labels" . | nindent 4 }} 8 | {{- with .Values.route.annotations }} 9 | annotations: 10 | {{ toYaml . | indent 4 }} 11 | {{- end }} 12 | subjects: 13 | - kind: ServiceAccount 14 | name: kubeseal-webgui 15 | namespace: {{ .Release.Namespace }} 16 | roleRef: 17 | kind: ClusterRole 18 | name: kubeseal-webgui-list-namespaces 19 | apiGroup: rbac.authorization.k8s.io 20 | {{- end }} 21 | 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: jaysiee, marvinf95, Jaydee94 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /ui/vite.config.mjs: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url' 2 | 3 | import { defineConfig } from 'vite' 4 | import vue from '@vitejs/plugin-vue' 5 | import vuetify from 'vite-plugin-vuetify' 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig({ 9 | server: { 10 | port: 8080, 11 | proxy: { 12 | "^/(secrets$|namespaces$|config$|docs|openapi\\.json)": { 13 | target: "http://localhost:5000", 14 | changeOrigin: false 15 | } 16 | } 17 | }, 18 | plugins: [ 19 | vue(), 20 | vuetify({ 21 | autoImport: true 22 | }) 23 | ], 24 | resolve: { 25 | alias: { 26 | '@': fileURLToPath(new URL('./src', import.meta.url)) 27 | } 28 | } 29 | }) 30 | -------------------------------------------------------------------------------- /.github/workflows/semantic-release.yml: -------------------------------------------------------------------------------- 1 | name: Semantic Release 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | permissions: 7 | contents: write 8 | issues: write 9 | pull-requests: write 10 | 11 | jobs: 12 | release: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | token: ${{ secrets.SEMANTIC_RELEASE_TOKEN }} 20 | 21 | - name: Semantic Release 22 | uses: cycjimmy/semantic-release-action@v6 23 | with: 24 | extra_plugins: | 25 | conventional-changelog-conventionalcommits 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.SEMANTIC_RELEASE_TOKEN }} 28 | -------------------------------------------------------------------------------- /api/kubeseal_webgui_api/routers/config.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import fastapi 4 | 5 | from kubeseal_webgui_api.app_config import settings 6 | from kubeseal_webgui_api.routers.models import WebGuiConfig 7 | 8 | LOGGER = logging.getLogger("kubeseal-webgui") 9 | router = fastapi.APIRouter() 10 | 11 | 12 | @router.get("/config", response_model=WebGuiConfig) 13 | def get_configs() -> WebGuiConfig: 14 | if settings.mock_enabled: 15 | return WebGuiConfig(kubeseal_version="0.1.0") 16 | try: 17 | return WebGuiConfig(kubeseal_version=settings.kubeseal_version) 18 | except RuntimeError: 19 | raise fastapi.HTTPException( 20 | status_code=500, detail="Failed to retrieve application configs." 21 | ) 22 | -------------------------------------------------------------------------------- /chart/kubeseal-webgui/templates/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: {{ include "kubeseal-webgui.fullname" . }} 5 | labels: 6 | {{- include "kubeseal-webgui.labels" . | nindent 4 }} 7 | data: 8 | {{- ( tpl (.Files.Glob "files/*").AsConfig . ) | nindent 2 }} 9 | {{- if not .Values.sealedSecrets.autoFetchCert }} 10 | kubeseal-cert.pem: 11 | {{- toYaml .Values.sealedSecrets.cert | nindent 4 }} 12 | {{- end }} 13 | config.json: |- 14 | { 15 | "api_url": {{ .Values.api.url | quote }}, 16 | "display_name": {{ .Values.displayName | quote }}, 17 | "kubeseal_webgui_ui_version": {{ .Values.ui.image.tag | quote}}, 18 | "kubeseal_webgui_api_version": {{ .Values.api.image.tag | quote}} 19 | } 20 | 21 | -------------------------------------------------------------------------------- /ui/src/components/AppLogo.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | 23 | -------------------------------------------------------------------------------- /ui/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js'; 2 | import pluginVue from 'eslint-plugin-vue'; 3 | import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'; 4 | 5 | export default [ 6 | { 7 | name: 'app/files-to-lint', 8 | files: ['**/*.{js,mjs,jsx,vue}'], 9 | }, 10 | 11 | { 12 | name: 'app/files-to-ignore', 13 | ignores: ['**/dist/**', '**/dist-ssr/**', '**/coverage/**'], 14 | }, 15 | 16 | js.configs.recommended, 17 | ...pluginVue.configs['flat/recommended'], 18 | 19 | { 20 | name: 'app/custom-rules', 21 | rules: { 22 | // Disable for Vue 3 - v-model with arguments is the correct syntax in Vue 3 23 | // This allows using v-model:propName="value" patterns 24 | 'vue/no-v-model-argument': 'off', 25 | }, 26 | }, 27 | 28 | skipFormatting, 29 | ]; 30 | -------------------------------------------------------------------------------- /api/kubeseal_webgui_api/routers/kubernetes_namespace_resolver.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from kubernetes import client, config 4 | 5 | LOGGER = logging.getLogger("kubeseal-webgui") 6 | 7 | 8 | def kubernetes_namespaces_resolver() -> list[str]: 9 | """Retrieve a list of namespaces from current kubernetes cluster.""" 10 | config.load_incluster_config() 11 | namespaces_list = [] 12 | 13 | LOGGER.info("Resolving in-cluster Namespaces") 14 | v1 = client.CoreV1Api() 15 | namespaces = v1.list_namespace() 16 | if isinstance(namespaces, client.V1NamespaceList) and namespaces.items: 17 | for ns in namespaces.items: 18 | namespaces_list.append(ns.metadata.name) 19 | else: 20 | LOGGER.warning("No valid namespace list available via %s", namespaces) 21 | 22 | LOGGER.debug("Namespaces list %s", namespaces_list) 23 | return namespaces_list 24 | -------------------------------------------------------------------------------- /ui/nginx-default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 8080; 3 | server_name localhost; 4 | 5 | #access_log /var/log/nginx/host.access.log main; 6 | 7 | location / { 8 | root /usr/share/nginx/html; 9 | index index.html index.htm; 10 | } 11 | 12 | #error_page 404 /404.html; 13 | 14 | # redirect server error pages to the static page /50x.html 15 | # 16 | error_page 500 502 503 504 /50x.html; 17 | location = /50x.html { 18 | root /usr/share/nginx/html; 19 | } 20 | 21 | location ~ /(secrets|namespaces|config|docs|openapi.json)(/.*)?$ { 22 | proxy_pass http://localhost:5000; 23 | proxy_http_version 1.1; 24 | proxy_set_header Upgrade $http_upgrade; 25 | proxy_set_header Connection 'upgrade'; 26 | proxy_set_header Host $host; 27 | proxy_cache_bypass $http_upgrade; 28 | 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /api/config/logging_config.yaml: -------------------------------------------------------------------------------- 1 | version: 1 2 | disable_existing_loggers: False 3 | formatters: 4 | default: 5 | "()": uvicorn.logging.DefaultFormatter 6 | format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s" 7 | access: 8 | "()": uvicorn.logging.AccessFormatter 9 | format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s" 10 | handlers: 11 | default: 12 | formatter: default 13 | class: logging.StreamHandler 14 | stream: ext://sys.stderr 15 | access: 16 | formatter: access 17 | class: logging.StreamHandler 18 | stream: ext://sys.stdout 19 | loggers: 20 | uvicorn.error: 21 | level: INFO 22 | handlers: 23 | - default 24 | propagate: no 25 | uvicorn.access: 26 | level: INFO 27 | handlers: 28 | - access 29 | propagate: no 30 | kubeseal-webgui: 31 | level: INFO 32 | handlers: 33 | - default 34 | propagate: no 35 | -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kubeseal-webgui", 3 | "version": "4.5.4", 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "vite build", 7 | "preview": "vite preview", 8 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore", 9 | "format": "prettier --write src/" 10 | }, 11 | "dependencies": { 12 | "@fontsource/roboto": "*", 13 | "core-js": "^3.45.1", 14 | "js-base64": "^3.7.8", 15 | "vue": "^3.5.22", 16 | "vuetify": "^3.10.5" 17 | }, 18 | "devDependencies": { 19 | "@eslint/js": "^9.39.1", 20 | "@mdi/font": "^7.4.47", 21 | "@rushstack/eslint-patch": "^1.13.0", 22 | "@vitejs/plugin-vue": "^5.2.4", 23 | "@vue/eslint-config-prettier": "^10.2.0", 24 | "eslint": "^9.37.0", 25 | "eslint-plugin-vue": "^9.33.0", 26 | "prettier": "^3.6.2", 27 | "vite": "^6.3.6", 28 | "vite-plugin-vuetify": "^2.1.2" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /chart/kubeseal-webgui/templates/route-ui.yaml: -------------------------------------------------------------------------------- 1 | 2 | {{- if .Values.route.enabled -}} 3 | apiVersion: route.openshift.io/v1 4 | kind: Route 5 | metadata: 6 | name: {{ include "kubeseal-webgui.fullname" . }} 7 | labels: 8 | {{- include "kubeseal-webgui.labels" . | nindent 4 }} 9 | {{- with .Values.route.annotations }} 10 | annotations: 11 | {{ toYaml . | indent 4 }} 12 | {{- end }} 13 | spec: 14 | {{- if .Values.route.hostname }} 15 | host: {{ .Values.route.hostname | quote }} 16 | {{- end }} 17 | to: 18 | kind: Service 19 | name: {{ include "kubeseal-webgui.fullname" . }} 20 | weight: 100 21 | port: 22 | targetPort: ui 23 | {{- if .Values.route.tls.enabled }} 24 | tls: 25 | termination: {{ .Values.route.tls.termination }} 26 | insecureEdgeTerminationPolicy: {{ .Values.route.tls.insecureEdgeTerminationPolicy }} 27 | {{- end }} 28 | wildcardPolicy: None 29 | {{- end }} 30 | -------------------------------------------------------------------------------- /api/kubeseal_webgui_api/routers/kubernetes.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import List 3 | 4 | import fastapi 5 | 6 | from kubeseal_webgui_api.app_config import settings 7 | from kubeseal_webgui_api.routers.kubernetes_namespace_resolver import ( 8 | kubernetes_namespaces_resolver, 9 | ) 10 | from kubeseal_webgui_api.routers.mock_namespace_resolver import mock_namespaces_resolver 11 | 12 | router = fastapi.APIRouter() 13 | LOGGER = logging.getLogger("kubeseal-webgui") 14 | 15 | if settings.mock_enabled: 16 | namespace_resolver = mock_namespaces_resolver 17 | else: 18 | namespace_resolver = kubernetes_namespaces_resolver 19 | 20 | 21 | @router.get("/namespaces", response_model=List[str]) 22 | def get_namespaces() -> List[str]: 23 | try: 24 | return namespace_resolver() 25 | except RuntimeError: 26 | raise fastapi.HTTPException( 27 | status_code=500, detail="Can't get namespaces from cluster." 28 | ) 29 | -------------------------------------------------------------------------------- /chart/kubeseal-webgui/files/logging_config.yaml: -------------------------------------------------------------------------------- 1 | version: 1 2 | disable_existing_loggers: False 3 | formatters: 4 | default: 5 | "()": uvicorn.logging.DefaultFormatter 6 | format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s" 7 | access: 8 | "()": uvicorn.logging.AccessFormatter 9 | format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s" 10 | handlers: 11 | default: 12 | formatter: default 13 | class: logging.StreamHandler 14 | stream: ext://sys.stderr 15 | access: 16 | formatter: access 17 | class: logging.StreamHandler 18 | stream: ext://sys.stdout 19 | loggers: 20 | uvicorn.error: 21 | level: {{ .Values.api.loglevel }} 22 | handlers: 23 | - default 24 | propagate: no 25 | uvicorn.access: 26 | level: {{ .Values.api.loglevel }} 27 | handlers: 28 | - access 29 | propagate: no 30 | kubeseal-webgui: 31 | level: {{ .Values.api.loglevel }} 32 | handlers: 33 | - default 34 | propagate: no 35 | -------------------------------------------------------------------------------- /api/tests/test_kubernetes_service.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from fastapi.testclient import TestClient 4 | 5 | from kubeseal_webgui_api.app import app 6 | from kubeseal_webgui_api.routers import kubernetes 7 | 8 | 9 | def dummy_namespace_resolver() -> List[str]: 10 | return ["default", "namespace-one", "namespace-two"] 11 | 12 | 13 | def broken_namespace_resolver() -> List[str]: 14 | raise RuntimeError("Go away") 15 | 16 | 17 | client = TestClient(app) 18 | 19 | 20 | def test_get_namespaces_with_exception(): 21 | kubernetes.namespace_resolver = broken_namespace_resolver 22 | response = client.get("/namespaces") 23 | assert response.status_code == 500 24 | assert response.json() == {"detail": "Can't get namespaces from cluster."} 25 | 26 | 27 | def test_get_namespaces(): 28 | kubernetes.namespace_resolver = dummy_namespace_resolver 29 | response = client.get("/namespaces") 30 | assert response.status_code == 200 31 | assert response.json() == dummy_namespace_resolver() 32 | -------------------------------------------------------------------------------- /ui/hooks/50-envsubst-default-conf.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -eu 2 | 3 | : "${PUBLIC_HOST:=localhost} 4 | : "${PUBLIC_PORT:=8080} 5 | : "${PUBLIC_SCHEME:=http} 6 | : "${API_HOST:=localhost} 7 | : "${API_PORT:=5000} 8 | : "${API_SCHEME:=http} 9 | 10 | sed_file() { 11 | local target 12 | local parent 13 | local buffer 14 | 15 | target="$1" 16 | parent=$(dirname "$1") 17 | shift 18 | 19 | if ! test -w "$target"; then 20 | echo "File (${target}) is not writable; skipping var expansion" 1>&2 21 | elif ! test -w "$parent"; then 22 | buffer=$(mktemp) 23 | 24 | sed "$@" "$target" > "$buffer" 25 | cat "$buffer" > "$target" 26 | rm "$buffer" 27 | else 28 | # inplace edit needs write permissions to the directory as well 29 | sed -i "$@" "$target" 30 | fi 31 | } 32 | 33 | sed_file /etc/nginx/conf.d/default.conf \ 34 | -e "s;http://localhost:5000;${API_SCHEME}://${API_HOST}:${API_PORT};" 35 | 36 | sed_file /usr/share/nginx/html/config.json \ 37 | -e "s;http://localhost:5000;${PUBLIC_SCHEME}://${PUBLIC_HOST}:${PUBLIC_PORT};" 38 | -------------------------------------------------------------------------------- /.github/workflows/frontend-tests.yml: -------------------------------------------------------------------------------- 1 | name: Run Python Playwright Tests 2 | on: 3 | pull_request: 4 | types: 5 | - opened 6 | - reopened 7 | - synchronize 8 | push: 9 | branches: 10 | - main 11 | - master 12 | 13 | jobs: 14 | test: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v4 20 | 21 | - name: Set up Python 22 | uses: actions/setup-python@v5 23 | with: 24 | python-version: '3.12' 25 | 26 | - name: Install dependencies 27 | run: | 28 | cd api 29 | pip install . 30 | 31 | - name: Install Playwright Browsers 32 | run: | 33 | cd api 34 | playwright install 35 | 36 | - name: Start Vue.js Development Server 37 | run: | 38 | cd ui 39 | npm install 40 | nohup npm run dev & 41 | sleep 5 42 | 43 | - name: Run Playwright Tests 44 | run: | 45 | cd ui 46 | pytest tests/ 47 | -------------------------------------------------------------------------------- /ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 14 | 18 | 22 | 26 | 27 | Kubeseal WebGui - Sealed Secrets Management 28 | 29 | 30 | 31 | 32 | 39 |
40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /dev/bump_version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Check if a version argument is provided 4 | if [ $# -ne 1 ]; then 5 | echo "Usage: $0 " 6 | exit 1 7 | fi 8 | 9 | VERSION=$1 10 | 11 | # Update the version in pyproject.toml 12 | if [ -f api/pyproject.toml ]; then 13 | sed -i "s/^version = \".*\"/version = \"$VERSION\"/" api/pyproject.toml 14 | echo "Updated api/pyproject.toml to version $VERSION" 15 | else 16 | echo "api/pyproject.toml not found!" 17 | fi 18 | 19 | # Update the version in package.json 20 | if [ -f ui/package.json ]; then 21 | sed -i "s/\"version\": \".*\"/\"version\": \"$VERSION\"/" ui/package.json 22 | echo "Updated ui/package.json to version $VERSION" 23 | else 24 | echo "ui/package.json not found!" 25 | fi 26 | 27 | # Update the version and appVersion in Chart.yaml 28 | if [ -f chart/kubeseal-webgui/Chart.yaml ]; then 29 | sed -i "s/^appVersion: .*/appVersion: $VERSION/" chart/kubeseal-webgui/Chart.yaml 30 | echo "Updated chart/kubeseal-webgui/Chart.yaml to version $VERSION" 31 | else 32 | echo "chart/kubeseal-webgui/Chart.yaml not found!" 33 | fi -------------------------------------------------------------------------------- /chart/kubeseal-webgui/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | apiVersion: networking.k8s.io/v1 3 | kind: Ingress 4 | metadata: 5 | name: {{ include "kubeseal-webgui.fullname" . }} 6 | namespace: {{ .Release.Namespace }} 7 | labels: 8 | {{- include "kubeseal-webgui.labels" . | nindent 4 }} 9 | {{- if .Values.ingress.annotations }} 10 | annotations: 11 | {{- range $key, $value := .Values.ingress.annotations }} 12 | {{ $key }}: {{ tpl $value $ | quote }} 13 | {{- end }} 14 | {{- end }} 15 | spec: 16 | {{- if .Values.ingress.ingressClassName }} 17 | ingressClassName: {{ .Values.ingress.ingressClassName }} 18 | {{- end }} 19 | {{- if .Values.ingress.tls.enabled }} 20 | tls: 21 | - hosts: 22 | - {{ .Values.ingress.hostname }} 23 | secretName: {{ .Values.ingress.tls.secretName }} 24 | {{- end }} 25 | rules: 26 | - host: {{ .Values.ingress.hostname }} 27 | http: 28 | paths: 29 | - backend: 30 | service: 31 | name: {{ include "kubeseal-webgui.fullname" . }} 32 | port: 33 | number: 8080 34 | path: / 35 | pathType: Prefix 36 | {{- end }} -------------------------------------------------------------------------------- /ui/src/App.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /api/kubeseal_webgui_api/app.py: -------------------------------------------------------------------------------- 1 | from contextlib import asynccontextmanager 2 | import logging 3 | 4 | import fastapi 5 | from fastapi.middleware.cors import CORSMiddleware 6 | 7 | from .app_config import fetch_sealed_secrets_cert 8 | from .routers import config, kubernetes, kubeseal 9 | 10 | LOGGER = logging.getLogger("kubeseal-webgui") 11 | 12 | 13 | @asynccontextmanager 14 | async def lifespan(fastapi_app: fastapi.FastAPI): # noqa: ANN201 skipcq: PYL-W0613 15 | LOGGER.info("Running startup tasks...") 16 | fetch_sealed_secrets_cert() 17 | LOGGER.info("Startup tasks complete.") 18 | yield 19 | 20 | 21 | app = fastapi.FastAPI(lifespan=lifespan) 22 | 23 | origins = [ 24 | "http://localhost:8080", 25 | ] 26 | 27 | app.add_middleware( 28 | CORSMiddleware, 29 | allow_origins=origins, 30 | allow_credentials=True, 31 | allow_methods=["*"], 32 | allow_headers=["*"], 33 | ) 34 | 35 | app.include_router( 36 | kubernetes.router, 37 | ) 38 | app.include_router( 39 | config.router, 40 | ) 41 | app.include_router( 42 | kubeseal.router, 43 | ) 44 | 45 | 46 | @app.get("/") 47 | def root() -> dict[str, str]: 48 | return {"status": "Kubeseal-WebGui API"} 49 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "branches": ["master"], 3 | "tagFormat": "${version}", 4 | "plugins": [ 5 | ["@semantic-release/commit-analyzer", { 6 | "releaseRules": [ 7 | {"type": "chore", "release": "patch"}, 8 | {"type": "deps", "release": "patch"} 9 | ] 10 | }], 11 | ["@semantic-release/release-notes-generator", { 12 | "preset": "conventionalcommits", 13 | "presetConfig": { 14 | "types": [ 15 | {"type": "feat", "section": "✨ Features"}, 16 | {"type": "fix", "section": "🐛 Bug Fixes"}, 17 | {"type": "perf", "section": "🚀 Performance Improvements"}, 18 | {"type": "revert", "section": "mb-rewind: Reverts"}, 19 | {"type": "docs", "section": "📚 Documentation"}, 20 | {"type": "style", "section": "💎 Styles"}, 21 | {"type": "chore", "section": "📦 Chores"}, 22 | {"type": "refactor", "section": "♻️ Code Refactoring"}, 23 | {"type": "test", "section": "✅ Tests"}, 24 | {"type": "build", "section": "👷 Build System"}, 25 | {"type": "ci", "section": "🔧 CI/CD"} 26 | ] 27 | } 28 | }], 29 | "@semantic-release/github" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /ui/src/utils/mockData.js: -------------------------------------------------------------------------------- 1 | const adjectives = [ 2 | "altered", "angry", "big", "blinking", "boring", "broken", "bubbling", "calculating", 3 | "cute", "diffing", "expensive", "fresh", "fierce", "floating", "generous", "golden", 4 | "green", "growing", "hidden", "hideous", "interesting", "kubed", "mumbling", "rusty", 5 | "singing", "small", "sniffing", "squared", "talking", "trusty", "wise", "walking", "zooming" 6 | ]; 7 | 8 | const nouns = [ 9 | "ant", "bike", "bird", "captain", "cheese", "clock", "digit", "gorilla", "kraken", "number", 10 | "maven", "monitor", "moose", "moon", "mouse", "news", "newt", "octopus", "opossum", "otter", 11 | "paper", "passenger", "potato", "ship", "spaceship", "spaghetti", "spoon", "store", "tomcat", 12 | "trombone", "unicorn", "vine", "whale" 13 | ]; 14 | 15 | export function mockNamespacesResolver(count) { 16 | const randomPairs = new Set(); 17 | while (randomPairs.size < count) { 18 | const adjective = adjectives[Math.floor(Math.random() * adjectives.length)]; 19 | const noun = nouns[Math.floor(Math.random() * nouns.length)]; 20 | randomPairs.add(`${adjective}-${noun}`); 21 | } 22 | return Array.from(randomPairs).sort(); 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | version: 6 | description: 'Contains application version' 7 | required: true 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Login to DockerHub 15 | uses: docker/login-action@v3 16 | with: 17 | username: ${{ secrets.DOCKERHUB_USERNAME }} 18 | password: ${{ secrets.DOCKERHUB_TOKEN }} 19 | - name: Set up Docker Buildx 20 | uses: docker/setup-buildx-action@v3 21 | - name: Build and push API 22 | id: docker_build_api 23 | uses: docker/build-push-action@v6 24 | with: 25 | push: true 26 | file: Dockerfile.api 27 | tags: | 28 | kubesealwebgui/api:latest 29 | kubesealwebgui/api:${{ github.event.inputs.version }} 30 | - name: Build and push UI 31 | id: docker_build_ui 32 | uses: docker/build-push-action@v6 33 | with: 34 | push: true 35 | file: Dockerfile.ui 36 | tags: | 37 | kubesealwebgui/ui:latest 38 | kubesealwebgui/ui:${{ github.event.inputs.version }} 39 | -------------------------------------------------------------------------------- /ui/src/composables/useConfig.js: -------------------------------------------------------------------------------- 1 | export function useConfig() { 2 | async function fetchConfig() { 3 | const response = await fetch("/config.json"); 4 | if (!response.ok) { 5 | throw Error(`Failed to fetch config.json: ${response.statusText}`); 6 | } 7 | return await response.json(); 8 | } 9 | 10 | async function fetchAppConfig() { 11 | const response_config = await fetch("/config.json"); 12 | if (!response_config.ok) { 13 | throw Error(`Failed to fetch config.json: ${response_config.statusText}`); 14 | } 15 | const data_config = await response_config.clone().json(); 16 | 17 | const kubeseal_webgui_ui_version = data_config["kubeseal_webgui_ui_version"]; 18 | const kubeseal_webgui_api_version = data_config["kubeseal_webgui_api_version"]; 19 | const apiUrl = data_config["api_url"]; 20 | 21 | const response = await fetch(`${apiUrl}/config`); 22 | 23 | const configs = await response.json(); 24 | configs.uiVersion = kubeseal_webgui_ui_version; 25 | configs.apiVersion = kubeseal_webgui_api_version; 26 | 27 | return { 28 | configs, 29 | success: response.ok && response_config.ok 30 | }; 31 | } 32 | 33 | return { 34 | fetchConfig, 35 | fetchAppConfig 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /chart/kubeseal-webgui/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "kubeseal-webgui.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | If release name contains chart name it will be used as a full name. 13 | */}} 14 | {{- define "kubeseal-webgui.fullname" -}} 15 | {{- if .Values.fullnameOverride -}} 16 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 17 | {{- else -}} 18 | {{- $name := default .Chart.Name .Values.nameOverride -}} 19 | {{- if contains $name .Release.Name -}} 20 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 21 | {{- else -}} 22 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 23 | {{- end -}} 24 | {{- end -}} 25 | {{- end -}} 26 | 27 | {{/* 28 | Create chart name and version as used by the chart label. 29 | */}} 30 | {{- define "kubeseal-webgui.chart" -}} 31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 32 | {{- end -}} 33 | 34 | {{/* 35 | Common labels 36 | */}} 37 | {{- define "kubeseal-webgui.labels" -}} 38 | app: {{ template "kubeseal-webgui.name" . }} 39 | chart: {{ .Chart.Name }} 40 | release: {{ .Release.Name }} 41 | heritage: {{ .Release.Service }} 42 | {{- end -}} -------------------------------------------------------------------------------- /ui/src/components/SecretKeyCard.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 53 | -------------------------------------------------------------------------------- /api/kubeseal_webgui_api/routers/models.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import List, Optional, Self 3 | 4 | from pydantic import BaseModel, ConfigDict, model_validator 5 | 6 | 7 | def to_camel(value: str) -> str: 8 | head, *tail = value.split("_") 9 | return head + "".join(word.capitalize() for word in tail) 10 | 11 | 12 | class WebGuiConfig(BaseModel): 13 | kubeseal_version: str 14 | model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) 15 | 16 | 17 | class KeyValuePair(BaseModel): 18 | key: str 19 | value: str 20 | 21 | 22 | class Secret(BaseModel): 23 | key: str 24 | value: Optional[str] = None 25 | file: Optional[str] = None 26 | 27 | @model_validator(mode="after") 28 | def value_or_file_set(self: Self) -> Self: 29 | file, value = self.file, self.value 30 | if file and value: 31 | raise AssertionError("Only one field of 'value' or 'file' can be used") 32 | if file is None and value is None: 33 | raise AssertionError("One field of 'value' or 'file' has to be set") 34 | return self 35 | 36 | 37 | class Scope(str, Enum): 38 | STRICT = "strict" 39 | CLUSTER_WIDE = "cluster-wide" 40 | NAMESPACE_WIDE = "namespace-wide" 41 | 42 | def needs_name(self: Self) -> bool: 43 | return self not in (Scope.CLUSTER_WIDE, Scope.NAMESPACE_WIDE) 44 | 45 | def needs_namespace(self: Self) -> bool: 46 | return self is not Scope.CLUSTER_WIDE 47 | 48 | 49 | class Data(BaseModel): 50 | secret: Optional[str] = None 51 | namespace: Optional[str] = None 52 | secrets: List[Secret] 53 | scope: Optional[Scope] = None 54 | -------------------------------------------------------------------------------- /api/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "kubeseal-webgui-api" 3 | version = "4.5.4" 4 | description = "A python based backend for the kubeseal binary." 5 | authors = ["Jan Herber "] 6 | license = "Apache-2.0" 7 | readme = "README.md" 8 | packages = [{ include = "kubeseal_webgui_api" }] 9 | 10 | [tool.poetry.dependencies] 11 | python = "^3.12" 12 | Jinja2 = "^3.1.2" 13 | PyYAML = "^6.0" 14 | kubernetes = "^26.1.0" 15 | fastapi = "^0.109.1" 16 | JSON-log-formatter = "^0.5.1" 17 | uvicorn = "^0.20.0" 18 | pydantic = "^2.0.0" 19 | pydantic-settings = "^2.0.3" 20 | playwright = "^1.47.0" 21 | pytest-playwright = "^0.5.2" 22 | 23 | [tool.poetry.group.dev.dependencies] 24 | black = "^22.8.0" 25 | pytest = "^7.1.3" 26 | isort = "^5.10.1" 27 | pydocstyle = "^6.1.1" 28 | pytest-cov = "^4.0.0" 29 | mypy = "^0.991" 30 | pyupgrade = "^3.0.0" 31 | httpx = "^0.25.0" 32 | ruff = "^0.6.8" 33 | 34 | [build-system] 35 | requires = ["poetry-core"] 36 | build-backend = "poetry.core.masonry.api" 37 | 38 | [tool.isort] 39 | profile = "black" 40 | 41 | [tool.ruff] 42 | line-length = 88 43 | src = [".", "test"] 44 | target-version = "py312" 45 | 46 | [tool.ruff.lint] 47 | ignore = ["SIM108"] 48 | extend-select = [ 49 | "F", 50 | "C90", 51 | "N", 52 | "ANN", 53 | "S", 54 | "B", 55 | "A", 56 | "C4", 57 | "DTZ", 58 | "G", 59 | "PIE", 60 | "RET", 61 | "SIM", 62 | "PTH", 63 | "ERA", 64 | "FLY", 65 | "PERF", 66 | "FURB", 67 | ] 68 | 69 | [tool.ruff.lint.per-file-ignores] 70 | "tests/**.py" = ["ANN201", "S101", "ANN001"] 71 | 72 | [tool.pytest.ini_options] 73 | markers = [ 74 | "cluster: marks test that requires a k8s cluster in their environment", 75 | "container: marks test that requires the container environment", 76 | ] 77 | -------------------------------------------------------------------------------- /ui/src/components/DarkMode.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 46 | 47 | 65 | -------------------------------------------------------------------------------- /api/kubeseal_webgui_api/routers/mock_namespace_resolver.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import random 3 | from typing import List 4 | 5 | from kubeseal_webgui_api.app_config import settings 6 | 7 | adjectives = [ 8 | "altered", 9 | "angry", 10 | "big", 11 | "blinking", 12 | "boring", 13 | "broken", 14 | "bubbling", 15 | "calculating", 16 | "cute", 17 | "diffing", 18 | "expensive", 19 | "fresh", 20 | "fierce", 21 | "floating", 22 | "generous", 23 | "golden", 24 | "green", 25 | "growing", 26 | "hidden", 27 | "hideous", 28 | "interesting", 29 | "kubed", 30 | "mumbling", 31 | "rusty", 32 | "singing", 33 | "small", 34 | "sniffing", 35 | "squared", 36 | "talking", 37 | "trusty", 38 | "wise", 39 | "walking", 40 | "zooming", 41 | ] 42 | nouns = [ 43 | "ant", 44 | "bike", 45 | "bird", 46 | "captain", 47 | "cheese", 48 | "clock", 49 | "digit", 50 | "gorilla", 51 | "kraken", 52 | "number", 53 | "maven", 54 | "monitor", 55 | "moose", 56 | "moon", 57 | "mouse", 58 | "news", 59 | "newt", 60 | "octopus", 61 | "opossum", 62 | "otter", 63 | "paper", 64 | "passenger", 65 | "potato", 66 | "ship", 67 | "spaceship", 68 | "spaghetti", 69 | "spoon", 70 | "store", 71 | "tomcat", 72 | "trombone", 73 | "unicorn", 74 | "vine", 75 | "whale", 76 | ] 77 | 78 | 79 | def mock_namespaces_resolver() -> List[str]: 80 | count = settings.mock_namespace_count 81 | return sorted( 82 | { 83 | "-".join(words) 84 | for words in random.choices( # noqa: S311 no security needed here 85 | list(itertools.product(adjectives, nouns)), k=count 86 | ) 87 | } 88 | ) 89 | -------------------------------------------------------------------------------- /api/tests/test_kubeseal_service.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from os import environ 3 | 4 | import pytest 5 | from fastapi.testclient import TestClient 6 | 7 | from kubeseal_webgui_api.app import app 8 | 9 | LOGGER = logging.getLogger(__name__) 10 | environ["ORIGIN_URL"] = "http://no-server-here" 11 | 12 | client = TestClient(app) 13 | 14 | 15 | @pytest.mark.container() 16 | @pytest.mark.cluster() 17 | def test_post_secrets_object(): 18 | sealing_request_data = { 19 | "namespace": "default", 20 | "secret": "bar", 21 | "scope": "strict", 22 | "secrets": [{"key": "my", "value": "cHJlY2lvdXMK"}], 23 | } 24 | res = client.post("/secrets", json=sealing_request_data) 25 | assert res.status_code == 200 26 | sealed = res.json() 27 | for index, secret in enumerate(sealing_request_data["secrets"]): 28 | assert secret["key"] == sealed[index]["key"] 29 | assert len(sealed[index]["value"]) > 0 30 | 31 | 32 | @pytest.mark.container() 33 | @pytest.mark.cluster() 34 | def test_get_api(): 35 | # given running http server 36 | # when GET /secrets 37 | res = client.get("/secrets") 38 | # then return non empty result 39 | assert res.status_code == 405 40 | assert res.json() != "" 41 | 42 | 43 | @pytest.mark.container() 44 | @pytest.mark.cluster() 45 | def test_post_api(): 46 | # given running http server 47 | # when POST /secrets 48 | data = { 49 | "secret": "default", 50 | "namespace": "test-namespace", 51 | "scope": "strict", 52 | "secrets": [ 53 | {"key": "foo", "value": "YmFyCg=="}, 54 | {"key": "bar", "value": "Zm9vCg=="}, 55 | ], 56 | } 57 | 58 | res = client.post("/secrets", json=data) 59 | 60 | # then return non empty result 61 | assert res.status_code == 200 62 | assert res.json() != "" 63 | -------------------------------------------------------------------------------- /api/.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v2.3.0 4 | hooks: 5 | - id: check-yaml 6 | exclude: ^chart/kubeseal-webgui/templates/ 7 | files: ^api/ 8 | - id: end-of-file-fixer 9 | files: ^api/ 10 | - id: trailing-whitespace 11 | files: ^api/ 12 | # - repo: https://github.com/PyCQA/pydocstyle 13 | # rev: 6.1.1 14 | # hooks: 15 | # - id: pydocstyle 16 | # - repo: https://github.com/PyCQA/prospector 17 | #rev: 1.5.1 18 | #hooks: 19 | #- id: prospector 20 | #args: ["--zero-exit"] 21 | - repo: local 22 | hooks: 23 | # - id: mypy 24 | # name: mypy 25 | # entry: bash -c 'cd api && poetry run mypy kubeseal-webgui-api' 26 | # language: system 27 | - id: isort 28 | name: isort (python) 29 | args: ["--profile", "black"] 30 | entry: bash -c 'cd api && poetry run isort' 31 | language: system 32 | types: [python] 33 | files: ^api/ 34 | - id: pyupgrade 35 | name: pyupgrade 36 | entry: bash -c 'cd api && poetry run pyupgrade' 37 | language: system 38 | files: ^api/ 39 | - id: format 40 | name: format 41 | language: system 42 | entry: bash -c 'cd api && poetry run ruff format' 43 | types: [python] 44 | files: ^api/ 45 | - id: ruff 46 | name: ruff 47 | entry: bash -c 'cd api && poetry run ruff check' 48 | language: system 49 | types: [python] 50 | files: ^api/ 51 | - id: tests 52 | name: run test suite 53 | entry: bash -c 'cd api && poetry install && poetry run pytest -m "not (container or cluster)"' 54 | language: system 55 | types: [python] 56 | pass_filenames: false 57 | files: ^api/ 58 | # stages: [push] 59 | -------------------------------------------------------------------------------- /ui/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import { createVuetify } from 'vuetify' 3 | import App from './App.vue' 4 | import * as components from 'vuetify/components' 5 | import * as directives from 'vuetify/directives' 6 | import { aliases, mdi } from 'vuetify/iconsets/mdi' 7 | 8 | import "@fontsource/roboto" 9 | import 'vuetify/styles' // Global CSS has to be imported 10 | import '@mdi/font/css/materialdesignicons.css' // Ensure you are using css-loader 11 | import './assets/styles.css' // Import custom global styles 12 | 13 | const app = createApp(App) 14 | const vuetify = createVuetify({ 15 | components, 16 | directives, 17 | icons: { 18 | defaultSet: 'mdi', 19 | aliases, 20 | sets: { 21 | mdi, 22 | } 23 | }, 24 | theme: { 25 | defaultTheme: 'light', 26 | themes: { 27 | light: { 28 | dark: false, 29 | colors: { 30 | primary: '#007bff', 31 | secondary: '#fd7e14', 32 | accent: '#17a2b8', 33 | error: '#fd7e14', 34 | info: '#0dcaf0', 35 | success: '#28a745', 36 | warning: '#ffc107', 37 | background: '#f8f9fa', 38 | surface: '#ffffff', 39 | 'on-surface': '#212529', 40 | 'surface-variant': '#e9ecef', 41 | 'on-surface-variant': '#495057', 42 | } 43 | }, 44 | dark: { 45 | dark: true, 46 | colors: { 47 | primary: '#3d94f6', 48 | secondary: '#fd8c3c', 49 | accent: '#20c997', 50 | error: '#fd8c3c', 51 | info: '#0dcaf0', 52 | success: '#28a745', 53 | warning: '#ffc107', 54 | background: '#212529', 55 | surface: '#343a40', 56 | 'on-surface': '#f8f9fa', 57 | 'surface-variant': '#495057', 58 | 'on-surface-variant': '#adb5bd', 59 | } 60 | } 61 | } 62 | } 63 | }) 64 | 65 | app 66 | .use(vuetify) 67 | .mount('#app') 68 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v2.3.0 4 | hooks: 5 | - id: check-yaml 6 | exclude: ^chart/kubeseal-webgui/templates/ 7 | - id: end-of-file-fixer 8 | - id: trailing-whitespace 9 | # - repo: https://github.com/PyCQA/pydocstyle 10 | # rev: 6.1.1 11 | # hooks: 12 | # - id: pydocstyle 13 | # - repo: https://github.com/PyCQA/prospector 14 | #rev: 1.5.1 15 | #hooks: 16 | #- id: prospector 17 | #args: ["--zero-exit"] 18 | - repo: local 19 | hooks: 20 | # - id: mypy 21 | # name: mypy 22 | # entry: bash -c 'cd api && poetry run mypy kubeseal-webgui-api' 23 | # language: system 24 | - id: isort 25 | name: isort (python) 26 | args: ["--profile", "black"] 27 | entry: bash -c 'cd api && poetry run isort' 28 | language: system 29 | types: [python] 30 | files: ^api/ 31 | - id: pyupgrade 32 | name: pyupgrade 33 | entry: bash -c 'cd api && poetry run pyupgrade' 34 | language: system 35 | files: ^api/ 36 | - id: black 37 | name: black 38 | language: system 39 | entry: bash -c 'cd api && poetry run black .' 40 | types: [python] 41 | files: ^api/ 42 | - id: flake8 43 | name: flake8 44 | entry: bash -c 'cd api && poetry run flake8 --max-line-length=88 kubeseal_webgui_api' 45 | language: system 46 | types: [python] 47 | files: ^api/ 48 | - id: tests 49 | name: run test suite 50 | entry: bash -c 'cd api && poetry install && poetry run pytest -m "not (container or cluster)"' 51 | language: system 52 | types: [python] 53 | pass_filenames: false 54 | files: ^api/ 55 | # stages: [push] 56 | - repo: https://github.com/pre-commit/mirrors-eslint 57 | rev: "v8.25.0" # Use the sha / tag you want to point at 58 | hooks: 59 | - id: eslint 60 | files: \.[jt]sx?$ # *.js, *.jsx, *.ts and *.tsx 61 | types: [file] 62 | -------------------------------------------------------------------------------- /Dockerfile.api: -------------------------------------------------------------------------------- 1 | FROM debian:bookworm-slim AS deps 2 | 3 | ARG KUBESEAL_VERSION=0.33.1 4 | ARG TARGETARCH 5 | ENV KUBESEAL_BINARY=/deps/kubeseal \ 6 | PRIVATE_KEY=/dev/null \ 7 | PUBLIC_KEY=/deps/cert.pem 8 | 9 | WORKDIR /deps 10 | 11 | RUN apt-get update && \ 12 | apt-get upgrade -y && \ 13 | apt-get install -y --no-install-recommends ca-certificates openssl curl && \ 14 | openssl req -x509 -days 365 -nodes -newkey rsa:4096 -keyout "$PRIVATE_KEY" -out "$PUBLIC_KEY" -subj "/CN=sealed-secret/O=sealed-secret" && \ 15 | curl -Lsf -o /tmp/kubeseal.tar.gz "https://github.com/bitnami-labs/sealed-secrets/releases/download/v${KUBESEAL_VERSION}/kubeseal-${KUBESEAL_VERSION}-linux-${TARGETARCH}.tar.gz" && \ 16 | tar -xzf /tmp/kubeseal.tar.gz && \ 17 | chmod 0755 "${KUBESEAL_BINARY}" && \ 18 | rm /tmp/kubeseal.tar.gz && \ 19 | apt-get autoremove -y && \ 20 | apt-get clean && \ 21 | rm -rf /var/lib/apt/lists/* 22 | 23 | FROM python:3.12-slim 24 | 25 | USER root 26 | 27 | ARG APP_PATH="/kubeseal-webgui" 28 | ARG APP_PORT=5000 29 | ARG KUBESEAL_VERSION=${KUBESEAL_VERSION} 30 | 31 | RUN adduser --gid 0 --home "${APP_PATH}" --disabled-password app \ 32 | && chmod 0750 "${APP_PATH}" 33 | 34 | RUN apt-get update && \ 35 | apt-get upgrade -y && \ 36 | apt-get clean && \ 37 | rm -rf /var/lib/apt/lists/* 38 | 39 | 40 | USER app 41 | 42 | ENV UVICORN_PORT=${APP_PORT} \ 43 | UVICORN_HOST=0.0.0.0 \ 44 | UVICORN_NO_DATE_HEADER=1 \ 45 | UVICORN_NO_SERVER_HEADER=1 \ 46 | KUBESEAL_BINARY=/tmp/kubeseal 47 | 48 | EXPOSE ${APP_PORT} 49 | 50 | WORKDIR ${APP_PATH} 51 | 52 | COPY api src/ 53 | 54 | RUN python3 -m venv "${APP_PATH}" && \ 55 | . "${APP_PATH}/bin/activate" && \ 56 | pip install --no-cache-dir --upgrade pip && \ 57 | pip install --no-cache-dir uvicorn wheel setuptools && \ 58 | pip install --no-cache-dir src/ && \ 59 | install --mode=755 --group=0 ./src/bin/* "${APP_PATH}/bin/" 60 | 61 | ENV PATH="${PATH}:${APP_PATH}/bin:${APP_PATH}/.local/bin" 62 | 63 | COPY --from=deps /deps/* /tmp/ 64 | 65 | CMD [ "uvicorn", "--log-config", "src/config/logging_config.yaml", "kubeseal_webgui_api.app:app"] 66 | -------------------------------------------------------------------------------- /.github/workflows/ghcr-build.yml: -------------------------------------------------------------------------------- 1 | name: Create and publish Docker images on ghcr.io 2 | 3 | on: 4 | push: 5 | branches: ['master'] 6 | tags: ['*'] 7 | pull_request: 8 | branches: ['master'] 9 | types: 10 | - opened 11 | - reopened 12 | - synchronize 13 | schedule: 14 | - cron: '42 6 * * SUN' 15 | 16 | env: 17 | REGISTRY: ghcr.io 18 | IMAGE_NAME: ${{ github.repository }} 19 | 20 | jobs: 21 | build-and-push-image: 22 | runs-on: ubuntu-latest 23 | if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} 24 | strategy: 25 | fail-fast: false 26 | matrix: 27 | include: 28 | - context: . 29 | dockerfile: Dockerfile.api 30 | image: ghcr.io/${{ github.repository }}/api 31 | - context: . 32 | dockerfile: Dockerfile.ui 33 | image: ghcr.io/${{ github.repository }}/ui 34 | permissions: 35 | contents: read 36 | packages: write 37 | 38 | steps: 39 | - name: Checkout repository 40 | uses: actions/checkout@v4 41 | 42 | - name: Set up QEMU for cross-platform builds 43 | uses: docker/setup-qemu-action@v3 44 | with: 45 | platforms: all 46 | 47 | - name: Set up Docker Buildx 48 | uses: docker/setup-buildx-action@v3 49 | 50 | - name: Log in to the Container registry 51 | uses: docker/login-action@v3 52 | with: 53 | registry: ${{ env.REGISTRY }} 54 | username: ${{ github.actor }} 55 | password: ${{ secrets.GITHUB_TOKEN }} 56 | 57 | - name: Extract metadata (tags, labels) for Docker 58 | id: meta 59 | uses: docker/metadata-action@v5 60 | with: 61 | images: ${{ matrix.image }} 62 | 63 | - name: Build and push Docker image 64 | uses: docker/build-push-action@v6 65 | with: 66 | context: ${{ matrix.context }} 67 | file: ${{ matrix.dockerfile }} 68 | push: true 69 | platforms: linux/amd64,linux/arm64 70 | tags: ${{ steps.meta.outputs.tags }} 71 | labels: ${{ steps.meta.outputs.labels }} 72 | -------------------------------------------------------------------------------- /ui/src/components/SealedSecretCard.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 81 | -------------------------------------------------------------------------------- /ui/src/composables/useSecrets.js: -------------------------------------------------------------------------------- 1 | import { Base64 } from "js-base64"; 2 | import { mockNamespacesResolver } from "@/utils/mockData"; 3 | 4 | export function useSecrets() { 5 | async function fetchNamespaces(config) { 6 | if (import.meta.env.VITE_MOCK_NAMESPACES) { 7 | return mockNamespacesResolver(10); 8 | } else { 9 | const response = await fetch(`${config.api_url}/namespaces`); 10 | if (!response.ok) { 11 | throw new Error(`Failed to fetch namespaces: ${response.statusText}`); 12 | } 13 | return await response.json(); 14 | } 15 | } 16 | 17 | function readFileAsync(file) { 18 | return new Promise((resolve, reject) => { 19 | let reader = new FileReader(); 20 | reader.onload = () => { 21 | resolve(reader.result); 22 | }; 23 | reader.onerror = reject; 24 | reader.readAsDataURL(file); 25 | }); 26 | } 27 | 28 | async function fetchEncodedSecrets(config, { secretName, namespaceName, scope, secrets }) { 29 | const requestObject = { 30 | secret: secretName, 31 | namespace: namespaceName, 32 | scope: scope, 33 | secrets: await Promise.all( 34 | secrets.map(async (element) => { 35 | if (element.value) { 36 | return { 37 | key: element.key, 38 | value: Base64.encode(element.value), 39 | }; 40 | } else { 41 | let fileContent = await readFileAsync(element.file); 42 | // we get a dataurl, so split the header from the data and use data, only 43 | fileContent = fileContent.split(",")[1]; 44 | return { 45 | key: element.key, 46 | file: fileContent, 47 | }; 48 | } 49 | }) 50 | ), 51 | }; 52 | 53 | const requestBody = JSON.stringify(requestObject, null, "\t"); 54 | 55 | const response = await fetch(`${config.api_url}/secrets`, { 56 | method: "POST", 57 | headers: { 58 | "Content-Type": "application/json", 59 | }, 60 | body: requestBody, 61 | }); 62 | 63 | if (!response.ok) { 64 | throw Error( 65 | "No sealed secrets in response from backend: " + 66 | (await response.text()) 67 | ); 68 | } else { 69 | return await response.json(); 70 | } 71 | } 72 | 73 | return { 74 | fetchNamespaces, 75 | fetchEncodedSecrets 76 | }; 77 | } 78 | -------------------------------------------------------------------------------- /ui/src/components/SecretInput.vue: -------------------------------------------------------------------------------- 1 | 67 | 68 | 94 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '38 15 * * 0' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'javascript', 'python' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v4 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v4 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v4 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v4 68 | -------------------------------------------------------------------------------- /ui/src/components/AppConfig.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 61 | 62 | 104 | -------------------------------------------------------------------------------- /ui/src/components/SecretsList.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 92 | -------------------------------------------------------------------------------- /api/kubeseal_webgui_api/app_config.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import subprocess 3 | from os import environ 4 | 5 | from pydantic_settings import BaseSettings 6 | 7 | binary = environ.get("KUBESEAL_BINARY", "/bin/false") 8 | mock = environ.get("MOCK_ENABLED", "False").lower() == "true" 9 | autofetch = environ.get("KUBESEAL_AUTOFETCH", "false") 10 | kubeseal_cert = environ.get("KUBESEAL_CERT", "/kubeseal-webgui/cert/kubeseal-cert.pem") 11 | 12 | 13 | class AppSettings(BaseSettings): 14 | kubeseal_version: str 15 | kubeseal_binary: str = binary 16 | kubeseal_cert: str = environ.get("KUBESEAL_CERT", "/dev/null") 17 | mock_enabled: bool = mock 18 | mock_namespace_count: int = 120 19 | 20 | 21 | LOGGER = logging.getLogger("kubeseal-webgui") 22 | 23 | 24 | def fetch_sealed_secrets_cert() -> None: 25 | if mock or autofetch == "false": 26 | return 27 | 28 | sealed_secrets_namespace = environ.get( 29 | "KUBESEAL_CONTROLLER_NAMESPACE", "sealed-secrets" 30 | ) 31 | sealed_secrets_controller_name = environ.get( 32 | "KUBESEAL_CONTROLLER_NAME", "sealed-secrets-controller" 33 | ) 34 | 35 | LOGGER.info( 36 | "Fetch certificate from sealed secrets controller '%s' in namespace '%s'", 37 | sealed_secrets_controller_name, 38 | sealed_secrets_namespace, 39 | ) 40 | kubeseal_subprocess = subprocess.Popen( 41 | [ 42 | binary, 43 | "--fetch-cert", 44 | "--controller-name", 45 | sealed_secrets_controller_name, 46 | "--controller-namespace", 47 | sealed_secrets_namespace, 48 | ], 49 | stdout=subprocess.PIPE, 50 | stderr=subprocess.PIPE, 51 | encoding="utf-8", 52 | ) 53 | output, error = kubeseal_subprocess.communicate() 54 | if error: 55 | error_message = f"Error in run_kubeseal: {error}" 56 | LOGGER.error(error_message) 57 | raise RuntimeError(error_message) 58 | with open(kubeseal_cert, "w") as file: 59 | LOGGER.info("Saving certificate in '%s'", kubeseal_cert) 60 | file.write(output) 61 | 62 | 63 | def get_kubeseal_version() -> str: 64 | """Retrieve the kubeseal binary version.""" 65 | LOGGER.debug("Retrieving kubeseal binary version.") 66 | kubeseal_subprocess = subprocess.Popen( 67 | [binary, "--version"], 68 | stdout=subprocess.PIPE, 69 | stderr=subprocess.PIPE, 70 | encoding="utf-8", 71 | ) 72 | output, error = kubeseal_subprocess.communicate() 73 | if error: 74 | error_message = f"Error in run_kubeseal: {error}" 75 | LOGGER.error(error_message) 76 | raise RuntimeError(error_message) 77 | 78 | version = "".join(output.split("\n")) 79 | 80 | return str(version).split(":")[1].replace('"', "").lstrip() 81 | 82 | 83 | settings = AppSettings( 84 | kubeseal_version=get_kubeseal_version(), 85 | ) 86 | -------------------------------------------------------------------------------- /ui/src/components/SecretsResults.vue: -------------------------------------------------------------------------------- 1 | 66 | 67 | 114 | -------------------------------------------------------------------------------- /.github/workflows/helm-release.yml: -------------------------------------------------------------------------------- 1 | name: Release Helm-Chart 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | permissions: 9 | contents: write 10 | packages: write 11 | 12 | jobs: 13 | release: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | token: ${{ secrets.SEMANTIC_RELEASE_TOKEN }} 21 | 22 | - name: Install Helm 23 | uses: azure/setup-helm@v4 24 | env: 25 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 26 | 27 | - name: Install yq 28 | run: | 29 | wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/local/bin/yq 30 | chmod +x /usr/local/bin/yq 31 | 32 | - name: Login to GitHub Container Registry 33 | uses: docker/login-action@v3 34 | with: 35 | registry: ghcr.io 36 | username: ${{ github.actor }} 37 | password: ${{ secrets.GITHUB_TOKEN }} 38 | 39 | - name: Package and Push Helm Chart 40 | run: | 41 | # Get the application version from the tag (remove refs/tags/ and optional v prefix) 42 | APP_VERSION=${GITHUB_REF#refs/tags/} 43 | APP_VERSION=${APP_VERSION#v} 44 | echo "Application version: $APP_VERSION" 45 | 46 | # Read current chart version from Chart.yaml 47 | CURRENT_CHART_VERSION=$(yq '.version' chart/kubeseal-webgui/Chart.yaml) 48 | echo "Current chart version: $CURRENT_CHART_VERSION" 49 | 50 | # Bump patch version of chart 51 | CHART_VERSION=$(echo "$CURRENT_CHART_VERSION" | awk -F. '{$NF = $NF + 1;} 1' | sed 's/ /./g') 52 | echo "New chart version: $CHART_VERSION" 53 | 54 | # Update Chart.yaml with new chart version and appVersion 55 | yq -i ".version = \"$CHART_VERSION\"" chart/kubeseal-webgui/Chart.yaml 56 | yq -i ".appVersion = \"$APP_VERSION\"" chart/kubeseal-webgui/Chart.yaml 57 | 58 | # Update values.yaml with application version for image tags 59 | yq -i ".ui.image.tag = \"$APP_VERSION\"" chart/kubeseal-webgui/values.yaml 60 | yq -i ".api.image.tag = \"$APP_VERSION\"" chart/kubeseal-webgui/values.yaml 61 | 62 | # Package the chart with the bumped chart version 63 | helm package chart/kubeseal-webgui --version "$CHART_VERSION" --app-version "$APP_VERSION" 64 | 65 | # Lowercase the repository name for OCI registry compatibility 66 | REPO_NAME=$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]') 67 | 68 | # Push the packaged chart 69 | helm push kubeseal-webgui-$CHART_VERSION.tgz oci://ghcr.io/$REPO_NAME/charts 70 | 71 | - name: Commit Version Updates 72 | run: | 73 | # Configure git 74 | git config user.name "github-actions[bot]" 75 | git config user.email "github-actions[bot]@users.noreply.github.com" 76 | 77 | # Add the updated files 78 | git add chart/kubeseal-webgui/Chart.yaml chart/kubeseal-webgui/values.yaml 79 | 80 | # Commit with a clear message 81 | git commit -m "chore: bump chart version to $(yq '.version' chart/kubeseal-webgui/Chart.yaml) for app version $(yq '.appVersion' chart/kubeseal-webgui/Chart.yaml) [skip ci]" || echo "No changes to commit" 82 | 83 | # Push to master 84 | git push origin HEAD:master 85 | -------------------------------------------------------------------------------- /chart/kubeseal-webgui/values.yaml: -------------------------------------------------------------------------------- 1 | replicaCount: 1 2 | annotations: {} 3 | api: 4 | # The value of api.url should be set to the public-accessible http endpoint (ingress url or OpenShift route). 5 | # api.url will be generated into config.json ConfigMap of the UI. This statically served JSON file 6 | # is used by the UI to locate the API. 7 | url: http://localhost:8080 8 | image: 9 | repository: ghcr.io/jaydee94/kubeseal-webgui/api 10 | tag: 4.6.1 11 | environment: {} 12 | loglevel: "INFO" 13 | ui: 14 | image: 15 | repository: ghcr.io/jaydee94/kubeseal-webgui/ui 16 | tag: 4.6.1 17 | image: 18 | pullPolicy: Always 19 | nameOverride: "" 20 | fullnameOverride: "" 21 | # Optionally setup a display name for your kubeseal-webgui instance. 22 | displayName: "" 23 | # Set this value to specify a ServiceAccount that is allowed to list namespaces. 24 | # Leave empty to use the ServiceAccount shipped with this chart. 25 | # If you use a custom ServiceAccount, it must be able to list namespaces in your cluster. 26 | customServiceAccountName: "" 27 | affinity: {} 28 | tolerations: [] 29 | nodeSelector: {} 30 | # Setup resources for the pod 31 | resources: 32 | limits: 33 | # cpu: 100m 34 | memory: 256Mi 35 | requests: 36 | cpu: 20m 37 | memory: 256Mi 38 | # Setup an ingress route optionally 39 | ingress: 40 | enabled: false 41 | annotations: {} 42 | ingressClassName: "" 43 | hostname: "kubeseal-webgui.example.com" 44 | tls: 45 | enabled: false 46 | secretName: "" 47 | # Optionally use a OpenShift-Route 48 | # If 'hostname' is an empty string (""), OpenShift will create a hostname for you. 49 | route: 50 | enabled: false 51 | hostname: "" 52 | tls: 53 | enabled: true 54 | termination: edge 55 | insecureEdgeTerminationPolicy: None 56 | sealedSecrets: 57 | autoFetchCert: false 58 | controllerName: sealed-secrets-controller 59 | controllerNamespace: kube-system 60 | ## Public Certificate of your Sealed-Secrets Controller. 61 | ## Login to your cluster with kubectl. 62 | ## Run kubeseal --fetch-cert --controller-name --controller-namespace 63 | ## Paste Cert as multiline YAML 64 | cert: | 65 | -----BEGIN CERTIFICATE----- 66 | MIIErjCCApagAwIBAgIRAM5gtpf74S6mYr/FZfnM7jIwDQYJKoZIhvcNAQELBQAw 67 | ADAeFw0yMDA5MjIxNzI5MTdaFw0zMDA5MjAxNzI5MTdaMAAwggIiMA0GCSqGSIb3 68 | DQEBAQUAA4ICDwAwggIKAoICAQDDWKl1PV+s6tuMDloSHxgJF65xzLmN7o9TF00N 69 | fCPgmkso2ev9OisBsUw87JrQPZVRFf2KpzN0L91EtLHj9HhPi3VlTjIk05AUweqq 70 | AHdKqvw0mkWmwcEngZhULUoJ8jGk2S5hDpitRMjXmYmfLjxyabY+Kd4waIwAKlLF 71 | onrBosMhIOvsIU+FwgZpo4OF7br7xCwdP9ZS9fZukqoXIDyOc1I/auDM1BWUi7I6 72 | zZ7kFXtY1E0Yv5tCj6U6Si6i3T7omzirphtnNktn3knCtNfkbfbk24OmwpH5RlOT 73 | 8V6VtO0u7QOqKdt9XJD3c+dumMQAwYWMjSUrEkmNXGOyXCcI/jwb2QR/kAPNFyKB 74 | wrAS+2f+lFnWhbqWU1jX4kr00zST6mAxL4QbGOmGUDYzVCmF1bFJ9nKnFbDc8Ssy 75 | H22Wn1iZjcrHj62WnPnUFRDnF4/CDa8royDEF0BvwSziOOq96/80MlHfEiMRZzuJ 76 | gbL7MNo1bWYsMyiwCNM/zen4Ob+T6coKZxJsJMEPAuSakvAC/L9lvsLRm4iPBhxk 77 | cODMWaHOUeKYXhwOGKXGgYw4/xec/y+Xv1z7XiYL4Es72K72sKLOWdIjous1fHcD 78 | OQbGHMjQZPaMcS73x9E5onm8QyvXa9zEbe4/e2uIjIXgPs9DhtjN4BB69D9qIbsi 79 | b7AR2wIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAAEwDwYDVR0TAQH/BAUwAwEB/zAN 80 | BgkqhkiG9w0BAQsFAAOCAgEAneBJD7v+p9DrFgjnoAuHJRuPoxksgU0EbAOnaMWG 81 | eutuHa6xztZZoufNSFAQrspWCnni5nbmHltpORP+JT+FAEb+iFeg7YsBr1gbTzxC 82 | mcl812El6/Vcec7TP1SVWfrcvYaITfZvKEhgMUhSguvCRAkFcuwJ319qrGcclX2F 83 | p6TLrnI5xzNjhCNTwDC+MtQXPNIUHNZgJbIbyJu267c4iQqfIp116rSmWcBxazqx 84 | XsQfVty+NyBZ3rPQc6HY/vYG0Fms34fSGJOd/0PTINE1USv2qt7hRhVxwU2pl1LO 85 | TToKGg/lniIfBLQKozIeeLsPm2/sXhm0aLskbyi6X1+bQzb0QDz+LSkvvlTi52Ay 86 | h55MnD9OslzvIk7h/WhYXcj4vpHPAcAGzbY+rVZKQfU1eA8AIjr6QjX8ndWNLexw 87 | yqi1hH0PRSSYPToIDQUrMm1aBlEgMDhjuNs7eQGDvIGRIg7wckEWEcbIsbvJoubG 88 | iaGs6/SY1jYoRPaPoiiBT0Ns3F3WthypK+a77WLq4weCpnF4fEE9f/5Uc6t/nYP7 89 | fZgxSAO2xSR1wszRWwwH+rKkh2bhqwdDCo5mKqZF5PLcdgypBQRIMMqjQO1qZG7t 90 | REt3DXiF9j6RyxP53tLAdVpuCvMaw2LRqPln4jbERLwx2ckjtvDhNJiEo0+k4NJV 91 | 120= 92 | -----END CERTIFICATE----- 93 | -------------------------------------------------------------------------------- /chart/kubeseal-webgui/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "kubeseal-webgui.fullname" . }} 5 | labels: 6 | {{- include "kubeseal-webgui.labels" . | nindent 4 }} 7 | spec: 8 | selector: 9 | matchLabels: 10 | app: {{ template "kubeseal-webgui.name" . }} 11 | replicas: {{ .Values.replicaCount }} 12 | template: 13 | metadata: 14 | labels: 15 | {{- include "kubeseal-webgui.labels" . | nindent 8 }} 16 | annotations: 17 | {{ toYaml .Values.annotations | nindent 8 }} 18 | spec: 19 | {{- with .Values.securityContext }} 20 | securityContext: 21 | {{- toYaml . | nindent 8 }} 22 | {{- end }} 23 | serviceAccountName: {{ .Values.customServiceAccountName | default "kubeseal-webgui" | quote }} 24 | {{- if .Values.tolerations }} 25 | tolerations: 26 | {{ toYaml .Values.tolerations | nindent 8 }} 27 | {{- end }} 28 | {{- if .Values.affinity }} 29 | affinity: 30 | {{ toYaml .Values.affinity | nindent 8 }} 31 | {{- end }} 32 | {{- if .Values.nodeSelector }} 33 | nodeSelector: 34 | {{ toYaml .Values.nodeSelector | nindent 8 }} 35 | {{- end }} 36 | containers: 37 | - name: "api" 38 | image: "{{ .Values.api.image.repository }}:{{ .Values.api.image.tag }}" 39 | imagePullPolicy: {{ .Values.image.pullPolicy }} 40 | env: 41 | {{- range $key, $value := .Values.api.environment }} 42 | - name: {{ $key }} 43 | value: {{ $value | quote }} 44 | {{- end }} 45 | - name: ORIGIN_URL 46 | value: "{{ .Values.api.url }}" 47 | - name: KUBESEAL_CERT 48 | value: "/kubeseal-webgui/cert/kubeseal-cert.pem" 49 | {{- if .Values.sealedSecrets.autoFetchCert }} 50 | - name: KUBESEAL_AUTOFETCH 51 | value: "true" 52 | - name: KUBESEAL_CONTROLLER_NAME 53 | value: {{ .Values.sealedSecrets.controllerName | quote }} 54 | - name: KUBESEAL_CONTROLLER_NAMESPACE 55 | value: {{ .Values.sealedSecrets.controllerNamespace | quote }} 56 | {{- end }} 57 | ports: 58 | - name: api 59 | containerPort: 5000 60 | protocol: TCP 61 | livenessProbe: 62 | httpGet: 63 | path: / 64 | port: api 65 | initialDelaySeconds: 20 66 | readinessProbe: 67 | httpGet: 68 | path: / 69 | port: api 70 | initialDelaySeconds: 20 71 | {{- with .Values.resources }} 72 | resources: 73 | {{- toYaml . | nindent 12 }} 74 | {{- end }} 75 | volumeMounts: 76 | {{- if .Values.sealedSecrets.autoFetchCert }} 77 | - name: sealed-secrets-certs 78 | mountPath: /kubeseal-webgui/cert 79 | {{- else }} 80 | - name: sealed-secret-configmap 81 | mountPath: /kubeseal-webgui/cert/kubeseal-cert.pem 82 | subPath: kubeseal-cert.pem 83 | {{- end }} 84 | - name: sealed-secret-configmap 85 | mountPath: /kubeseal-webgui/src/config/logging_config.yaml 86 | subPath: logging_config.yaml 87 | - name: "ui" 88 | image: "{{ .Values.ui.image.repository }}:{{ .Values.ui.image.tag }}" 89 | imagePullPolicy: {{ .Values.image.pullPolicy }} 90 | ports: 91 | - name: ui 92 | containerPort: 8080 93 | protocol: TCP 94 | livenessProbe: 95 | httpGet: 96 | path: / 97 | port: ui 98 | initialDelaySeconds: 10 99 | readinessProbe: 100 | httpGet: 101 | path: / 102 | port: ui 103 | initialDelaySeconds: 10 104 | resources: 105 | {{- toYaml .Values.resources | nindent 12 }} 106 | volumeMounts: 107 | - name: sealed-secret-configmap 108 | mountPath: /usr/share/nginx/html/config.json 109 | subPath: config.json 110 | volumes: 111 | {{- if .Values.sealedSecrets.autoFetchCert }} 112 | - name: sealed-secrets-certs 113 | emptyDir: {} 114 | {{- end }} 115 | - name: sealed-secret-configmap 116 | configMap: 117 | name: {{ include "kubeseal-webgui.fullname" . }} 118 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Web-Gui for Bitnami Sealed-Secrets 2 | 3 | [![made-with-python](https://img.shields.io/badge/Made%20with-Python-1f425f.svg)](https://www.python.org/) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![DeepSource](https://static.deepsource.io/deepsource-badge-light-mini.svg)](https://deepsource.io/gh/Jaydee94/kubeseal-webgui/?ref=repository-badge) [![CodeQL](https://github.com/Jaydee94/kubeseal-webgui/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/Jaydee94/kubeseal-webgui/actions/workflows/codeql-analysis.yml) 4 | 5 |

6 | 7 |

8 | 9 | ## Description 10 | 11 | This is a python based webapp for using Bitnami-Sealed-Secrets in a web-gui. 12 | 13 | This app uses the kubeseal binary of the original project: 14 | 15 | The docker images can be found here: 16 | 17 | * https://hub.docker.com/repository/docker/kubesealwebgui/ui 18 | * https://hub.docker.com/repository/docker/kubesealwebgui/api 19 | 20 | ## Demo 21 | 22 | ![KubeSeal WebGui Demo](demo/kubeseal-webgui-demo-4.6.0.gif) 23 | 24 | ## Prerequisites 25 | 26 | To use this Web-Gui you have to install [Bitnami-Sealed-Secrets](https://github.com/bitnami-labs/sealed-secrets) in your cluster first! 27 | 28 | ## Installation 29 | 30 | You can use the helm chart which is included inside this repository to install kubseal-webgui. 31 | 32 | You can install the chart directly from the GitHub Container Registry as an OCI artifact: 33 | 34 | ```bash 35 | # Make sure to configure all required values (with helm's --set argument) documented in our helm Chart before installing. 36 | helm install kubeseal-webgui oci://ghcr.io/jaydee94/kubeseal-webgui/charts/kubeseal-webgui --namespace 37 | ``` 38 | 39 | ## Usage 40 | 41 | Mount the public certificate of your sealed secrets controller to `/kubeseal-webgui/cert/kubeseal-cert.pem` in the container. 42 | 43 | Please use the [helm chart](https://github.com/Jaydee94/kubeseal-webgui/tree/master/chart/kubeseal-webgui) which is included in this repository. 44 | 45 | ## Upgrade from 2.0.X to 2.1.0 46 | 47 | When upgrading to `2.1.0` make sure that you also update the helm chart for installing kubeseal-webgui. 48 | The application reads namespaces from current kubernetes cluster and needs to have access to list them. 49 | If your default serviceaccount has this RBAC rule already you could disable `serviceaccount.create` in the `values.yaml` of the helm chart. 50 | 51 | ## Upgrade from 2.0.X to 3.0.X 52 | 53 | When upgrading to `3.0.X` you dont need to deploy a ingress route to the api. The nginx serving the ui will proxy the requests to the api. 54 | You can use the new helm chart located inside the `chart` folder to deploy the new kubseal-webgui version. 55 | 56 | ## Upgrade from 4.0.X to 4.1.X 57 | 58 | When upgrading from `4.0.X` to `4.1.X` you need to use the provided helm chart in version `>=5.0.0` **if you use the autofetch certificate feature**. 59 | This is because the autofetch certificate functionality is no longer executed as an initContainer. 60 | The api container will fetch the certificate from the sealed-secrets controller on application startup. 61 | 62 | ### Get Public-Cert from sealed-secrets controller 63 | 64 | (Login to your kubernetes cluster first) 65 | 66 | `kubeseal --fetch-cert --controller-name --controller-namespace > kubeseal-cert.pem` 67 | 68 | # Contribute 69 | 70 | ## Working on the API 71 | 72 | ### Requirements 73 | 74 | * Make sure you have Python 3.12 installed. 75 | 76 | #### Setup API 77 | 78 | * Clone this repository and run `cd api`. 79 | * `python3 -m venv venv` (to create a virtual environment called `venv` that doesn't interfere with other projects) 80 | * `source venv/bin/activate` (to activate the virtual environment) 81 | * `python -m pip install .` (to install all required packages for this project) 82 | * `pytest` (should run all tests successfully) 83 | 84 | ### Local API testing 85 | 86 | * Running uvicorn server 87 | 88 | ```bash 89 | MOCK_ENABLED=true poetry run uvicorn kubeseal_webgui_api.app:app --port 5000 --log-config config/logging_config.yaml 90 | ``` 91 | 92 | or use a container and set the environment variables there 93 | 94 | ```bash 95 | docker build -t api -f Dockerfile.api . 96 | docker run --rm -t \ 97 | -p 5000:5000 \ 98 | -e MOCK_ENABLED=TRUE \ 99 | -e KUBESEAL_CERT=/tmp/cert.pem \ 100 | api 101 | ``` 102 | 103 | ## Working on the UI 104 | 105 | ### Setup UI 106 | 107 | * Clone this repository and run `cd ui`. 108 | * You can either use `yarn` or `npm` for the following commands. 109 | * `yarn install` to install all dependencies 110 | * `npm install` to install all dependencies 111 | 112 | ### Local UI testing 113 | 114 | * `yarn dev` to compile and start HTTP server on `port 8080` with hot-reloads for development 115 | * `npm run dev` to compile and start HTTP server on `port 8080` with hot-reloads for development 116 | -------------------------------------------------------------------------------- /ui/src/components/SecretFormInputs.vue: -------------------------------------------------------------------------------- 1 | 108 | 109 | 161 | -------------------------------------------------------------------------------- /.github/workflows/kind.yaml: -------------------------------------------------------------------------------- 1 | name: End to end test 2 | on: 3 | pull_request: 4 | types: 5 | - opened 6 | - reopened 7 | - synchronize 8 | - edited 9 | push: 10 | branches: 11 | - main 12 | - master 13 | 14 | jobs: 15 | create-cluster: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: checkout 19 | uses: actions/checkout@v4 20 | - name: Create k8s kind cluster 21 | uses: helm/kind-action@v1 22 | with: 23 | config: ./kind-config.yaml 24 | wait: 3m 25 | - name: Setup ingress controller 26 | run: | 27 | kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml 28 | kubectl wait --namespace ingress-nginx \ 29 | --for=condition=ready pod \ 30 | --selector=app.kubernetes.io/component=controller \ 31 | --timeout=90s 32 | - name: Setup kubeseal controller 33 | run: | 34 | helm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secrets 35 | helm install sealed-secrets -n kube-system \ 36 | --set-string fullnameOverride=sealed-secrets-controller \ 37 | sealed-secrets/sealed-secrets 38 | - name: Build and upload images 39 | run: | 40 | docker build -t ghcr.io/jaydee94/kubeseal-webgui/api:snapshot -f Dockerfile.api . 41 | docker build -t ghcr.io/jaydee94/kubeseal-webgui/ui:snapshot -f Dockerfile.ui . 42 | kind load docker-image --name chart-testing ghcr.io/jaydee94/kubeseal-webgui/api:snapshot 43 | kind load docker-image --name chart-testing ghcr.io/jaydee94/kubeseal-webgui/ui:snapshot 44 | - name: Deploy stuff 45 | run: | 46 | kubectl create namespace kubeseal-webgui 47 | helm template \ 48 | --release-name e2e-test \ 49 | --namespace kubeseal-webgui \ 50 | --set api.image.tag=snapshot \ 51 | --set api.url=http://$(hostname -f):80 \ 52 | --set autoFetchCertResources=null \ 53 | --set image.pullPolicy=Never \ 54 | --set ingress.enabled=true \ 55 | --set ingress.hostname=$(hostname -f) \ 56 | --set resources=null \ 57 | --set sealedSecrets.autoFetchCert=true \ 58 | --set ui.image.tag=snapshot \ 59 | --set securityContext.runAsUser=1042 \ 60 | chart/kubeseal-webgui \ 61 | | kubectl apply \ 62 | -f - \ 63 | --namespace kubeseal-webgui 64 | - name: Wait until ready 65 | run: | 66 | while ! curl -f http://$(hostname -f):80/namespaces 67 | do 68 | sleep 5 69 | echo "wait 5s" 70 | done 71 | timeout-minutes: 1 72 | - name: Call URL 73 | run: | 74 | curl -f http://$(hostname -f):80/namespaces 75 | curl -f http://$(hostname -f):80/config 76 | echo '{"secret": "a","namespace": "kube-system","scope": "strict","secrets": [{"key": "a","value": "YQ=="},{"key": "b","value": "Yw=="}]}' \ 77 | | curl -f -H 'content-type: application/json' -X POST --data @- http://$(hostname -f):80/secrets 78 | - name: Test Secrets 79 | run: | 80 | API_URL="https://$(hostname -f):443" 81 | kubectl create namespace e2e 82 | strict_secret=$( 83 | echo '{"secret": "strict-secret", "namespace": "e2e", "scope": "strict", "secrets": [{"key": "a-secret","value": "YQ=="}]}' | 84 | curl -f -H 'content-type: application/json' -X POST -k --data @- "${API_URL}/secrets" | 85 | jq -r -s '.[0][] | select(.key=="a-secret") | "a-secret: " + .value') 86 | namespace_secret=$( 87 | echo '{"namespace": "e2e", "scope": "namespace-wide", "secrets": [{"key": "a-secret","value": "YQ=="}]}' | 88 | curl -f -H 'content-type: application/json' -X POST -k --data @- "${API_URL}/secrets" | 89 | jq -r -s '.[0][] | select(.key=="a-secret") | "a-secret: " + .value') 90 | cluster_secret=$( 91 | echo '{"scope": "cluster-wide", "secrets": [{"key": "different-secret","value": "YQ=="}]}' | 92 | curl -f -H 'content-type: application/json' -X POST -k --data @- "${API_URL}/secrets" | 93 | jq -r -s '.[0][] | select(.key=="different-secret") | "a-secret: " + .value') 94 | 95 | cat < 8 | 9 | # with ingress and autofetch certificate 10 | helm install kubeseal-webgui oci://ghcr.io/jaydee94/kubeseal-webgui/charts/kubeseal-webgui --namespace --set ingress.enabled=true --set api.url="http://kubeseal-webgui.example.com" --set sealedSecrets.autoFetchCert=true 11 | ``` 12 | 13 | ## Upgrade from 5.X.X to 6.X.X 14 | 15 | **IMAGES have been moved to GitHub Container Registry!!!** 16 | 17 | Container Images are now uploaded to the GitHub Registry instead of DockerHub. 18 | 19 | [API-Image](https://github.com/Jaydee94/kubeseal-webgui/pkgs/container/kubeseal-webgui%2Fapi) 20 | 21 | [UI-Image](https://github.com/Jaydee94/kubeseal-webgui/pkgs/container/kubeseal-webgui%2Fui) 22 | 23 | ## Uninstalling the Chart 24 | 25 | ```console 26 | To uninstall/delete the my-release deployment: 27 | 28 | helm uninstall kubeseal-webgui kubesealwebgui/kubeseal-webgui --namespace 29 | ``` 30 | 31 | The command removes all the Kubernetes components associated with the chart and deletes the release. 32 | 33 | ## Configuration 34 | 35 | | Parameter | Description | Default | 36 | |-------------------------------------------|---------------------------------------------------|----------------------------------------| 37 | | `replicaCount` | Number of nodes | `1` | 38 | | `annotations` | Optional annotations for the pods | `{}` | 39 | | `api.image.repository` | Image-Repository and name of the api image. | `ghcr.io/jaydee94/kubeseal-webgui/api` | 40 | | `api.image.tag` | Image Tag of the api image. | `4.5.4` | 41 | | `api.environment` | Additional env variables for the api image. | `{}` | 42 | | `api.loglevel` | Loglevel for the api image. | `INFO` | 43 | | `ui.image.repository` | Image-Repository and name of the ui image. | `ghcr.io/jaydee94/kubeseal-webgui/ui` | 44 | | `ui.image.tag` | Image Tag of the ui image. | `4.5.4` | 45 | | `image.pullPolicy` | Image Pull Policy | `Always` | 46 | | `nameOverride` | Name-Override for the objects | `""` | 47 | | `fullnameOverride` | Fullname-Override for the objects | `""` | 48 | | `customServiceAccountName` | Optionallyn define your own serviceaccount to use | `true` | 49 | | `tolerations` | Add tolerations to the deployment. | `[]` | 50 | | `affinity` | Add affinity rules to the deployment. | `{}` | 51 | | `nodeSelector` | Add a nodeSelector to the deployment. | `{}` | 52 | | `displayName` | Optional display name for the kubeseal instance | `""` | 53 | | `resources.limits.cpu` | Limits CPU | `` | 54 | | `resources.limits.memory` | Limits memory | `256Mi` | 55 | | `resources.requests.cpu` | Requests CPU | `20m` | 56 | | `resources.requests.memory` | Requests memory | `20m` | 57 | | `ingress.enabled` | Enable an ingress route | `false` | 58 | | `ingress.annotations` | Additional annotations for the ingress object. | `{}` | 59 | | `ingress.ingressClassName` | Additional ingressClassName. | `""` | 60 | | `ingress.hostname` | The hostname for the ingress route | `kubeseal-webgui.example.com` | 61 | | `ingress.tls.enabled` | Enable TLS for the ingress route | `false` | 62 | | `ingress.tls.secretName` | The secret name for private and public key | `""` | 63 | | `route.enabled` | Deploy OpenShift route | `false` | 64 | | `route.hostname` | Set Hostname of the route | `""` | 65 | | `route.tls.enabled` | Enable/Disable TLS for OpenShift Route | `true` | 66 | | `route.tls.termination` | TLS Termination of the route | `""` | 67 | | `route.tls.insecureEdgeTerminationPolicy` | TLS insecureEdgeTerminationPolicy of the route | `""` | 68 | | `sealedSecrets.autoFetchCert` | Load the cert from the Controller on start | `false` | 69 | | `sealedSecrets.controllerName` | Deployment name of the Controller | `sealed-secrets-controller` | 70 | | `sealedSecrets.controllerNamespace` | Namespace the Controller resides in | `kube-system` | 71 | | `sealedSecrets.cert` | Public-Key of your SealedSecrets controller | `""` | 72 | | `api.environment` | Additional API environment variables | `{}` | 73 | -------------------------------------------------------------------------------- /ui/tests/test_ui.py: -------------------------------------------------------------------------------- 1 | import os 2 | from playwright.sync_api import sync_playwright, Page 3 | 4 | 5 | def test_ui_start(): 6 | with sync_playwright() as ctx: 7 | browser = ctx.chromium.launch(headless=True) 8 | page = browser.new_page() 9 | page.goto("http://localhost:8080") 10 | page.wait_for_load_state("load") 11 | assert page.title() == "Kubeseal WebGui - Sealed Secrets Management" 12 | browser.close() 13 | 14 | 15 | def test_secret_form_with_value(): 16 | with sync_playwright() as ctx: 17 | browser = ctx.chromium.launch(headless=True) 18 | page = browser.new_page() 19 | page.goto("http://localhost:8080") 20 | page.wait_for_load_state("load") 21 | 22 | disabled_encrypt_button(page) 23 | namespace_select(page) 24 | secret_name(page) 25 | scope_strict(page) 26 | add_secret_key_value(page) 27 | click_encrypt_button(page) 28 | 29 | browser.close() 30 | 31 | 32 | def test_secret_form_with_file(): 33 | with sync_playwright() as ctx: 34 | browser = ctx.chromium.launch(headless=True) 35 | page = browser.new_page() 36 | page.goto("http://localhost:8080") 37 | page.wait_for_load_state("load") 38 | 39 | disabled_encrypt_button(page) 40 | namespace_select(page) 41 | secret_name(page) 42 | scope_strict(page) 43 | add_secret_key_file(page) 44 | click_encrypt_button(page) 45 | 46 | browser.close() 47 | 48 | 49 | def test_secret_form_with_invalid_file(): 50 | with sync_playwright() as ctx: 51 | browser = ctx.chromium.launch(headless=True) 52 | page = browser.new_page() 53 | page.goto("http://localhost:8080") 54 | page.wait_for_load_state("load") 55 | 56 | file_input = page.locator('input[type="file"]#fileInput') 57 | # Try to upload a non-valid file 58 | invalid_file_path = os.path.join(os.getcwd(), "large_test_file.txt") 59 | with open(invalid_file_path, "w") as f: 60 | f.write( 61 | "X" * (10 * 1024 * 1024) 62 | ) # Create a 10MB file (assuming it's too large) 63 | file_input.set_input_files(invalid_file_path) 64 | 65 | # Check for the snackbar error message 66 | snackbar_message = page.locator("div[role='status']").get_by_text("File size should be less than 1 MB!") 67 | assert ( 68 | snackbar_message.is_visible() 69 | ), "Snackbar error message should be visible for invalid file" 70 | 71 | if os.path.exists(invalid_file_path): 72 | os.remove(invalid_file_path) 73 | browser.close() 74 | 75 | 76 | def namespace_select(page: Page): 77 | input_selector = "input#namespaceSelection" 78 | page.wait_for_selector(input_selector, timeout=10000) 79 | page.click(input_selector) 80 | page.wait_for_selector(".v-list-item-title") 81 | suggestions = page.query_selector_all(".v-list-item-title") 82 | assert len(suggestions) > 0, "No suggestions found." 83 | first_suggestion_text = suggestions[0].inner_text() 84 | suggestions[0].click() 85 | selected_text_selector = ".v-autocomplete__selection-text" 86 | page.wait_for_selector(selected_text_selector, timeout=10000) 87 | displayed_text = page.inner_text(selected_text_selector) 88 | assert ( 89 | displayed_text == first_suggestion_text 90 | ), f"Expected '{first_suggestion_text}', but got '{displayed_text}'" 91 | 92 | 93 | def secret_name(page: Page): 94 | input_selector = "#secretName" 95 | page.wait_for_selector(input_selector) 96 | input_text = "valid-secret-name" 97 | page.fill(input_selector, input_text) 98 | assert ( 99 | page.input_value(input_selector) == input_text 100 | ), f"Expected {input_text}, but got {page.input_value(input_selector)}" 101 | 102 | 103 | def scope_strict(page: Page): 104 | select_selector = "div.v-select" 105 | page.wait_for_selector(select_selector) 106 | page.click(select_selector) 107 | item_selector = "div.v-list-item" 108 | page.wait_for_selector(item_selector) 109 | items = page.query_selector_all(item_selector) 110 | assert len(items) > 0, "No items found in the dropdown." 111 | for item in items: 112 | title_element = item.query_selector("div.v-list-item-title") 113 | if title_element and title_element.inner_text().strip() == "strict": 114 | item.click() 115 | break 116 | 117 | 118 | def add_secret_key_value(page: Page): 119 | page.wait_for_selector("textarea#secretKey") 120 | page.fill("textarea#secretKey", "my-secret-key") 121 | assert page.locator("textarea#secretKey").input_value() == "my-secret-key" 122 | page.wait_for_selector("textarea#secretValue") 123 | page.fill("textarea#secretValue", "my-secret-value") 124 | assert page.locator("textarea#secretValue").input_value() == "my-secret-value" 125 | file_input = page.locator('input[type="file"]#fileInput') 126 | assert ( 127 | not file_input.is_enabled() 128 | ), "File input should be disabled when value is filled" 129 | 130 | 131 | def add_secret_key_file(page: Page): 132 | page.wait_for_selector("textarea#secretKey") 133 | page.fill("textarea#secretKey", "my-secret-key") 134 | assert page.locator("textarea#secretKey").input_value() == "my-secret-key" 135 | 136 | file_input = page.locator('input[type="file"]#fileInput') 137 | assert file_input.is_visible() 138 | test_file_path = os.path.join(os.getcwd(), "test_file.txt") 139 | with open(test_file_path, "w") as f: 140 | f.write("This is a test file.") 141 | file_input.set_input_files(test_file_path) 142 | if os.path.exists(test_file_path): 143 | os.remove(test_file_path) 144 | 145 | 146 | def disabled_encrypt_button(page: Page): 147 | encrypt_button = page.locator('button:has-text("Encrypt")') 148 | encrypt_button.wait_for() 149 | assert encrypt_button.is_disabled() 150 | 151 | 152 | def click_encrypt_button(page: Page): 153 | # Mock the fetchEncodedSecrets function by overriding it in the browser context 154 | page.evaluate( 155 | """ 156 | const appElement = document.querySelector('#app'); 157 | const vueApp = appElement.__vue_app__; 158 | 159 | if (vueApp) { 160 | vueApp._instance.proxy.fetchEncodedSecrets = function() { 161 | window.fetchEncodedSecretsCalled = true; 162 | }; 163 | } 164 | """ 165 | ) 166 | encrypt_button = page.locator('button:has-text("Encrypt")') 167 | encrypt_button.wait_for() 168 | assert encrypt_button.is_enabled() 169 | encrypt_button.click() 170 | is_called = page.evaluate("window.fetchEncodedSecretsCalled === true") 171 | assert is_called, "fetchEncodedSecrets function was not called!" 172 | -------------------------------------------------------------------------------- /kind-setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Set-Up a kind cluster with kubeseal and kubeseal-webgui 3 | # The ui will listen on http://localhost:7180 4 | 5 | set -euo pipefail 6 | 7 | # Configuration 8 | readonly CLUSTER_NAME="chart-testing" 9 | readonly NAMESPACE="kubeseal-webgui" 10 | readonly E2E_NAMESPACE="e2e" 11 | HOSTNAME_FQDN="$(hostname -f)" 12 | readonly API_URL="https://${HOSTNAME_FQDN}:7143" 13 | readonly TIMEOUT="90s" 14 | readonly MAX_RETRIES=3 15 | readonly RETRY_DELAY=5 16 | 17 | # Colors for output 18 | readonly RED='\033[0;31m' 19 | readonly GREEN='\033[0;32m' 20 | readonly YELLOW='\033[1;33m' 21 | readonly NC='\033[0m' # No Color 22 | 23 | # Helper functions 24 | log_info() { 25 | echo -e "${GREEN}[INFO]${NC} $*" 26 | } 27 | 28 | log_warn() { 29 | echo -e "${YELLOW}[WARN]${NC} $*" 30 | } 31 | 32 | log_error() { 33 | echo -e "${RED}[ERROR]${NC} $*" >&2 34 | } 35 | 36 | wait_for_api() { 37 | local url="$1" 38 | local retries="$2" 39 | local delay="$3" 40 | 41 | for i in $(seq 1 "$retries"); do 42 | if curl -s -k -f "$url" > /dev/null 2>&1; then 43 | log_info "API is ready" 44 | return 0 45 | fi 46 | log_warn "Waiting for API (attempt $i/$retries)..." 47 | sleep "$delay" 48 | done 49 | 50 | log_error "API failed to become ready after $retries attempts" 51 | return 1 52 | } 53 | 54 | create_sealed_secret() { 55 | local name="$1" 56 | local namespace="$2" 57 | local scope="$3" 58 | local key="$4" 59 | local annotations="$5" 60 | 61 | local payload 62 | if [[ "$scope" == "strict" ]]; then 63 | payload="{\"secret\": \"$name\", \"namespace\": \"$namespace\", \"scope\": \"$scope\", \"secrets\": [{\"key\": \"$key\",\"value\": \"YQ==\"}]}" 64 | elif [[ "$scope" == "namespace-wide" ]]; then 65 | payload="{\"namespace\": \"$namespace\", \"scope\": \"$scope\", \"secrets\": [{\"key\": \"$key\",\"value\": \"YQ==\"}]}" 66 | else 67 | payload="{\"scope\": \"$scope\", \"secrets\": [{\"key\": \"$key\",\"value\": \"YQ==\"}]}" 68 | fi 69 | 70 | local encrypted_data 71 | encrypted_data=$(echo "$payload" | \ 72 | curl -sf -H 'content-type: application/json' -X POST -k --data @- "${API_URL}/secrets" | \ 73 | jq -r -s ".[0][] | select(.key==\"$key\") | \"$key: \" + .value") 74 | 75 | cat </dev/null || true 116 | if helm list -n kube-system | grep -q "^sealed-secrets"; then 117 | log_info "Sealed-secrets already installed, skipping" 118 | else 119 | helm install sealed-secrets -n kube-system \ 120 | --set-string fullnameOverride=sealed-secrets-controller \ 121 | sealed-secrets/sealed-secrets 122 | fi 123 | 124 | log_info "Installing ingress-nginx" 125 | if kubectl get namespace ingress-nginx &>/dev/null; then 126 | log_info "Ingress-nginx already installed, skipping" 127 | else 128 | kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml 129 | fi 130 | kubectl wait --namespace ingress-nginx \ 131 | --for=condition=ready pod \ 132 | --selector=app.kubernetes.io/component=controller \ 133 | --timeout="$TIMEOUT" 134 | 135 | log_info "Building Docker images in parallel" 136 | docker build -t ghcr.io/jaydee94/kubeseal-webgui/api:snapshot -f Dockerfile.api . & 137 | api_build_pid=$! 138 | docker build -t ghcr.io/jaydee94/kubeseal-webgui/ui:snapshot -f Dockerfile.ui . & 139 | ui_build_pid=$! 140 | 141 | # Wait for both builds to complete 142 | wait "$api_build_pid" || { log_error "API build failed"; exit 1; } 143 | wait "$ui_build_pid" || { log_error "UI build failed"; exit 1; } 144 | log_info "Docker images built successfully" 145 | 146 | log_info "Loading images into kind cluster" 147 | kind load docker-image --name "$CLUSTER_NAME" \ 148 | ghcr.io/jaydee94/kubeseal-webgui/api:snapshot \ 149 | ghcr.io/jaydee94/kubeseal-webgui/ui:snapshot 150 | 151 | log_info "Creating namespace: $NAMESPACE" 152 | kubectl create namespace "$NAMESPACE" 2>/dev/null || log_info "Namespace '$NAMESPACE' already exists" 153 | 154 | log_info "Deploying kubeseal-webgui via Helm" 155 | helm template \ 156 | --release-name e2e-test \ 157 | --create-namespace \ 158 | --namespace "$NAMESPACE" \ 159 | --set api.image.tag=snapshot \ 160 | --set api.url="$API_URL" \ 161 | --set autoFetchCertResources=null \ 162 | --set image.pullPolicy=Never \ 163 | --set ingress.enabled=true \ 164 | --set ingress.hostname="$(hostname -f)" \ 165 | --set resources=null \ 166 | --set sealedSecrets.autoFetchCert=true \ 167 | --set ui.image.tag=snapshot \ 168 | --set securityContext.runAsUser=1042 \ 169 | chart/kubeseal-webgui \ 170 | | kubectl apply -f - --namespace "$NAMESPACE" 171 | 172 | log_info "Restarting deployment to use new images" 173 | kubectl rollout restart deployment/e2e-test-kubeseal-webgui -n "$NAMESPACE" 174 | 175 | log_info "Waiting for kubeseal-webgui deployment to complete" 176 | kubectl rollout status deployment/e2e-test-kubeseal-webgui -n "$NAMESPACE" --timeout="$TIMEOUT" 177 | 178 | kubectl wait --namespace "$NAMESPACE" \ 179 | --for=condition=ready pod \ 180 | --selector=app=kubeseal-webgui \ 181 | --timeout="$TIMEOUT" 182 | 183 | log_info "Verifying API accessibility" 184 | wait_for_api "$API_URL" "$MAX_RETRIES" "$RETRY_DELAY" 185 | 186 | log_info "Creating test namespace: $E2E_NAMESPACE" 187 | kubectl create namespace "$E2E_NAMESPACE" 2>/dev/null || log_info "Namespace '$E2E_NAMESPACE' already exists" 188 | 189 | log_info "Creating sealed secrets for testing" 190 | create_sealed_secret "strict-secret" "$E2E_NAMESPACE" "strict" "a-secret" "{ }" 191 | create_sealed_secret "namespace-secret" "$E2E_NAMESPACE" "namespace-wide" "a-secret" "{ sealedsecrets.bitnami.com/namespace-wide: \"true\" }" 192 | create_sealed_secret "cluster-secret" "$E2E_NAMESPACE" "cluster-wide" "a-secret" "{ sealedsecrets.bitnami.com/cluster-wide: \"true\" }" 193 | 194 | log_info "Waiting for secrets to be unsealed" 195 | sleep 5 196 | 197 | log_info "Verifying unsealed secrets" 198 | for secret_name in strict-secret namespace-secret cluster-secret; do 199 | if [[ "$(kubectl get secret "$secret_name" -n "$E2E_NAMESPACE" \ 200 | -o go-template --template '{{ index .data "a-secret" }}')" == "YQ==" ]]; then 201 | log_info "Testing ${secret_name}: OK" 202 | else 203 | log_error "Secret $secret_name verification failed" 204 | exit 1 205 | fi 206 | done 207 | 208 | log_info "Setup complete! Access the UI at: http://$(hostname -f):7180" 209 | -------------------------------------------------------------------------------- /api/tests/test_run_kubeseal.py: -------------------------------------------------------------------------------- 1 | from base64 import b64encode 2 | from unittest.mock import MagicMock, patch 3 | 4 | import pytest 5 | 6 | from kubeseal_webgui_api.routers.kubeseal import ( 7 | Scope, 8 | decode_base64_bytearray, 9 | decode_base64_string, 10 | run_kubeseal, 11 | valid_k8s_name, 12 | ) 13 | from kubeseal_webgui_api.routers.models import Secret 14 | 15 | 16 | @pytest.mark.parametrize( 17 | "value", 18 | [ 19 | "abc", 20 | "l" + "o" * 60 + "ng", 21 | "some-1-too-check", 22 | "ends-on-digit-1", 23 | "1starts-with-it", 24 | "some.dots.or_underscore", 25 | "long-" + "a" * 248, 26 | ], 27 | ) 28 | def test_valid_k8s_name(value): 29 | # given a valid k8s-label-name 30 | # when valid_k8s_name is called on the label-name 31 | # then the label-name is returned unchanged 32 | assert valid_k8s_name(value) == value 33 | 34 | 35 | @pytest.mark.parametrize( 36 | "value", 37 | [ 38 | "", 39 | "-something", 40 | "too-l" + "o" * 247 + "ng", 41 | "ähm", 42 | "_not-valid", 43 | "with spaces", 44 | " not-trimmed ", 45 | "no-special-chars-like/,#+%", 46 | "ends-on-dash-", 47 | "Uppercase", 48 | "U", 49 | "uPPer", 50 | ], 51 | ) 52 | def test_invalid_k8s_name(value): 53 | # given an invalid k8s-label-name 54 | # when valid_k8s_name is called on the label-name 55 | # then a ValueError is raised 56 | with pytest.raises(ValueError, match="Invalid k8s name"): 57 | valid_k8s_name(value) 58 | 59 | 60 | def test_run_kubeseal_with_with_empty_string_namespace(): 61 | # given an empty string secretNamespace 62 | # when run_kubeseal is called 63 | # then raise ValueError 64 | with pytest.raises(ValueError, match="secret_namespace was not given"): 65 | run_kubeseal([Secret(key="foo", value="YmFy")], "", "secretName") 66 | 67 | 68 | def test_run_kubeseal_with_with_none_namespace(): 69 | # given a None secretNamespace 70 | # when run_kubeseal is called 71 | # then raise ValueError 72 | with pytest.raises(ValueError, match="secret_namespace was not given"): 73 | run_kubeseal([Secret(key="foo", value="YmFy")], None, "secretName") 74 | 75 | 76 | def test_run_kubeseal_with_with_empty_string_secret_name(): 77 | # given an empty string secretName 78 | # when run_kubeseal is called 79 | # then raise ValueError 80 | with pytest.raises(ValueError, match="secret_name was not given"): 81 | run_kubeseal([Secret(key="foo", value="YmFy")], "secretNamespace", "") 82 | 83 | 84 | def test_run_kubeseal_with_with_none_secret_name(): 85 | # given a None secretName 86 | # when run_kubeseal is called 87 | # then raise ValueError 88 | with pytest.raises(ValueError, match="secret_name was not given"): 89 | run_kubeseal([Secret(key="foo", value="YmFy")], "secretNamespace", None) 90 | 91 | 92 | def test_run_kubeseal_with_with_empty_secrets_list_but_otherwise_valid_inputs(): 93 | # given an empty list 94 | # when run_kubeseal is called 95 | sealed_secrets = run_kubeseal([], "secretNamespace", "secretName") 96 | # then return empty list 97 | assert sealed_secrets == [] 98 | 99 | 100 | @patch("kubeseal_webgui_api.routers.kubeseal.run_kubeseal_command") 101 | @pytest.mark.parametrize("scope", list(Scope)) 102 | def test_run_kubeseal_with_scope(mock_run_command: MagicMock, scope: Scope): 103 | encoded_value = b64encode(b"something").decode("ascii") 104 | secrets = [Secret(key="foo", value=encoded_value)] 105 | run_kubeseal( 106 | secrets, 107 | "namespace", 108 | "name", 109 | scope, 110 | ) 111 | mock_run_command.assert_called_with(secrets[0], "namespace", "name", scope) 112 | 113 | 114 | @patch("kubeseal_webgui_api.routers.kubeseal.run_kubeseal_command") 115 | @pytest.mark.parametrize("scope", list(Scope)) 116 | def test_run_kubeseal_with_scope_needed_params_only( 117 | mock_run_command: MagicMock, scope: Scope 118 | ): 119 | encoded_value = b64encode(b"something").decode("ascii") 120 | secrets = [ 121 | Secret( 122 | key="foo", 123 | value=encoded_value, 124 | ) 125 | ] 126 | if scope.needs_namespace(): 127 | namespace = "namespace" 128 | else: 129 | namespace = None 130 | if scope.needs_name(): 131 | name = "name" 132 | else: 133 | name = None 134 | run_kubeseal( 135 | secrets, 136 | namespace, 137 | name, 138 | scope, 139 | ) 140 | mock_run_command.assert_called_with(secrets[0], namespace, name, scope) 141 | 142 | 143 | @pytest.mark.container() 144 | @pytest.mark.cluster() 145 | def test_run_kubeseal_with_cli(): 146 | # given run test against cli with test cluster 147 | # when run_kubeseal is called 148 | # then return valid encrypted secret 149 | pass 150 | 151 | 152 | @pytest.mark.cluster() 153 | @patch( 154 | "kubeseal_webgui_api.app_config.settings.kubeseal_binary", "/bin/no-such-thing-here" 155 | ) 156 | def test_run_kubeseal_without_cli(): 157 | # given k8s cluster but no kubeseal cli 158 | # when run_kubeseal is called 159 | # then raise RuntimeError 160 | with pytest.raises(RuntimeError): 161 | run_kubeseal( 162 | [Secret(key="foo", value="YmFy")], "secret-namespace", "secret-name" 163 | ) 164 | 165 | 166 | def test_run_kubeseal_with_invalid_secrets_list_but_otherwise_valid_inputs(): 167 | # given a secret list with string element 168 | # when run_kubeseal is called 169 | # then raise ValueError 170 | with pytest.raises( 171 | ValueError, match="Input of cleartext_secrets was not a list of dicts." 172 | ): 173 | run_kubeseal(["this-should-be-a-dict-object"], "secretNamespace", "secretName") # type: ignore 174 | 175 | 176 | @pytest.mark.container() 177 | def test_run_kubeseal_without_k8s_cluster(): 178 | # given kubeseal cli but no k8s cluster 179 | # when run_kubeseal is called 180 | # then raise RuntimeError 181 | with pytest.raises(RuntimeError) as error_cert_missing: 182 | run_kubeseal( 183 | [Secret(key="foo", value="YmFy")], "secret-namespace", "secret-name" 184 | ) 185 | assert "/kubeseal-webgui/cert/kubeseal-cert.pem: no such file or directory" in str( 186 | error_cert_missing 187 | ) 188 | 189 | 190 | @pytest.mark.parametrize( 191 | ("base64_input", "expected_output"), 192 | [("YWJjZGVm", "abcdef"), ("w6TDtsO8", "äöü"), ("LV8jIT8kwqc=", "-_#!?$§")], 193 | ) 194 | def test_decode_base64_string(base64_input, expected_output): 195 | """ 196 | Test decode_base64_string. 197 | 198 | Given a tuple with a Base64 input string and the corresponding output string. 199 | When calling decode_base64_string on input string. 200 | Then return the corresponding output string. 201 | """ 202 | base64_encoded_string = decode_base64_string(base64_input) 203 | assert base64_encoded_string == expected_output 204 | 205 | 206 | @pytest.mark.parametrize( 207 | ("base64_input", "expected_output"), 208 | [ 209 | ("YWJjZGVm", b"abcdef"), 210 | ("w6TDtsO8", "äöü".encode("utf-8")), 211 | ("LV8jIT8kwqc=", "-_#!?$§".encode("utf-8")), 212 | ], 213 | ) 214 | def test_decode_base64_bytearray(base64_input, expected_output): 215 | """ 216 | Test decode_base64_bytearray. 217 | 218 | Given a tuple with a Base64 input string and the corresponding output bytearray. 219 | When calling decode_base64_bytearray on input string. 220 | Then return the corresponding output bytearray. 221 | """ 222 | decoded = decode_base64_bytearray(base64_input) 223 | assert decoded == bytearray(expected_output) 224 | -------------------------------------------------------------------------------- /api/kubeseal_webgui_api/routers/kubeseal.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import logging 3 | import re 4 | import subprocess # noqa: S404 the binary has to be configured by an admin 5 | from typing import List, Optional, Union, overload 6 | 7 | from fastapi import APIRouter, HTTPException 8 | 9 | from kubeseal_webgui_api.app_config import settings 10 | from kubeseal_webgui_api.routers.models import Data, KeyValuePair, Scope, Secret 11 | 12 | router = APIRouter() 13 | LOGGER = logging.getLogger("kubeseal-webgui") 14 | 15 | 16 | @router.post("/secrets", response_model=List[KeyValuePair]) 17 | def encrypt(data: Data) -> list[KeyValuePair]: 18 | try: 19 | return run_kubeseal( 20 | data.secrets, 21 | data.namespace, 22 | data.secret, 23 | data.scope or Scope.STRICT, 24 | ) 25 | except (KeyError, ValueError) as e: 26 | raise HTTPException(400, f"Invalid data for sealing secrets: {e}") 27 | except RuntimeError: 28 | raise HTTPException(500, "Server is dreaming...") 29 | 30 | 31 | def is_blank(value: Optional[str]) -> bool: 32 | return value is None or value.strip() == "" 33 | 34 | 35 | def verify(name: str, value: Optional[str], mandatory: bool = True) -> None: 36 | if mandatory and is_blank(value): 37 | error_message = f"{name} was not given" 38 | LOGGER.error(error_message) 39 | raise ValueError(error_message) 40 | 41 | 42 | def run_kubeseal( 43 | cleartext_secrets: List[Secret], 44 | secret_namespace: Optional[str], 45 | secret_name: Optional[str], 46 | scope: Scope = Scope.STRICT, 47 | ) -> list[KeyValuePair]: 48 | """Check input and initiate kubeseal-cli call.""" 49 | 50 | verify("secret_namespace", secret_namespace, scope.needs_namespace()) 51 | verify("secret_name", secret_name, scope.needs_name()) 52 | 53 | list_of_non_dict_inputs = [ 54 | element for element in cleartext_secrets if not isinstance(element, Secret) 55 | ] 56 | if cleartext_secrets and list_of_non_dict_inputs: 57 | error_message = "Input of cleartext_secrets was not a list of dicts." 58 | raise ValueError(error_message) 59 | 60 | return [ 61 | run_kubeseal_command( 62 | cleartext_secret_tuple, secret_namespace, secret_name, scope 63 | ) 64 | for cleartext_secret_tuple in cleartext_secrets 65 | ] 66 | 67 | 68 | def valid_k8s_name(value: str | None) -> str: 69 | if not value: 70 | raise ValueError("Invalid k8s name: Must not be empty or None") 71 | if re.match(r"^[a-z0-9]([a-z0-9_.-]{,251}[a-z0-9])?$", value): 72 | return value 73 | raise ValueError(f"Invalid k8s name: {value}") 74 | 75 | 76 | def run_kubeseal_command( 77 | cleartext_secret_tuple: Secret, 78 | secret_namespace: Optional[str], 79 | secret_name: Optional[str], 80 | scope: Scope = Scope.STRICT, 81 | ) -> KeyValuePair: 82 | LOGGER.info( 83 | "Sealing secret '%s.%s' for namespace '%s' with scope '%s'.", 84 | secret_name, 85 | cleartext_secret_tuple.key, 86 | secret_namespace, 87 | scope, 88 | ) 89 | 90 | if settings.mock_enabled: 91 | return KeyValuePair( 92 | key=cleartext_secret_tuple.key, 93 | value="AgBy3... (Mock Sealed Secret)" 94 | ) 95 | 96 | if cleartext_secret_tuple.value is not None: 97 | cleartext_secret = decode_base64_string(cleartext_secret_tuple.value) 98 | return encrypt_value_or_file( 99 | cleartext_secret_tuple, 100 | secret_namespace, 101 | secret_name, 102 | cleartext_secret, 103 | settings.kubeseal_binary, 104 | settings.kubeseal_cert, 105 | scope, 106 | ) 107 | if cleartext_secret_tuple.file is not None: 108 | file_secret = decode_base64_bytearray(cleartext_secret_tuple.file) 109 | return encrypt_value_or_file( 110 | cleartext_secret_tuple, 111 | secret_namespace, 112 | secret_name, 113 | file_secret, 114 | settings.kubeseal_binary, 115 | settings.kubeseal_cert, 116 | scope, 117 | encoding=None, 118 | ) 119 | raise RuntimeError("Invalid parameters. Must have a file or a value") 120 | 121 | 122 | @overload 123 | def encrypt_value_or_file( 124 | cleartext_secret_tuple: Secret, 125 | secret_namespace: str | None, 126 | secret_name: str | None, 127 | cleartext_secret: str, 128 | binary: str, 129 | cert: str, 130 | scope: Scope, 131 | encoding: str = "utf-8", 132 | ) -> KeyValuePair: ... 133 | 134 | 135 | @overload 136 | def encrypt_value_or_file( 137 | cleartext_secret_tuple: Secret, 138 | secret_namespace: str | None, 139 | secret_name: str | None, 140 | cleartext_secret: bytearray, 141 | binary: str, 142 | cert: str, 143 | scope: Scope, 144 | encoding: None = None, 145 | ) -> KeyValuePair: ... 146 | 147 | 148 | def encrypt_value_or_file( 149 | cleartext_secret_tuple: Secret, 150 | secret_namespace: str | None, 151 | secret_name: str | None, 152 | cleartext_secret: Union[str, bytearray], 153 | binary: str, 154 | cert: str, 155 | scope: Scope, 156 | encoding: Optional[str] = "utf-8", 157 | ) -> KeyValuePair: 158 | kubeseal_command_cmd = construct_kubeseal_cmd( 159 | secret_namespace, secret_name, binary, cert, scope 160 | ) 161 | try: 162 | kubeseal_subprocess = subprocess.Popen( # noqa: S603 input has been checked above 163 | kubeseal_command_cmd, 164 | stdin=subprocess.PIPE, 165 | stdout=subprocess.PIPE, 166 | stderr=subprocess.PIPE, 167 | encoding=encoding, 168 | ) 169 | except FileNotFoundError as file_error: 170 | raise RuntimeError("Could not find kubeseal binary") from file_error 171 | 172 | if encoding: 173 | output, error = kubeseal_subprocess.communicate(input=cleartext_secret) 174 | else: 175 | output_bytes, error_bytes = kubeseal_subprocess.communicate( 176 | input=cleartext_secret 177 | ) 178 | output, error = output_bytes.decode("utf-8"), error_bytes.decode("utf-8") 179 | 180 | if error: 181 | error_message = f"Error in run_kubeseal: {error}" 182 | LOGGER.error(error_message) 183 | raise RuntimeError(error_message) 184 | 185 | sealed_secret = "".join(output.split("\n")) 186 | return KeyValuePair(key=cleartext_secret_tuple.key, value=sealed_secret) 187 | 188 | 189 | def construct_kubeseal_cmd( 190 | secret_namespace: str | None, 191 | secret_name: str | None, 192 | binary: str, 193 | cert: str, 194 | scope: Scope, 195 | ) -> list[str]: 196 | exec_kubeseal_command = [ 197 | binary, 198 | "--raw", 199 | "--from-file=/dev/stdin", 200 | "--cert", 201 | cert, 202 | "--scope", 203 | scope.value, 204 | ] 205 | if scope.needs_namespace(): 206 | exec_kubeseal_command.extend( 207 | [ 208 | "--namespace", 209 | valid_k8s_name(secret_namespace), 210 | ] 211 | ) 212 | if scope.needs_name(): 213 | exec_kubeseal_command.extend( 214 | [ 215 | "--name", 216 | valid_k8s_name(secret_name), 217 | ] 218 | ) 219 | 220 | return exec_kubeseal_command 221 | 222 | 223 | def decode_base64_string(base64_string_message: str) -> str: 224 | """Decode base64 ascii-encoded input.""" 225 | base64_bytes = base64_string_message.encode("ascii") 226 | message_bytes = base64.b64decode(base64_bytes) 227 | return message_bytes.decode("utf-8") 228 | 229 | 230 | def decode_base64_bytearray(base64_string_message: str) -> bytearray: 231 | """Decode base64 ascii-encoded input.""" 232 | base64_bytes = base64_string_message.encode("ascii") 233 | return bytearray(base64.b64decode(base64_bytes)) 234 | -------------------------------------------------------------------------------- /ui/src/assets/styles.css: -------------------------------------------------------------------------------- 1 | /* Modern Design System - Global Styles */ 2 | 3 | /* CSS Custom Properties */ 4 | :root { 5 | /* Gradients */ 6 | --gradient-primary: linear-gradient(135deg, #007bff 0%, #0056b3 100%); 7 | --gradient-secondary: linear-gradient(135deg, #fd7e14 0%, #e8590c 100%); 8 | --gradient-accent: linear-gradient(135deg, #17a2b8 0%, #117a8b 100%); 9 | --gradient-success: linear-gradient(135deg, #28a745 0%, #1e7e34 100%); 10 | --gradient-warm: linear-gradient(135deg, #fd7e14 0%, #ffc107 100%); 11 | --gradient-cool: linear-gradient(135deg, #007bff 0%, #17a2b8 100%); 12 | 13 | /* Glassmorphism - Minimal */ 14 | --glass-bg: rgba(255, 255, 255, 0.05); 15 | --glass-bg-strong: rgba(255, 255, 255, 0.08); 16 | --glass-border: rgba(255, 255, 255, 0.1); 17 | --glass-shadow: 0 2px 8px 0 rgba(31, 38, 135, 0.15); 18 | 19 | /* Shadows - Minimal */ 20 | --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); 21 | --shadow-md: 0 2px 4px rgba(0, 0, 0, 0.08); 22 | --shadow-lg: 0 4px 8px rgba(0, 0, 0, 0.1); 23 | --shadow-xl: 0 8px 16px rgba(0, 0, 0, 0.12); 24 | --shadow-glow-primary: 0 4px 12px rgba(0, 123, 255, 0.2); 25 | --shadow-glow-secondary: 0 4px 12px rgba(253, 126, 20, 0.2); 26 | 27 | /* Transitions */ 28 | --transition-fast: 0.15s cubic-bezier(0.4, 0, 0.2, 1); 29 | --transition-base: 0.3s cubic-bezier(0.4, 0, 0.2, 1); 30 | --transition-slow: 0.5s cubic-bezier(0.4, 0, 0.2, 1); 31 | 32 | /* Border Radius */ 33 | --radius-sm: 8px; 34 | --radius-md: 12px; 35 | --radius-lg: 16px; 36 | --radius-xl: 24px; 37 | --radius-full: 9999px; 38 | 39 | /* Spacing Scale */ 40 | --spacing-xs: 4px; 41 | --spacing-sm: 8px; 42 | --spacing-md: 16px; 43 | --spacing-lg: 24px; 44 | --spacing-xl: 32px; 45 | --spacing-2xl: 48px; 46 | 47 | /* Typography Scale */ 48 | --font-size-xs: 0.75rem; /* 12px */ 49 | --font-size-sm: 0.875rem; /* 14px */ 50 | --font-size-base: 1rem; /* 16px */ 51 | --font-size-lg: 1.125rem; /* 18px */ 52 | --font-size-xl: 1.25rem; /* 20px */ 53 | --font-size-2xl: 1.5rem; /* 24px */ 54 | } 55 | 56 | /* Dark Mode Overrides - Minimal */ 57 | .v-theme--dark { 58 | --glass-bg: rgba(0, 0, 0, 0.1); 59 | --glass-bg-strong: rgba(0, 0, 0, 0.15); 60 | --glass-border: rgba(255, 255, 255, 0.08); 61 | --glass-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.3); 62 | } 63 | 64 | /* Glassmorphism Utilities - Minimal */ 65 | .glass { 66 | background: var(--glass-bg); 67 | backdrop-filter: blur(4px); 68 | -webkit-backdrop-filter: blur(4px); 69 | border: 1px solid var(--glass-border); 70 | box-shadow: var(--shadow-sm); 71 | } 72 | 73 | .glass-strong { 74 | background: var(--glass-bg-strong); 75 | backdrop-filter: blur(6px); 76 | -webkit-backdrop-filter: blur(6px); 77 | border: 1px solid var(--glass-border); 78 | box-shadow: var(--shadow-sm); 79 | } 80 | 81 | .glass-app-bar { 82 | background: linear-gradient(135deg, rgba(0, 123, 255, 0.03), rgba(253, 126, 20, 0.03)) !important; 83 | backdrop-filter: blur(4px); 84 | -webkit-backdrop-filter: blur(4px); 85 | border-bottom: 1px solid var(--glass-border) !important; 86 | box-shadow: var(--shadow-sm) !important; 87 | } 88 | 89 | /* Gradient Backgrounds */ 90 | .bg-gradient-primary { 91 | background: var(--gradient-primary); 92 | } 93 | 94 | .bg-gradient-secondary { 95 | background: var(--gradient-secondary); 96 | } 97 | 98 | .bg-gradient-accent { 99 | background: var(--gradient-accent); 100 | } 101 | 102 | /* Gradient Text */ 103 | .text-gradient-primary { 104 | background: var(--gradient-primary); 105 | -webkit-background-clip: text; 106 | -webkit-text-fill-color: transparent; 107 | background-clip: text; 108 | } 109 | 110 | .text-gradient-secondary { 111 | background: var(--gradient-secondary); 112 | -webkit-background-clip: text; 113 | -webkit-text-fill-color: transparent; 114 | background-clip: text; 115 | } 116 | 117 | .text-gradient-warm { 118 | background: var(--gradient-warm); 119 | -webkit-background-clip: text; 120 | -webkit-text-fill-color: transparent; 121 | background-clip: text; 122 | } 123 | 124 | /* Animation Utilities */ 125 | .transition-all { 126 | transition: all var(--transition-base); 127 | } 128 | 129 | .transition-fast { 130 | transition: all var(--transition-fast); 131 | } 132 | 133 | .transition-slow { 134 | transition: all var(--transition-slow); 135 | } 136 | 137 | /* Hover Effects - Minimal */ 138 | .hover-scale { 139 | transition: transform var(--transition-fast); 140 | } 141 | 142 | .hover-scale:hover { 143 | transform: scale(1.01); 144 | } 145 | 146 | .hover-lift { 147 | transition: transform var(--transition-fast), box-shadow var(--transition-fast); 148 | } 149 | 150 | .hover-lift:hover { 151 | transform: translateY(-1px); 152 | box-shadow: var(--shadow-md); 153 | } 154 | 155 | .hover-glow-primary { 156 | transition: box-shadow var(--transition-base); 157 | } 158 | 159 | .hover-glow-primary:hover { 160 | box-shadow: var(--shadow-glow-primary); 161 | } 162 | 163 | .hover-glow-secondary { 164 | transition: box-shadow var(--transition-base); 165 | } 166 | 167 | .hover-glow-secondary:hover { 168 | box-shadow: var(--shadow-glow-secondary); 169 | } 170 | 171 | /* Pulse Animation */ 172 | @keyframes pulse { 173 | 0%, 100% { 174 | opacity: 1; 175 | transform: scale(1); 176 | } 177 | 50% { 178 | opacity: 0.9; 179 | transform: scale(1.02); 180 | } 181 | } 182 | 183 | .pulse { 184 | animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; 185 | } 186 | 187 | /* Fade Animations */ 188 | .fade-enter-active, 189 | .fade-leave-active { 190 | transition: opacity var(--transition-base); 191 | } 192 | 193 | .fade-enter-from, 194 | .fade-leave-to { 195 | opacity: 0; 196 | } 197 | 198 | /* Slide Animations */ 199 | .slide-up-enter-active, 200 | .slide-up-leave-active { 201 | transition: all var(--transition-base); 202 | } 203 | 204 | .slide-up-enter-from { 205 | opacity: 0; 206 | transform: translateY(20px); 207 | } 208 | 209 | .slide-up-leave-to { 210 | opacity: 0; 211 | transform: translateY(-20px); 212 | } 213 | 214 | /* Modern Card Styles - Minimal */ 215 | .modern-card { 216 | border-radius: var(--radius-md) !important; 217 | transition: all var(--transition-fast) !important; 218 | overflow: hidden; 219 | border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)); 220 | } 221 | 222 | .modern-card:hover { 223 | box-shadow: var(--shadow-md) !important; 224 | border-color: rgba(var(--v-theme-primary), 0.3); 225 | } 226 | 227 | /* Card Accents */ 228 | .result-card { 229 | border-top: 4px solid rgb(var(--v-theme-success)) !important; 230 | } 231 | 232 | .secret-key-card { 233 | border-left: 4px solid rgb(var(--v-theme-primary)) !important; 234 | } 235 | 236 | /* Modern Button Styles - Minimal */ 237 | .modern-btn-gradient { 238 | border-radius: var(--radius-md) !important; 239 | transition: all var(--transition-fast) !important; 240 | } 241 | 242 | .encrypt-btn { 243 | border-radius: var(--radius-md) !important; 244 | font-size: 1.1rem !important; 245 | font-weight: 600 !important; 246 | letter-spacing: 0.5px !important; 247 | text-transform: none !important; 248 | background: rgb(var(--v-theme-primary)) !important; 249 | color: rgb(var(--v-theme-on-primary)) !important; 250 | transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; 251 | box-shadow: 0 4px 12px rgba(var(--v-theme-primary), 0.25) !important; 252 | height: 56px !important; 253 | border: 1px solid rgba(255, 255, 255, 0.1) !important; 254 | } 255 | 256 | .encrypt-btn .v-icon { 257 | transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); 258 | } 259 | 260 | .encrypt-btn:hover:not(:disabled) { 261 | transform: translateY(-2px); 262 | box-shadow: 0 8px 20px rgba(var(--v-theme-primary), 0.35) !important; 263 | filter: brightness(1.05); 264 | } 265 | 266 | .encrypt-btn:hover .v-icon { 267 | transform: scale(1.1) rotate(-10deg); 268 | } 269 | 270 | .encrypt-btn:active:not(:disabled) { 271 | transform: translateY(0); 272 | box-shadow: 0 2px 8px rgba(var(--v-theme-primary), 0.25) !important; 273 | } 274 | 275 | 276 | 277 | /* Gradient Border */ 278 | .gradient-border { 279 | position: relative; 280 | border-radius: var(--radius-md); 281 | padding: 2px; 282 | background: var(--gradient-primary); 283 | } 284 | 285 | .gradient-border::before { 286 | content: ''; 287 | position: absolute; 288 | inset: 2px; 289 | background: rgb(var(--v-theme-surface)); 290 | border-radius: calc(var(--radius-md) - 2px); 291 | z-index: -1; 292 | } 293 | 294 | /* Smooth Scroll */ 295 | html { 296 | scroll-behavior: smooth; 297 | } 298 | 299 | /* Custom Scrollbar */ 300 | ::-webkit-scrollbar { 301 | width: 8px; 302 | height: 8px; 303 | } 304 | 305 | ::-webkit-scrollbar-track { 306 | background: rgba(0, 0, 0, 0.05); 307 | border-radius: var(--radius-full); 308 | } 309 | 310 | ::-webkit-scrollbar-thumb { 311 | background: var(--gradient-primary); 312 | border-radius: var(--radius-full); 313 | transition: background var(--transition-base); 314 | } 315 | 316 | ::-webkit-scrollbar-thumb:hover { 317 | background: var(--gradient-secondary); 318 | } 319 | 320 | /* Loading Shimmer Effect */ 321 | @keyframes shimmer { 322 | 0% { 323 | background-position: -1000px 0; 324 | } 325 | 100% { 326 | background-position: 1000px 0; 327 | } 328 | } 329 | 330 | .shimmer { 331 | animation: shimmer 2s linear infinite; 332 | background: linear-gradient( 333 | to right, 334 | rgba(255, 255, 255, 0) 0%, 335 | rgba(255, 255, 255, 0.3) 50%, 336 | rgba(255, 255, 255, 0) 100% 337 | ); 338 | background-size: 1000px 100%; 339 | } 340 | 341 | /* Focus Styles */ 342 | .v-field--focused { 343 | box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2) !important; 344 | } 345 | 346 | /* Modern Input Styles */ 347 | .modern-input .v-field__outline { 348 | --v-field-border-opacity: 0.15; 349 | } 350 | 351 | .modern-input.v-input--is-focused .v-field__outline { 352 | --v-field-border-opacity: 0.5; 353 | } 354 | 355 | .modern-input .v-field--focused { 356 | box-shadow: 0 0 0 4px rgba(0, 123, 255, 0.1); 357 | } 358 | 359 | /* Add colored left border on focus for better visibility */ 360 | .modern-input.v-input--is-focused .v-field__input { 361 | border-left: 3px solid rgb(var(--v-theme-primary)); 362 | padding-left: calc(var(--v-field-padding-start) - 3px); 363 | border-top-left-radius: 0; 364 | border-bottom-left-radius: 0; 365 | } 366 | 367 | .modern-input .v-field { 368 | border-radius: var(--radius-md) !important; 369 | transition: all var(--transition-base) !important; 370 | } 371 | 372 | .modern-input .v-field:hover { 373 | box-shadow: var(--shadow-sm) !important; 374 | } 375 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /ui/src/components/Secrets.vue: -------------------------------------------------------------------------------- 1 | 130 | 131 | 408 | 409 | 501 | --------------------------------------------------------------------------------